pax_global_header00006660000000000000000000000064146750505130014520gustar00rootroot0000000000000052 comment=5704120d25902119cb1139e04bca3db7742a9f73 goss-0.4.9/000077500000000000000000000000001467505051300125055ustar00rootroot00000000000000goss-0.4.9/.editorconfig000066400000000000000000000004341467505051300151630ustar00rootroot00000000000000# EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] indent_size = 4 [Makefile] indent_style = tab goss-0.4.9/.github/000077500000000000000000000000001467505051300140455ustar00rootroot00000000000000goss-0.4.9/.github/CODEOWNERS000066400000000000000000000000161467505051300154350ustar00rootroot00000000000000* @aelsabbahy goss-0.4.9/.github/CONTRIBUTING.md000066400000000000000000000071051467505051300163010ustar00rootroot00000000000000# Contributing to Goss Thank you for your interest in contributing to Goss. Goss wouldn't be where it is today if it wasn't for people like you. Some ways you can contribute: * Improve the [README](https://github.com/goss-org/goss/blob/master/README.md) and/or [Docs](https://github.com/goss-org/goss/blob/master/docs/). This makes it easier for new users to learn goss. * Vote on bugs and feature requests by adding a :+1: reaction to the inital post. * Create tutorials, blog posts and example use-cases on how to use Goss. * Help users with [questions](https://github.com/goss-org/goss/labels/question) tracker. * Fix verified [bugs](https://github.com/goss-org/goss/issues?q=is%3Aopen+is%3Aissue+label%3Aapproved+label%3Abug+sort%3Areactions-%2B1-desc). * Implement approved [feature requests](https://github.com/goss-org/goss/issues?q=is%3Aopen+is%3Aissue+label%3Aapproved+label%3Aenhancement+sort%3Areactions-%2B1-desc). * Spread the word. ## Features and bug reports and questions Please search the [issues](https://github.com/goss-org/goss/issues) page before opening a feature request or a bug report. If a feature or a bug report already exists, please thumbs up the initial post to indicate it's importance to you and raise it's priority. Please comment and contribute to said issue if you feel it's deficient. ## Bug reports If you think you found a bug in Goss, please submit a [bug report](https://github.com/goss-org/goss/issues). ## Feature requests If there's a feature you wish Goss would support, please open a feature request. Some things to note prior to opening a Goss feature request: * Goss is intended to be quick and easy to learn. * Goss is focused on the 20% of the 80/20 rule. In other words, Goss focuses on the 20% of features that cover the core aspects of OS testing and benefit 80% of users. * Goss is intended to test the local machine it's running on. Tests aren't intended to be used to validate remote systems or endpoints. * Goss provides a generic [command](https://goss.rocks/gossfile/#command) runner to allow users to cover more nuanced test cases. If you believe your feature adheres to the goals of Goss, please open a [feature request](https://github.com/goss-org/goss/issues) on GitHub which describes the feature you would like to see, why it is useful, and how it should work. Once a feature is submitted, it will be reviewed. Upon approval, the issue can be worked on and PRs can be submitted that implement this new feature. ## Contributing code and documentation changes If you have a bugfix or new feature that you would like to contribute to Goss, please find or open an issue about it first. Talk about what you would like to do. It may be that somebody is already working on it, or that there are particular issues that you should know about before implementing the change. We enjoy working with contributors to get their code accepted. There are many approaches to fixing a problem and it is important to find the best approach before writing too much code. Note that it is unlikely the project will merge refactors for the sake of refactoring or niche features that aren't common use-cases (see the feature request section above). These types of pull requests have a high cost to maintainers but provide little benefit to the community. Lastly, in order for a pull request to be merged, it must provide automated tests (unit and/or integration) proving the change works as intended, this also prevents future changes from introducing regressions. It would be quite odd for a testing tool to not have a healthy approach to test automation, after all. :smile: goss-0.4.9/.github/ISSUE_TEMPLATE/000077500000000000000000000000001467505051300162305ustar00rootroot00000000000000goss-0.4.9/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000011151467505051300207200ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** **How To Reproduce** **Expected Behavior** **Actual Behavior** **Environment:** - Version of goss - OS/Distribution version (if applicable) goss-0.4.9/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000023331467505051300217560ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Describe the feature:** **Describe the solution you'd like** **Describe alternatives you've considered** goss-0.4.9/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000003261467505051300204220ustar00rootroot00000000000000--- name: Question about: Ask a question about goss title: '' labels: question assignees: '' --- goss-0.4.9/.github/dependabot.yml000066400000000000000000000011001467505051300166650ustar00rootroot00000000000000--- # https://docs.github.com/en/github/administering-a-repository/enabling-and-disabling-version-updates version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" day: "saturday" assignees: - "aelsabbahy" reviewers: - "aelsabbahy" open-pull-requests-limit: 0 - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" day: "saturday" - package-ecosystem: "pip" directory: "/docs" schedule: interval: "weekly" day: "saturday" goss-0.4.9/.github/pull_request_template.md000066400000000000000000000016221467505051300210070ustar00rootroot00000000000000 ##### Checklist - [ ] `make test-all` (UNIX) passes. CI will also test this - [ ] unit and/or integration tests are included (if applicable) - [ ] documentation is changed or added (if applicable) ### Description of change goss-0.4.9/.github/stale.yml.disabled000066400000000000000000000012371467505051300174510ustar00rootroot00000000000000# Number of days of inactivity before an issue becomes stale daysUntilStale: 60 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - approved # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false goss-0.4.9/.github/workflows/000077500000000000000000000000001467505051300161025ustar00rootroot00000000000000goss-0.4.9/.github/workflows/docker-goss.yaml000066400000000000000000000072301467505051300212100ustar00rootroot00000000000000name: Docker image for Goss on: push: branches: - master tags: - "v*" workflow_dispatch: env: PLATFORMS: "linux/amd64,linux/arm64" jobs: goss: name: Build and push Docker image runs-on: ubuntu-latest permissions: packages: write contents: read security-events: write # To upload Trivy sarif files steps: - name: Checkout uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: images: | ghcr.io/${{ github.repository_owner }}/goss - name: Get latest git tag if: github.ref_name == 'master' id: get-latest-tag run: | # source: https://github.com/actions-ecosystem/action-get-latest-tag/blob/main/entrypoint.sh set -e git config --global --add safe.directory /github/workspace git fetch --tags --force # This suppress an error occurred when the repository is a complete one. git fetch --prune --unshallow 2>/dev/null || true latest_tag=$(git describe --abbrev=0 --tags || true) echo "tag=${latest_tag}" >> "$GITHUB_OUTPUT" echo "Latest tag: $latest_tag" - name: Set short git commit SHA if: github.ref_name == 'master' run: | calculatedSha=$(git rev-parse --short ${{ github.sha }}) echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV echo "COMMIT_SHORT_SHA: $calculatedSha" - name: Get the current version of Go from project. run: echo "GO_VERSION_FROM_PROJECT=$(go mod edit -json | jq -r .Go)" >> $GITHUB_ENV - name: Build master goss image if: github.ref_name == 'master' uses: docker/build-push-action@v6 with: build-args: | GO_VERSION=${{ env.GO_VERSION_FROM_PROJECT }} GOSS_VERSION=${{ steps.get-latest-tag.outputs.tag }}-${{ github.ref_name }}+${{ env.COMMIT_SHORT_SHA }} context: . push: true tags: | ghcr.io/${{ github.repository_owner }}/goss:master labels: ${{ steps.meta.outputs.labels }} platforms: ${{ env.PLATFORMS }} - name: Build release goss image if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') uses: docker/build-push-action@v6 with: build-args: | GO_VERSION=${{ env.GO_VERSION_FROM_PROJECT }} GOSS_VERSION=${{ github.ref_name }} context: . push: true tags: | ghcr.io/${{ github.repository_owner }}/goss:latest ghcr.io/${{ github.repository_owner }}/goss:${{ github.ref_name }} labels: ${{ steps.meta.outputs.labels }} platforms: ${{ env.PLATFORMS }} - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: image-ref: ghcr.io/${{ github.repository_owner }}/goss:master format: "sarif" output: "trivy-results.sarif" - name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@v3 with: sarif_file: "trivy-results.sarif" goss-0.4.9/.github/workflows/docker-integration-tests.yaml000066400000000000000000000047611467505051300237260ustar00rootroot00000000000000name: Docker images for integration tests on: # push: # branches: # - master workflow_dispatch: env: PLATFORMS: "linux/amd64" jobs: list-dockerfiles: name: Create list of existing dockerfiles runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Get file list id: set-matrix run: | # lists all Dockerfile_* and ignore (grep) files with extension (e.g. *.md5) # tranforms the file list in JSON array (StackOverflow#10234327) # converts the list into objects of dockerfile and image name ls integration-tests/Dockerfile_* | grep -Ev "\..{0,3}$" | jq -R -s 'split("\n")[:-1]' | jq '. | map({dockerfile: ., image: sub(".*_"; "")})' > filelist.json echo "matrix=$(jq -c . filelist.json)" >> "$GITHUB_OUTPUT" outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} docker: needs: [list-dockerfiles] name: Build and push Docker image runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: ${{ fromJson(needs.list-dockerfiles.outputs.matrix) }} permissions: packages: write contents: read steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: MD5 of Dockerfile id: md5_result run: | echo "md5=$(md5sum "${{ matrix.dockerfile }}" | awk '{ print $1 }')" >> $GITHUB_OUTPUT - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: images: | ghcr.io/${{ github.repository_owner }}/${{ matrix.image }} labels: | rocks.goss.dockerfile-md5=${{ steps.md5_result.outputs.md5 }} - name: Build and push tag uses: docker/build-push-action@v6 with: context: . file: ${{ matrix.dockerfile }} push: true tags: | ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:latest labels: ${{ steps.meta.outputs.labels }} platforms: ${{ env.PLATFORMS }} goss-0.4.9/.github/workflows/docs.yaml000066400000000000000000000021521467505051300177160ustar00rootroot00000000000000name: Documentation on: push: branches: - master pull_request: paths: - mkdocs.yml - docs/** - README.md - LICENSE - extras/**/README.md - .github/CONTRIBUTING.md workflow_dispatch: jobs: lint: name: Lint Documentation runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: DavidAnson/markdownlint-cli2-action@v16 with: globs: | docs/**/*.md README.md extras/**/README.md .github/CONTRIBUTING.md build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" cache: 'pip' - name: Install dependencies run: | pip install --upgrade pip pip install --requirement docs/requirements.txt - name: Build documentation run: mkdocs build # To remove if not using github pages - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: site goss-0.4.9/.github/workflows/golangci.yaml000066400000000000000000000027351467505051300205600ustar00rootroot00000000000000name: Golang ci on: # don't build any branch other than master (and prs) when git pushed pull_request: {} push: branches: - master - "/^v\\d+\\.\\d+(\\.\\d+)?(-\\S*)?$/" paths-ignore: - "**/*.md" permissions: contents: read pull-requests: read jobs: lint: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: version: v1.59 coverage: needs: [lint] name: coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Unit tests and coverage run: make cov integration-test: needs: [coverage] name: Integration tests runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Integration tests shell: bash run: | os_name="$(go env GOOS)" if [[ "${os_name}" == "darwin" || "${os_name}" == "windows" ]]; then make "test-int-${os_name}-all" else # linux runs all tests; make test-int-all fi goss-0.4.9/.github/workflows/preview-docs.yaml000066400000000000000000000007171467505051300214020ustar00rootroot00000000000000name: Preview documentation on: pull_request_target: types: - opened paths: - mkdocs.yml - docs/** - README.md - LICENSE - extras/**/README.md - .github/CONTRIBUTING.md jobs: pull-request-links: name: Add preview link to pull-request runs-on: ubuntu-latest permissions: pull-requests: write steps: - uses: readthedocs/actions/preview@v1 with: project-slug: goss goss-0.4.9/.github/workflows/release.yaml000066400000000000000000000023651467505051300204140ustar00rootroot00000000000000name: "Build release artifacts" on: push: tags: - "v*" workflow_dispatch: permissions: contents: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Get version from tag if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') run: echo "TRAVIS_TAG=${{ github.ref_name }}" >> $GITHUB_ENV - run: make release - run: make dgoss-sha256 dcgoss-sha256 kgoss-sha256 - name: "Upload binary as artifact" uses: actions/upload-artifact@v4 with: retention-days: 5 if-no-files-found: error name: build path: | release/* extras/*/*goss extras/*/*goss.sha256 attach-assets: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: ["build"] runs-on: ubuntu-latest steps: - name: Fetch all binaries uses: actions/download-artifact@v4 - name: Attach to release uses: softprops/action-gh-release@v2 with: files: build/** fail_on_unmatched_files: true goss-0.4.9/.gitignore000066400000000000000000000005631467505051300145010ustar00rootroot00000000000000*.swp /main *.bak /goss /release /integration-tests/goss/goss /integration-tests/**/*-generated* /vendor/ /integration-tests/**/goss-linux-386 /integration-tests/**/goss-linux-amd64 # Random stuff for my local testing/development that I don't want checked in tmp/ /goss.yaml /.idea /c.out /c.out.tmp # Documentation ## Virtualenv /.venv ## MkDocs rendered site /site goss-0.4.9/.golangci.yaml000066400000000000000000000005531467505051300152350ustar00rootroot00000000000000linters: # Disable all linters. # Default: false disable-all: true # Enable specific linter # https://golangci-lint.run/usage/linters/#enabled-by-default enable: # default linter # - errcheck # there are to many failures at the moment - gosimple - govet - ineffassign - staticcheck - unused # custom linter - gofmt goss-0.4.9/.markdownlint.yaml000066400000000000000000000022601467505051300161600ustar00rootroot00000000000000--- # Enable all rules default: true # Enforce asterisk for unordered lists # See: https://github.com/DavidAnson/markdownlint/blob/main/doc/md004.md MD004: style: asterisk # Set list indent level to 4 which Python-Markdown requires # See: # - https://github.com/DavidAnson/markdownlint/blob/main/doc/md007.md # - https://python-markdown.github.io/#differences MD007: indent: 4 # Tune `line-length` # See: https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md MD013: line_length: 120 tables: false code_blocks: false # Disable `blanks-around-list` (to stay close from GitHub-flavored markdown) # See: # - https://github.com/DavidAnson/markdownlint/blob/main/doc/md032.md # - https://python-markdown.github.io/#differences MD032: false # Disable `no-space-in-code` # Generate lots of false positive with admonitions and code blocks MD038: false # Disable `code-blocks-style` # Use fenced code blocks everywhere but raise false positives with admonitions MD046: false # Disable `link-fragments` # Only works for github-rendered markdown (which does not have the same rules as MkDocs) # See: https://github.com/DavidAnson/markdownlint/blob/main/doc/md051.md MD051: false goss-0.4.9/.readthedocs.yaml000066400000000000000000000006661467505051300157440ustar00rootroot00000000000000# Read the Docs configuration file for MkDocs projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.12" mkdocs: configuration: mkdocs.yml # Optionally declare the Python requirements required to build your docs python: install: - requirements: docs/requirements.txt goss-0.4.9/.travis.yml000066400000000000000000000040311467505051300146140ustar00rootroot00000000000000--- language: go go: - 1.23.x os: - osx - linux - windows dist: focal osx_image: xcode12.2 services: - docker # don't build any branch other than master (and prs) when git pushed branches: only: - master - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ before_install: - if [[ "${TRAVIS_OS_NAME}" == "windows" ]]; then choco install make; fi # bash from macOS is too old to have readarray. Install newer version. - if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then HOMEBREW_NO_AUTO_UPDATE=1 brew install bash; fi script: - ./ci/build.sh # deploy: # provider: releases # api_key: # secure: ijNltjw/mIHIOx8vLZ6asUun3SbY7D+XZbs5NX8vcIv0jvOiwaaT1hqny7SQBHfGZzqHsYUSS/GYAYJdBqKFFfGmTZsl90hFT6D0RGdz9C71UVxNFX4wQ5KQ/WVvdMT2SrLymGvu9TvoU0VG8OWqWVdxSlUPf6qOTGAagrzg+Tbsbb6czeiG67mlBBL23XSlfMG1p45UxzvI41SZj2R3ElUb0hym1CrFaoC36PBGrb0x41TXzvd8J7cu6xDzgczYhnYQQZpS6f2YcqNV1z0f+P67EQqQiDWIIcK2jE/YG+RgM8cbpLMiMec8CDiwNCsejBA5EbVMlGJlODvBXT5NmMBeugueqfSHEfkl5qZTQG4AOAT7UsqbnM7r0NqzmaE5Lj90igvJK6rNsH1ZRe79WfSsTtuzlkkouHGvyoz0M8gnMSzpbbwoyIy+UT0hhPMoZvIpXfr43en5WkbkPKfop0p4Vjc8NGg0iD45q1JAvIVTtz/WvWTknM1P8e3u+TiDTaZkcJJmFaBqgaeLoWktOGfi54p9nhgQnSyBYt4PyvhWDQs7QFmX0BdKlqJCESvUOJTe1t6zJJsV7Gn/3sGCN7JUEwbnXTsCoMjjFFUvQdm0Ur7t7/2xU3kO+dyfqcdM/5SYFeppQcjHI0ckhI51mIoBTsJsGvaVwKKL1I4cyBU= # file: # - release/goss-darwin-amd64 # - release/goss-darwin-amd64.sha256 # - release/goss-darwin-arm64 # - release/goss-darwin-arm64.sha256 # - release/goss-linux-amd64 # - release/goss-linux-amd64.sha256 # - release/goss-linux-386 # - release/goss-linux-386.sha256 # - release/goss-linux-arm # - release/goss-linux-arm.sha256 # - release/goss-linux-arm64 # - release/goss-linux-arm64.sha256 # - release/goss-linux-s390x # - release/goss-linux-s390x.sha256 # - release/goss-windows-amd64.exe # - release/goss-windows-amd64.exe.sha256 # - extras/dgoss/dgoss # - extras/dgoss/dgoss.sha256 # skip_cleanup: true # on: # repo: goss-org/goss # tags: true # condition: $TRAVIS_OS_NAME = linux goss-0.4.9/.yamllint000066400000000000000000000001351467505051300143360ustar00rootroot00000000000000--- extends: default rules: line-length: disable truthy: allowed-values: - on goss-0.4.9/Dockerfile000066400000000000000000000005641467505051300145040ustar00rootroot00000000000000ARG GO_VERSION=1.22 FROM docker.io/golang:${GO_VERSION}-alpine AS base ARG GOSS_VERSION=v0.0.0 WORKDIR /build RUN --mount=target=. \ CGO_ENABLED=0 go build \ -ldflags "-X github.com/goss-org/goss/util.Version=${GOSS_VERSION} -s -w" \ -o "/release/goss" \ ./cmd/goss FROM alpine:3.19 COPY --from=base /release/* /usr/bin/ RUN mkdir /goss VOLUME /goss goss-0.4.9/LICENSE000066400000000000000000000261211467505051300135140ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2015 Ahmed Elsabbahy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. goss-0.4.9/Makefile000066400000000000000000000114271467505051300141520ustar00rootroot00000000000000export GO15VENDOREXPERIMENT=1 exe = github.com/goss-org/goss/cmd/goss pkgs = $(shell ./novendor.sh) cmd = goss GO111MODULE=on GO_FILES = $(shell git ls-files -- '*.go' ':!:*vendor*_test.go') VENV := $(shell echo $${VIRTUAL_ENV-.venv}) PYTHON := $(VENV)/bin/python DOCS_DEPS := $(VENV)/.docs.dependencies .PHONY: all build install test release bench fmt lint vet test-int-all gen centos7 wheezy trusty alpine3 arch test-int32 centos7-32 wheezy-32 trusty-32 alpine3-32 arch-32 all: test-short-all test-int-all dgoss-sha256 dcgoss-sha256 kgoss-sha256 test-short-all: fmt lint vet test install: release/goss-linux-amd64 $(info INFO: Starting build $@) cp release/$(cmd)-linux-amd64 $(GOPATH)/bin/goss test: $(info INFO: Starting build $@) ./ci/go-test.sh cov: go test -coverpkg=./... -coverprofile=c.out ./... # go tool cover -func ./c.out funcov: go test -coverpkg=./... -coverprofile=c.out ./... go tool cover -func ./c.out htmlcov: go test -v -coverpkg=./... -coverprofile=c.out ./... go tool cover -html ./c.out lint: $(info INFO: Starting build $@) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59 golangci-lint run --timeout 5m $(pkgs) || true vet: $(info INFO: Starting build $@) go vet $(pkgs) || true fmt: $(info INFO: Starting build $@) ./ci/go-fmt.sh bench: $(info INFO: Starting build $@) go test -bench=. test-int-validate-%: release/goss-% $(info INFO: Starting build $@) ./integration-tests/run-validate-tests.sh $* test-int-serve-%: release/goss-% $(info INFO: Starting build $@) ./integration-tests/run-serve-tests.sh $* release/goss-%: $(GO_FILES) ./release-build.sh $* release: $(MAKE) clean $(MAKE) build build: release/goss-darwin-amd64 release/goss-darwin-arm64 release/goss-linux-386 release/goss-linux-amd64 release/goss-linux-arm release/goss-linux-arm64 release/goss-linux-s390x release/goss-windows-amd64 gen: $(info INFO: Starting build $@) go generate -tags genny $(pkgs) clean: $(info INFO: Starting build $@) rm -rf ./release rm -rf ./site rm -rf ${VENV} build-images: $(info INFO: Starting build $@) development/build_images.sh push-images: $(info INFO: Starting build $@) development/push_images.sh # Update the matcher test golden files update-matcher-tests: go test -v -run '^TestMatchers' . -update test-darwin-all: test-short-all test-int-darwin-all # linux _does_ have the docker-style testing, but does _not_ currently have the same style integration tests darwin+windows do, _because_ of the docker-style testing. test-linux-all: test-short-all test-int-64 test-int-32 test-windows-all: test-short-all test-int-windows-all test-int-64: rockylinux9 wheezy trusty alpine3 arch test-int-serve-linux-amd64 test-int-32: rockylinux9-32 wheezy-32 trusty-32 alpine3-32 arch-32 test-int-darwin-all: test-int-validate-darwin-amd64 test-int-serve-darwin-amd64 test-int-windows-all: test-int-validate-windows-amd64 test-int-serve-windows-amd64 test-int-all: test-int-32 test-int-64 centos7-32: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh centos7 386 .PHONY: rockylinux9-32 rockylinux9-32: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh rockylinux9 386 wheezy-32: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh wheezy 386 trusty-32: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh trusty 386 alpine3-32: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh alpine3 386 arch-32: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh arch 386 centos7: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh centos7 amd64 .PHONY: rockylinux9 rockylinux9: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh rockylinux9 amd64 wheezy: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh wheezy amd64 trusty: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh trusty amd64 alpine3: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh alpine3 amd64 arch: build $(info INFO: Starting build $@) cd integration-tests/ && ./test.sh arch amd64 dgoss-sha256: cd extras/dgoss/ && sha256sum dgoss > dgoss.sha256 dcgoss-sha256: cd extras/dcgoss/ && sha256sum dcgoss > dcgoss.sha256 kgoss-sha256: cd extras/kgoss/ && sha256sum kgoss > kgoss.sha256 $(PYTHON): $(info Creating virtualenv in $(VENV)) @python -m venv $(VENV) $(DOCS_DEPS): $(PYTHON) docs/requirements.txt $(info Installing dependencies) @pip install --upgrade pip @pip install --requirement docs/requirements.txt @touch $(DOCS_DEPS) docs/setup: $(DOCS_DEPS) docs/serve: docs/setup $(info Running documentation live development server) @mkdocs serve --strict .PHONY: docs docs: docs/setup $(info Building documentation) @mkdocs build --strict goss-0.4.9/README.md000066400000000000000000000251771467505051300140000ustar00rootroot00000000000000# Goss - Quick and Easy server validation [![Build Status](https://travis-ci.org/goss-org/goss.svg?branch=master)](https://travis-ci.org/goss-org/goss) [![Github All Releases](https://img.shields.io/github/downloads/goss-org/goss/total.svg?maxAge=604800)](https://github.com/goss-org/goss/releases) [![Documentation Status](https://readthedocs.org/projects/goss/badge/)](https://goss.rocks/) ** [![Blog](https://img.shields.io/badge/follow-blog-brightgreen.svg)](https://medium.com/@aelsabbahy) ## Goss in 45 seconds asciicast **Note:** For testing containers see the [dgoss](https://github.com/goss-org/goss/tree/master/extras/dgoss) wrapper. Also, user submitted wrapper scripts for Kubernetes [kgoss](https://github.com/goss-org/goss/tree/master/extras/kgoss) and Docker Compose [dcgoss](https://github.com/goss-org/goss/tree/master/extras/dcgoss). **Note:** For some Docker/Kubernetes healthcheck, health endpoint, and container ordering examples, see my blog post [here][kubernetes-simplified-health-checks]. ## Introduction ### What is Goss? Goss is a YAML based [serverspec](http://serverspec.org/) alternative tool for validating a server's configuration. It eases the process of writing tests by allowing the user to generate tests from the current system state. Once the test suite is written they can be executed, waited-on, or served as a health endpoint. ### Why use Goss? * Goss is EASY! - [Goss in 45 seconds](#goss-in-45-seconds) * Goss is FAST! - small-medium test suites are near instantaneous, see [benchmarks](https://github.com/goss-org/goss/wiki/Benchmarks) * Goss is SMALL! - <10MB single self-contained binary ## Installation **Note:** For macOS and Windows, see: [platform-feature-parity]. This will install goss and [dgoss](https://github.com/goss-org/goss/tree/master/extras/dgoss). **Note:** Using `curl | sh` is not recommended for production systems, use manual installation below. ```bash # Install latest version to /usr/local/bin curl -fsSL https://goss.rocks/install | sh # Install v0.4.8 version to ~/bin curl -fsSL https://goss.rocks/install | GOSS_VER=v0.4.8 GOSS_DST=~/bin sh ``` ### Manual installation #### Latest ```bash curl -L https://github.com/goss-org/goss/releases/latest/download/goss-linux-amd64 -o /usr/local/bin/goss chmod +rx /usr/local/bin/goss curl -L https://github.com/goss-org/goss/releases/latest/download/dgoss -o /usr/local/bin/dgoss # Alternatively, using the latest master # curl -L https://raw.githubusercontent.com/goss-org/goss/master/extras/dgoss/dgoss -o /usr/local/bin/dgoss chmod +rx /usr/local/bin/dgoss ``` #### Specific Version ```bash # See https://github.com/goss-org/goss/releases for release versions VERSION=v0.4.8 curl -L "https://github.com/goss-org/goss/releases/download/${VERSION}/goss-linux-amd64" -o /usr/local/bin/goss chmod +rx /usr/local/bin/goss # (optional) dgoss docker wrapper (use 'master' for latest version) VERSION=v0.4.8 curl -L "https://github.com/goss-org/goss/releases/download/${VERSION}/dgoss" -o /usr/local/bin/dgoss chmod +rx /usr/local/bin/dgoss ``` ### Build it yourself ```bash make build ``` ## Full Documentation [Full Documentation](https://goss.readthedocs.io/en/stable/) ## Using the container image [Using the Goss container image](https://goss.readthedocs.io/en/stable/container_image/) ## Quick start ### Writing a simple sshd test An initial set of tests can be derived from the system state by using the [add](https://goss.rocks/cli/#add) or [autoadd](https://goss.rocks/cli/#autoadd) commands. Let's write a simple sshd test using autoadd. ```txt # Running it as root will allow it to also detect ports $ sudo goss autoadd sshd ``` Generated `goss.yaml`: ```yaml port: tcp:22: listening: true ip: - 0.0.0.0 tcp6:22: listening: true ip: - '::' service: sshd: enabled: true running: true user: sshd: exists: true uid: 74 gid: 74 groups: - sshd home: /var/empty/sshd shell: /sbin/nologin group: sshd: exists: true gid: 74 process: sshd: running: true ``` Now that we have a test suite, we can: * Run it once ```console $ goss validate ............... Total Duration: 0.021s # <- yeah, it's that fast.. Count: 15, Failed: 0 ``` * Edit it to use [templates](https://goss.rocks/gossfile/#templates), and run with a vars file ```console goss --vars vars.yaml validate ``` * keep running it until the system enters a valid state or we timeout ```console goss validate --retry-timeout 30s --sleep 1s ``` * serve the tests as a health endpoint ```console $ goss serve & $ curl localhost:8080/healthz # JSON endpoint $ goss serve --format json & $ curl localhost:8080/healthz # rspecish response via content negotiation $ goss serve --format json & $ curl -H "Accept: application/vnd.goss-rspecish" localhost:8080/healthz ``` ### Manually editing Goss files Goss files can be manually edited to improve readability and expressiveness of tests. A [Json draft 7 schema](https://github.com/json-schema-org/json-schema-spec/blob/draft-07/schema.json) available at makes it easier to edit simple goss.yaml files in IDEs, providing usual coding assistance such as inline documentation, completion and static analysis. See #793 for screenshots. For example, to configure the Json schema in JetBrains intellij IDEA, follow [documented instructions](https://www.jetbrains.com/help/idea/json.html#ws_json_schema_add_custom), with arguments such as: * `schema url=https://goss.rocks/schema.yaml` * `schema version=Json schema version 7` * `file path pattern=*/goss.yaml` In addition, Goss files can also be further manually edited (without yet full json support) to use: * [Patterns](https://goss.rocks/gossfile/#patterns) * [Advanced Matchers](https://goss.rocks/gossfile/#advanced-matchers) * [Templates](https://goss.rocks/gossfile/#templates) * `title` and `meta` (arbitrary data) attributes are persisted when adding other resources with `goss add` Some examples: ```yaml+jinja user: sshd: title: UID must be between 50-100, GID doesn't matter. home is flexible meta: desc: Ensure sshd is enabled and running since it's needed for system management sev: 5 exists: true uid: # Validate that UID is between 50 and 100 and: gt: 50 lt: 100 home: # Home can be any of the following or: - /var/empty/sshd - /var/run/sshd package: kernel: installed: true versions: # Must have 3 kernels and none of them can be 4.4.0 and: - have-len: 3 - not: contain-element: 4.4.0 # Loaded from --vars YAML/JSON file {{.Vars.package}}: installed: true {{if eq .Env.OS "centos"}} # This test is only when $OS environment variable is set to "centos" libselinux: installed: true {{end}} ``` Goss.yaml files with templates can still be validated through the Json schema after being rendered using the `goss render` command. See example below ```console $ cd docs $ goss --vars ./vars.yaml render > rendered_goss.yaml # proceed with json schema validation of rendered_goss.yaml in your favorite IDE # or in one of the Json schema validator listed in https://json-schema.org/implementations.html # The following example is for a Linux AMD64 host $ curl -LO https://github.com/neilpa/yajsv/releases/download/v1.4.1/yajsv.linux.amd64 $ chmod a+x yajsv.linux.amd64 $ sudo mv yajsv.linux.amd64 /usr/sbin/yajsv $ yajsv -s goss-json-schema.yaml rendered_goss.yaml rendered_goss.yaml: fail: process.chrome: skip is required rendered_goss.yaml: fail: service.sshd: skip is required 1 of 1 failed validation rendered_goss.yaml: fail: process.chrome: skip is required rendered_goss.yaml: fail: service.sshd: skip is required ``` Full list of available Json schema validators can be found in ## Supported resources * package - add new package * file - add new file * addr - add new remote address:port - ex: google.com:80 * port - add new listening [protocol]:port - ex: 80 or udp:123 * service - add new service * user - add new user * group - add new group * command - add new command * dns - add new dns * process - add new process name * kernel-param - add new kernel-param * mount - add new mount * interface - add new network interface * http - add new network http url with proxy support * goss - add new goss file, it will be imported from this one * matching - test for matches in supplied content ## Supported output formats * rspecish - **(default)** Similar to rspec output * documentation - Verbose test results * json - JSON, detailed test result * tap - TAP style * junit - JUnit style * nagios - Nagios/Sensu compatible output /w exit code 2 for failures. * prometheus - Prometheus compatible output. * silent - No output. Avoids exposing system information (e.g. when serving tests as a healthcheck endpoint). ## Community Contributions * [goss-ansible](https://github.com/indusbox/goss-ansible) - Ansible module for Goss. * [degoss](https://github.com/naftulikay/ansible-role-degoss) - Ansible role for installing, running, and removing Goss in a single go. * [kitchen-goss](https://github.com/ahelal/kitchen-goss) - A test-kitchen verifier plugin for Goss. * [goss-fpm-files](https://github.com/deanwilson/unixdaemon-fpm-cookery-recipes) - Might be useful for building goss system packages. * [packer-provisioner-goss](https://github.com/YaleUniversity/packer-provisioner-goss) - A packer plugin to run Goss as a provision step. * [gossboss](https://github.com/mdb/gossboss) - Collect and view aggregated Goss test results from multiple remote Goss servers. ## Limitations `goss` works well on Linux, but support on Windows & macOS is alpha. See [platform-feature-parity]. The following tests have limitations. Package: * rpm * deb * Alpine apk * pacman Service: * systemd * sysV init * OpenRC init * Upstart [kubernetes-simplified-health-checks]: https://medium.com/@aelsabbahy/docker-1-12-kubernetes-simplified-health-checks-and-container-ordering-with-goss-fa8debbe676c [platform-feature-parity]: https://goss.rocks/platforms goss-0.4.9/add.go000066400000000000000000000127231467505051300135710ustar00rootroot00000000000000package goss import ( "fmt" "os" "strconv" "strings" "github.com/goss-org/goss/resource" "github.com/goss-org/goss/system" "github.com/goss-org/goss/util" ) // AddResources is a simple wrapper to add multiple resources func AddResources(fileName, resourceName string, keys []string, c *util.Config) error { var err error err = setLogLevel(c) if err != nil { return err } outStoreFormat, err = getStoreFormatFromFileName(fileName) if err != nil { return err } var gossConfig GossConfig if _, err := os.Stat(fileName); err == nil { gossConfig, err = ReadJSON(fileName) if err != nil { return err } } else { gossConfig = *NewGossConfig() } sys := system.New(c.PackageManager) for _, key := range keys { if err := AddResource(fileName, gossConfig, resourceName, key, *c, sys); err != nil { return err } } return WriteJSON(fileName, gossConfig) } // AddResource adds a single resource to fileName func AddResource(fileName string, gossConfig GossConfig, resourceName, key string, config util.Config, sys *system.System) error { var err error var res resource.ResourceRead // Need to figure out a good way to refactor this switch resourceName { case resource.AddResourceName: res, err = gossConfig.Addrs.AppendSysResource(key, sys, config) case resource.CommandResourceName: res, err = gossConfig.Commands.AppendSysResource(key, sys, config) case resource.DNSResourceName: res, err = gossConfig.DNS.AppendSysResource(key, sys, config) case resource.FileResourceName: res, err = gossConfig.Files.AppendSysResource(key, sys, config) case resource.GroupResourceName: res, err = gossConfig.Groups.AppendSysResource(key, sys, config) case resource.PackageResourceName: res, err = gossConfig.Packages.AppendSysResource(key, sys, config) case resource.PortResourceName: res, err = gossConfig.Ports.AppendSysResource(key, sys, config) case resource.ProcessResourceName: res, err = gossConfig.Processes.AppendSysResource(key, sys, config) case resource.ServiceResourceName: res, err = gossConfig.Services.AppendSysResource(key, sys, config) case resource.UserResourceName: res, err = gossConfig.Users.AppendSysResource(key, sys, config) case resource.GossFileResourceName: res, err = gossConfig.Gossfiles.AppendSysResource(key, sys, config) case resource.KernelParamResourceName: res, err = gossConfig.KernelParams.AppendSysResource(key, sys, config) case resource.MountResourceName: res, err = gossConfig.Mounts.AppendSysResource(key, sys, config) case resource.InterfaceResourceName: res, err = gossConfig.Interfaces.AppendSysResource(key, sys, config) case resource.HTTPResourceName: res, err = gossConfig.HTTPs.AppendSysResource(key, sys, config) default: err = fmt.Errorf("undefined resource name: %s", resourceName) } if err != nil { return err } resourcePrint(fileName, res, config.AnnounceToCLI) return nil } // AutoAddResources is a simple wrapper to add multiple resources func AutoAddResources(fileName string, keys []string, c *util.Config) error { var err error outStoreFormat, err = getStoreFormatFromFileName(fileName) if err != nil { return err } var gossConfig GossConfig if _, err = os.Stat(fileName); err == nil { gossConfig, err = ReadJSON(fileName) if err != nil { return err } } else { gossConfig = *NewGossConfig() } sys := system.New(c.PackageManager) for _, key := range keys { if err := AutoAddResource(fileName, gossConfig, key, c, sys); err != nil { return err } } return WriteJSON(fileName, gossConfig) } // AutoAddResource adds a single resource to fileName with automatic detection of the type of resource func AutoAddResource(fileName string, gossConfig GossConfig, key string, c *util.Config, sys *system.System) error { // file if strings.Contains(key, "/") { res, _, ok, err := gossConfig.Files.AppendSysResourceIfExists(key, sys) if err != nil { return err } if ok { resourcePrint(fileName, res, c.AnnounceToCLI) } } // group if res, _, ok, err := gossConfig.Groups.AppendSysResourceIfExists(key, sys); err != nil { return err } else if ok { resourcePrint(fileName, res, c.AnnounceToCLI) } // package if res, _, ok, err := gossConfig.Packages.AppendSysResourceIfExists(key, sys); err != nil { return err } else if ok { resourcePrint(fileName, res, c.AnnounceToCLI) } // port if res, _, ok, err := gossConfig.Ports.AppendSysResourceIfExists(key, sys); err != nil { return err } else if ok { resourcePrint(fileName, res, c.AnnounceToCLI) } // process if res, sysres, ok, err := gossConfig.Processes.AppendSysResourceIfExists(key, sys); err != nil { return err } else if ok { resourcePrint(fileName, res, c.AnnounceToCLI) ports := system.GetPorts(true) pids, _ := sysres.Pids() for _, pid := range pids { pidS := strconv.Itoa(pid) for port, entries := range ports { for _, entry := range entries { if entry.Pid == pidS { // port if res, _, ok, err := gossConfig.Ports.AppendSysResourceIfExists(port, sys); err != nil { return err } else if ok { resourcePrint(fileName, res, c.AnnounceToCLI) } } } } } } // Service if res, _, ok, err := gossConfig.Services.AppendSysResourceIfExists(key, sys); err != nil { return err } else if ok { resourcePrint(fileName, res, c.AnnounceToCLI) } // user if res, _, ok, err := gossConfig.Users.AppendSysResourceIfExists(key, sys); err != nil { return err } else if ok { resourcePrint(fileName, res, c.AnnounceToCLI) } return nil } goss-0.4.9/ci/000077500000000000000000000000001467505051300131005ustar00rootroot00000000000000goss-0.4.9/ci/build.sh000077500000000000000000000006071467505051300145410ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail os_name="$(go env GOOS)" # darwin & windows do not support integration-testing approach via docker. # platform support is coupled to the travis CI environment, which is stable 'enough'. if [[ "${os_name}" == "darwin" || "${os_name}" == "windows" ]]; then make "test-${os_name}-all" else # linux runs all tests; unit and integration. make all fi goss-0.4.9/ci/go-fmt.sh000077500000000000000000000007301467505051300146300ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail os_name="$(go env GOOS)" # gofmt must be on PATH command -v gofmt if [[ "${os_name}" == "windows" ]]; then echo "Skipping go-fmt on Windows because line-endings cause every file to need formatting." echo "Linux is treated as authoritative." echo "Exiting 0..." exit 0 fi fmt="$(go fmt github.com/goss-org/goss/...)" if [[ -z "${fmt}" ]]; then echo "valid gofmt" else echo "invalid gofmt:" echo "${fmt}" exit 1 fi goss-0.4.9/ci/go-test.sh000077500000000000000000000002751467505051300150250ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail command -v go go test -coverpkg=./... ./... -coverprofile="c.out" sed 's|github.com/goss-org/goss/||' <"c.out" >"c.out.tmp" mv "c.out.tmp" "c.out" goss-0.4.9/ci/lib/000077500000000000000000000000001467505051300136465ustar00rootroot00000000000000goss-0.4.9/ci/lib/log.sh000066400000000000000000000020741467505051300147660ustar00rootroot00000000000000#!/usr/bin/env bash # comment out the unused-ones so far until they're needed. Otherwise it's a google search to find them again. NOCOLOUR='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' # BLUE='\033[0;34m' # PURPLE='\033[0;35m' CYAN='\033[0;36m' # LIGHTGRAY='\033[0;37m' # DARKGRAY='\033[1;30m' LIGHTRED='\033[1;31m' LIGHTGREEN='\033[1;32m' # YELLOW='\033[1;33m' # LIGHTBLUE='\033[1;34m' # LIGHTPURPLE='\033[1;35m' LIGHTCYAN='\033[1;36m' # WHITE='\033[1;37m' is_ci() { if [[ "${CI:-}" == "true" ]]; then echo "true" else echo "false" fi } log_action() { echo -e "${LIGHTGREEN}${*}${NOCOLOUR}" >&2 } log_warn() { echo -e "${ORANGE}${*}${NOCOLOUR}" >&2 } log_error() { echo -e "${LIGHTRED}${*}${NOCOLOUR}" >&2 } log_debug() { if [[ -n "${SCRIPT_LOG_LEVEL:-}" && "${SCRIPT_LOG_LEVEL}" == "debug" ]]; then echo -e "${CYAN}${*}${NOCOLOUR}" >&2 fi } log_info() { echo -e "${LIGHTCYAN}${*}${NOCOLOUR}" >&2 } log_success() { echo -e "${GREEN}${*}${NOCOLOUR}" >&2 } log_fatal() { echo -e "${RED}${*}${NOCOLOUR}" >&2 exit "${2:-"1"}" } goss-0.4.9/ci/lib/setup.sh000066400000000000000000000046121467505051300153450ustar00rootroot00000000000000# configure cwd, vars and logging _setup_env() { # -ET: propagate DEBUG/RETURN/ERR traps to functions and subshells set -ET # exit on unhandled error set -o errexit # exit on unset variable set -o nounset # pipefail: any failure in a pipe causes the pipe to fail set -o pipefail if [[ -n "${SCRIPT_DEBUG:-}" ]]; then set -o xtrace # http://www.skybert.net/bash/debugging-bash-scripts-on-the-command-line/ export PS4='# ${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]:-}() - [${SHLVL},${BASH_SUBSHELL},$?] ' fi trap _err_trap ERR # shellcheck disable=SC2034 # START_DIR is used elsewhere. START_DIR="${PWD}" export START_DIR readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[2]}")" && pwd)" readonly TOP_SCRIPT="${SCRIPT_DIR}/$(basename "${BASH_SOURCE[2]}")" if [[ -z "${SCRIPT_DIR}" ]]; then echo >&2 -e "setup.sh:\tFailed to determine directory containing executed script." return 1 fi if ! cd "$(dirname "${BASH_SOURCE[0]}")/../.."; then echo >&2 -e "setup.sh:\tFailed to cd to repository root" return 1 fi REPO_ROOT="$(pwd)" export REPO_ROOT if ! source ci/lib/log.sh; then echo >&2 -e "setup.sh:\tFailed to source logging library" return 1 fi } _err_trap() { local err=$? local cmd="${BASH_COMMAND:-}" # Disable echoing all commands as this makes the traceback really hard to follow set +x if [[ -n "${SKIP_BASH_STACKTRACE:-}" ]]; then log_debug "SKIP_BASH_STACKTRACE was set to something; silencing bash stack-trace." exit "${err}" fi echo >&2 "panic: uncaught error" 1>&2 print_traceback 1 echo >&2 "${cmd} exited ${err}" 1>&2 } _setup_constants() { export EXIT_SUCCESS=0 export EXIT_INVALID_ARGUMENT=66 export EXIT_FAILED_TO_SOURCE=67 export EXIT_FAILED_TO_CD=68 export EXIT_FAILED_AFTER_RETRY=69 export EXIT_NOT_FOUND=70 } # Print traceback of call stack, starting from the call location. # An optional argument can specify how many additional stack frames to skip. print_traceback() { local skip=${1:-0} local start=$((skip + 1)) local end=${#BASH_SOURCE[@]} local curr=0 echo >&2 "Traceback (most recent call first):" 1>&2 for ((curr = start; curr < end; curr++)); do local prev=$((curr - 1)) local func="${FUNCNAME[$curr]}" local file="${BASH_SOURCE[$curr]}" local line="${BASH_LINENO[$prev]}" echo >&2 " at ${file}:${line} in ${func}()" 1>&2 done } _setup_env || exit $? goss-0.4.9/cmd/000077500000000000000000000000001467505051300132505ustar00rootroot00000000000000goss-0.4.9/cmd/goss/000077500000000000000000000000001467505051300142235ustar00rootroot00000000000000goss-0.4.9/cmd/goss/goss.go000066400000000000000000000306361467505051300155350ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "runtime" "strings" "time" "github.com/goss-org/goss" "github.com/goss-org/goss/outputs" "github.com/goss-org/goss/resource" "github.com/goss-org/goss/system" "github.com/goss-org/goss/util" "github.com/fatih/color" "github.com/urfave/cli" ) // converts a cli context into a goss Config func newRuntimeConfigFromCLI(c *cli.Context) *util.Config { cfg := &util.Config{ AllowInsecure: c.Bool("insecure"), AnnounceToCLI: true, Cache: c.Duration("cache"), Debug: c.Bool("debug"), LogLevel: c.GlobalString("log-level"), Endpoint: c.String("endpoint"), FormatOptions: c.StringSlice("format-options"), IgnoreList: c.GlobalStringSlice("exclude-attr"), ListenAddress: c.String("listen-addr"), MaxConcurrent: c.Int("max-concurrent"), NoFollowRedirects: c.Bool("no-follow-redirects"), OutputFormat: c.String("format"), PackageManager: c.GlobalString("package"), Password: c.String("password"), Proxy: c.String("proxy"), RetryTimeout: c.Duration("retry-timeout"), Server: c.String("server"), Sleep: c.Duration("sleep"), Spec: c.GlobalString("gossfile"), Timeout: c.Duration("timeout"), Username: c.String("username"), Vars: c.GlobalString("vars"), VarsInline: c.GlobalString("vars-inline"), } if c.Bool("no-color") { util.WithNoColor()(cfg) } if c.Bool("color") { util.WithColor()(cfg) } return cfg } func timeoutFlag(value time.Duration) cli.DurationFlag { return cli.DurationFlag{ Name: "timeout", Value: value, } } func main() { app := cli.NewApp() app.EnableBashCompletion = true app.Version = util.Version app.Name = "goss" app.Usage = "Quick and Easy server validation" app.Flags = []cli.Flag{ cli.StringFlag{ Name: "log-level, loglevel, L, l", Value: "INFO", Usage: "Goss log verbosity level", EnvVar: "GOSS_LOGLEVEL", }, cli.StringFlag{ Name: "gossfile, g", Value: "./goss.yaml", Usage: "Goss file to read from / write to", EnvVar: "GOSS_FILE", }, cli.StringFlag{ Name: "vars", Usage: "json/yaml file containing variables for template", EnvVar: "GOSS_VARS", }, cli.StringFlag{ Name: "vars-inline", Usage: "json/yaml string containing variables for template (overwrites vars)", EnvVar: "GOSS_VARS_INLINE", }, cli.StringFlag{ Name: "package", Usage: fmt.Sprintf("Package type to use [%s]", strings.Join(system.SupportedPackageManagers(), ", ")), }, } app.Commands = []cli.Command{ { Name: "validate", Aliases: []string{"v"}, Usage: "Validate system", Flags: []cli.Flag{ cli.StringFlag{ Name: "format, f", Value: "rspecish", Usage: fmt.Sprintf("Format to output in, valid options: %s", outputs.Outputers()), EnvVar: "GOSS_FMT", }, cli.StringSliceFlag{ Name: "format-options, o", Usage: fmt.Sprintf("Extra options passed to the formatter, valid options: %s", outputs.FormatOptions()), EnvVar: "GOSS_FMT_OPTIONS", }, cli.BoolFlag{ Name: "color", Usage: "Force color on", EnvVar: "GOSS_COLOR", }, cli.BoolFlag{ Name: "no-color", Usage: "Force color off", EnvVar: "GOSS_NOCOLOR", }, cli.DurationFlag{ Name: "sleep,s", Usage: "Time to sleep between retries, only active when -r is set", Value: 1 * time.Second, EnvVar: "GOSS_SLEEP", }, cli.DurationFlag{ Name: "retry-timeout,r", Usage: "Retry on failure so long as elapsed + sleep time is less than this", Value: 0, EnvVar: "GOSS_RETRY_TIMEOUT", }, cli.IntFlag{ Name: "max-concurrent", Usage: "Max number of tests to run concurrently", Value: 50, EnvVar: "GOSS_MAX_CONCURRENT", }, }, Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) code, err := goss.Validate(newRuntimeConfigFromCLI(c)) if err != nil { color.Red(fmt.Sprintf("Error: %v\n", err)) } os.Exit(code) return nil }, }, { Name: "serve", Aliases: []string{"s"}, Usage: "Serve a health endpoint", Flags: []cli.Flag{ cli.StringFlag{ Name: "format, f", Value: "rspecish", Usage: fmt.Sprintf("Format to output in, valid options: %s", outputs.Outputers()), EnvVar: "GOSS_FMT", }, cli.StringSliceFlag{ Name: "format-options, o", Usage: fmt.Sprintf("Extra options passed to the formatter, valid options: %s", outputs.FormatOptions()), EnvVar: "GOSS_FMT_OPTIONS", }, cli.DurationFlag{ Name: "cache, c", Usage: "Time to cache the results", Value: 5 * time.Second, EnvVar: "GOSS_CACHE", }, cli.StringFlag{ Name: "listen-addr, l", Value: ":8080", Usage: "Address to listen on [ip]:port", EnvVar: "GOSS_LISTEN", }, cli.StringFlag{ Name: "endpoint, e", Value: "/healthz", Usage: "Endpoint to expose", EnvVar: "GOSS_ENDPOINT", }, cli.IntFlag{ Name: "max-concurrent", Usage: "Max number of tests to run concurrently", Value: 50, EnvVar: "GOSS_MAX_CONCURRENT", }, }, Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.Serve(newRuntimeConfigFromCLI(c)) }, }, { Name: "render", Aliases: []string{"r"}, Usage: "render gossfile after imports", Flags: []cli.Flag{ cli.BoolFlag{ Name: "debug, d", Usage: "Print debugging info when rendering", }, }, Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) j, err := goss.RenderJSON(newRuntimeConfigFromCLI(c)) if err != nil { return err } fmt.Print(j) return nil }, }, { Name: "autoadd", Aliases: []string{"aa"}, Usage: "automatically add all matching resource to the test suite", Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AutoAddResources(c.GlobalString("gossfile"), c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: "add", Aliases: []string{"a"}, Usage: "add a resource to the test suite", Flags: []cli.Flag{ cli.StringSliceFlag{ Name: "exclude-attr", Usage: "Exclude the following attributes when adding a new resource", }, }, Subcommands: []cli.Command{ { Name: resource.PackageResourceKey, Usage: "add new package", Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.PackageResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.FileResourceKey, Usage: "add new file", Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.FileResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.AddrResourceKey, Usage: "add new remote address:port - ex: google.com:80", Flags: []cli.Flag{ timeoutFlag(500 * time.Millisecond), }, Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.AddResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.PortResourceKey, Usage: "add new listening [protocol]:port - ex: 80 or udp:123", Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.PortResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.ServiceResourceKey, Usage: "add new service", Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.ServiceResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.UserResourceKey, Usage: "add new user", Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.UserResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.GroupResourceKey, Usage: "add new group", Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.GroupResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.CommandResourceKey, Usage: "add new command", Flags: []cli.Flag{ timeoutFlag(10 * time.Second), }, Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.CommandResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.DNSResourceKey, Usage: "add new dns", Flags: []cli.Flag{ timeoutFlag(500 * time.Millisecond), cli.StringFlag{ Name: "server", Usage: "The IP address of a DNS server to query", }, }, Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.DNSResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.ProcessResourceKey, Usage: "add new process name", Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.ProcessResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.HTTPResourceKey, Usage: "add new http", Flags: []cli.Flag{ cli.BoolFlag{ Name: "insecure, k", }, cli.BoolFlag{ Name: "no-follow-redirects, r", }, timeoutFlag(5 * time.Second), cli.StringFlag{ Name: "username, u", Usage: "Username for basic auth", }, cli.StringFlag{ Name: "password, p", Usage: "Password for basic auth", }, cli.StringFlag{ Name: "proxy, x", Usage: "Proxy server to use. e.g. http://10.0.0.2:8080", }, }, Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.HTTPResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: "goss", Usage: "add new goss file, it will be imported from this one", Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.GossFileResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.KernelParamResourceKey, Usage: "add new goss kernel param", Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.KernelParamResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.MountResourceKey, Usage: "add new mount", Flags: []cli.Flag{ timeoutFlag(1000 * time.Millisecond), }, Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.MountResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, { Name: resource.InterfaceResourceKey, Usage: "add new interface", Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) return goss.AddResources(c.GlobalString("gossfile"), resource.InterfaceResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, }, }, } addAlphaFlagIfNeeded(app) err := app.Run(os.Args) if err != nil { log.Fatal(err) } } func addAlphaFlagIfNeeded(app *cli.App) { if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { app.Flags = append(app.Flags, cli.StringFlag{ Name: "use-alpha", Usage: "goss on macOS/Windows is alpha-quality. Set to 1 to use anyway.", EnvVar: "GOSS_USE_ALPHA", Value: "0", }) } } func fatalAlphaIfNeeded(c *cli.Context) { if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { if c.GlobalString("use-alpha") != "1" { howto := map[string]string{ "darwin": "export GOSS_USE_ALPHA=1", "windows": "In cmd: set GOSS_USE_ALPHA=1\nIn powershell: $env:GOSS_USE_ALPHA=1\nIn bash: export GOSS_USE_ALPHA=1", } log.Printf(`Terminating. To bypass this and use the binary anyway: %s`, howto[runtime.GOOS]) os.Exit(1) } } } goss-0.4.9/development/000077500000000000000000000000001467505051300150275ustar00rootroot00000000000000goss-0.4.9/development/README.md000066400000000000000000000000771467505051300163120ustar00rootroot00000000000000# Random development scripts Nothing to see here, carry on :) goss-0.4.9/development/build_images.sh000077500000000000000000000022601467505051300200120ustar00rootroot00000000000000#!/usr/bin/env bash set -xeu SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" INTEGRATION_TEST_DIR="$SCRIPT_DIR/../integration-tests/" CONTAINER_REPOSITORY="aelsabbahy" LABEL_DATE=$(date -u +'%Y-%m-%dT%H:%M:%S.%3NZ') LABEL_URL="https://github.com/goss-org/goss" LABEL_REVISION=$(git rev-parse HEAD) for docker_file in $INTEGRATION_TEST_DIR/Dockerfile_*; do [[ $docker_file == *.md5 ]] && continue os=$(cut -d '_' -f2 <<<"$docker_file") md5=$(md5sum "$docker_file" | awk '{ print $1 }') docker build \ --label "org.opencontainers.image.created=$LABEL_DATE" \ --label "org.opencontainers.image.description=Quick and Easy server testing/validation" \ --label "org.opencontainers.image.licenses=Apache-2.0" \ --label "org.opencontainers.image.revision=$LABEL_REVISION" \ --label "org.opencontainers.image.source=$LABEL_URL" \ --label "org.opencontainers.image.title=goss" \ --label "org.opencontainers.image.url=$LABEL_URL" \ --label "org.opencontainers.image.version=manual" \ --label "rocks.goss.dockerfile-md5"=$md5 \ -t "$CONTAINER_REPOSITORY/goss_${os}:latest" - < "$docker_file" done goss-0.4.9/development/debian/000077500000000000000000000000001467505051300162515ustar00rootroot00000000000000goss-0.4.9/development/debian/.gitignore000066400000000000000000000000121467505051300202320ustar00rootroot00000000000000.vagrant/ goss-0.4.9/development/debian/Vagrantfile000066400000000000000000000057231467505051300204450ustar00rootroot00000000000000# -*- mode: ruby -*- # vi: set ft=ruby : # All Vagrant configuration is done below. The "2" in Vagrant.configure # configures the configuration version (we support older styles for # backwards compatibility). Please don't change it unless you know what # you're doing. Vagrant.configure(2) do |config| # The most common configuration options are documented and commented below. # For a complete reference, please see the online documentation at # https://docs.vagrantup.com. # Every Vagrant development environment requires a box. You can search for # boxes at https://atlas.hashicorp.com/search. config.vm.box = "debian/jessie64" # Disable automatic box update checking. If you disable this, then # boxes will only be checked for updates when the user runs # `vagrant box outdated`. This is not recommended. # config.vm.box_check_update = false # Create a forwarded port mapping which allows access to a specific port # within the machine from a port on the host machine. In the example below, # accessing "localhost:8080" will access port 80 on the guest machine. # config.vm.network "forwarded_port", guest: 80, host: 8080 # Create a private network, which allows host-only access to the machine # using a specific IP. # config.vm.network "private_network", ip: "192.168.33.10" # Create a public network, which generally matched to bridged network. # Bridged networks make the machine appear as another physical device on # your network. # config.vm.network "public_network" # Share an additional folder to the guest VM. The first argument is # the path on the host to the actual folder. The second argument is # the path on the guest to mount the folder. And the optional third # argument is a set of non-required options. # config.vm.synced_folder "../data", "/vagrant_data" # Provider-specific configuration so you can fine-tune various # backing providers for Vagrant. These expose provider-specific options. # Example for VirtualBox: # # config.vm.provider "virtualbox" do |vb| # # Display the VirtualBox GUI when booting the machine # vb.gui = true # # # Customize the amount of memory on the VM: # vb.memory = "1024" # end # # View the documentation for the provider you are using for more # information on available options. # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies # such as FTP and Heroku are also available. See the documentation at # https://docs.vagrantup.com/v2/push/atlas.html for more information. # config.push.define "atlas" do |push| # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" # end # Enable provisioning with a shell script. Additional provisioners such as # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the # documentation for more information about their specific syntax and use. # config.vm.provision "shell", inline: <<-SHELL # sudo apt-get update # sudo apt-get install -y apache2 # SHELL end goss-0.4.9/development/push_images.sh000077500000000000000000000007601467505051300176750ustar00rootroot00000000000000#!/usr/bin/env bash set -xeu SCRIPT_DIR=$(readlink -f "$(dirname "$0")") CONTAINER_REPOSITORY="aelsabbahy" images=$(docker images | grep "^$CONTAINER_REPOSITORY/goss_.*latest" | awk '$0=$1') # Use md5sum to determine if CI needs to do a docker build pushd "$SCRIPT_DIR/../integration-tests"; for dockerfile in Dockerfile_*;do [[ $dockerfile == *.md5 ]] && continue md5sum "$dockerfile" > "${dockerfile}.md5" done popd for image in $images; do docker push "${image}:latest" done goss-0.4.9/docs/000077500000000000000000000000001467505051300134355ustar00rootroot00000000000000goss-0.4.9/docs/.pages000066400000000000000000000004051467505051300145340ustar00rootroot00000000000000nav: - Home: index.md - installation.md - quickstart.md - container_image.md - Command Reference: cli.md - The gossfile: gossfile.md - migrations.md - platforms.md - containers - Contributing: contributing.md - changelog.md - license.md goss-0.4.9/docs/changelog.md000066400000000000000000000002241467505051300157040ustar00rootroot00000000000000# Changelog `Goss` does not (yet?) maintain a changelog file. However, you can consult [Goss releases](https://github.com/goss-org/goss/releases). goss-0.4.9/docs/cli.md000066400000000000000000000320431467505051300145300ustar00rootroot00000000000000# Command Line Interface ## Usage ```console NAME: goss - Quick and Easy server validation USAGE: goss [global options] command [command options] [arguments...] VERSION: 0.0.0 COMMANDS: validate, v Validate system serve, s Serve a health endpoint render, r render gossfile after imports autoadd, aa automatically add all matching resource to the test suite add, a add a resource to the test suite help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --gossfile value, -g value Goss file to read from / write to (default: "./goss.yaml") [$GOSS_FILE] --vars value json/yaml file containing variables for template [$GOSS_VARS] --vars-inline value json/yaml string containing variables for template (overwrites vars) [$GOSS_VARS_INLINE] --package value Package type to use [rpm, deb, apk, pacman] --help, -h show help --version, -v print the version ``` !!! note Most flags can be set by using environment variables, see `--help` for more info. ## Global options `--gossfile/-g ` : The file to use when reading/writing tests. Use `--gossfile -` or `-g -` to read from `STDIN`. Valid formats: * `yaml` *(default)* * `json` `--vars ` : The file to read variables from when rendering gossfile [templates](gossfile.md#templates). Valid formats: * `yaml` *(default)* * `json` `--package ` : The package type to check for. Valid options are: * `apk` * `deb` * `pacman` * `rpm` ## Commands Commands are the actions goss can run. * [add](#add): add a single test for a resource * [autoadd](#autoadd): automatically add multiple tests for a resource * [render](#render): renders and outputs the gossfile, importing all included gossfiles * [serve](#serve): serves the gossfile validation as an HTTP endpoint on a specified address and port, so you can use your gossfile as a health report for the host * [validate](#validate): runs the goss test suite on your server ### `add` !!! abstract "Add system resource to test suite" ```console goss add [--exclude-attr ] [] goss a [--exclude-attr ] [] ``` This will add a test for a resource. Non existent resources will add a test to ensure they do not exist on the system. A sub-command *resource type* has to be provided when running `add`. `--exclude-attr` : Ignore **non-required** attribute(s) matching the provided glob when adding a new resource, may be specified multiple times. !!! example ```console goss add file /etc/passwd goss a user nobody goss add --exclude-attr home --exclude-attr shell user nobody goss a --exclude-attr '*' user nobody ``` #### Resources types | Type | Description | |--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| | [`addr`](gossfile.md#addr) | Verify if a remote `address:port` is reachable | | [`command`](gossfile.md#command) | Run a [command](gossfile.md#command) and validate the exit status and/or output | | [`dns`](gossfile.md#dns) | Resolves a [dns](gossfile.md#dns) name and validates the addresses | | [`file`](gossfile.md#file) | Validate a [file](gossfile.md#file) existence, permissions, stats (size, etc) and contents | | [`goss`](gossfile.md#gossfile) | Includes the contents of another [gossfile](gossfile.md) | | [`group`](gossfile.md#group) | can validate the existence and values of a [group](gossfile.md#group) on the system | | [`http`](gossfile.md#http) | Validate the HTTP response code, headers, and content of a URI | | [`interface`](gossfile.md#interface) | Validate the existence and values (es. the addresses) of a network interface | | [`kernel-param`](gossfile.md#kernel-param) | Validate kernel parameters (sysctl values) | | [`mount`](gossfile.md#mount) | Validate the existence and options relative to a [mount](gossfile.md#mount) point | | [`package`](gossfile.md#package) | Validate the status of a [package](gossfile.md#package) using the package manager specified on the commandline with `--package` | | [`port`](gossfile.md#port) | Validate the status of a local [port](gossfile.md#port), for example `80` or `udp:123` | | [`process`](gossfile.md#process) | Validate the status of a [process](gossfile.md#process) | | [`service`](gossfile.md#service) | Validate if a [service](gossfile.md#service) is running and/or enabled at boot | | [`user`](gossfile.md#user) | Validate the existence and values of a [user](gossfile.md#user) on the system | ### `autoadd` !!! abstract "Auto add all matching resources to test suite" ```console goss autoadd [arguments...] goss aa [arguments...] ``` Automatically [adds](#add) all **existing** resources matching the provided argument. Will automatically add the following matching resources: * `file` - only if argument contains `/` * `group` * `package` * `port` * `process` - Also adding any ports it's listening to (if run as root) * `service` * `user` Will **NOT** automatically add: * `addr` * `command` - for safety * `dns` * `http` * `interface` * `kernel-param` * `mount` !!! example ```console goss autoadd sshd ``` Generates the following `goss.yaml` ```yaml port: tcp:22: listening: true ip: - 0.0.0.0 tcp6:22: listening: true ip: - '::' service: sshd: enabled: true running: true user: sshd: exists: true uid: 74 gid: 74 groups: - sshd home: /var/empty/sshd shell: /sbin/nologin group: sshd: exists: true gid: 74 process: sshd: running: true ``` ### `render` !!! abstract "Render gossfile after importing all referenced gossfiles" ``` goss render goss r ``` This command allows you to keep your tests separated and render a single, valid, gossfile, by including them with the `gossfile` directive. `--debug` : This prints the rendered golang template prior to printing the parsed JSON/YAML gossfile. !!! example ```console $ cat goss_httpd_package.yaml package: httpd: installed: true versions: - 2.2.15 $ cat goss_httpd_service.yaml service: httpd: enabled: true running: true $ cat goss_nginx_service-NO.yaml service: nginx: enabled: false running: false $ cat goss.yaml gossfile: goss_httpd_package.yaml: {} goss_httpd_service.yaml: {} goss_nginx_service-NO.yaml: {} $ goss -g goss.yaml render package: httpd: installed: true versions: - 2.2.15 service: httpd: enabled: true running: true nginx: enabled: false running: false ``` ### `serve` !!! abstract "Serve a health endpoint" ```console goss serve [...] goss s [...] ``` `serve` exposes the goss test suite as a health endpoint on your server. The end-point will return the stest results in the format requested and an http status of 200 or 503. `serve` will look for a test suite in the same order as [validate](#validate) `--cache `, `-c ` : Time to cache the results (default: 5s) `--endpoint `, `-e ` : Endpoint to expose (default: `/healthz`) `--format `, `-f ` : Output format, same as [validate](#validate) `--listen-addr [ip]:port`, `-l [ip]:port` : Address to listen on (default: `:8080`) `--loglevel `, `-L ` : Goss logging verbosity level (default: `INFO`). Lower levels of tracing include all upper levels traces also (ie. `INFO` include `WARN` and `ERROR`). `level` can be one of: - `ERROR` - Critical errors that halt goss or significantly affect its functionality, requiring immediate intervention. - `WARN` - Non-critical issues that may require attention, such as overwritten keys or deprecated features. - `INFO` - General operational messages, useful for tasks where a more structured output is needed (e.g. goss serve). - `DEBUG` - Information useful for the goss user to debug. - `TRACE` - Detailed internal system activities useful for goss developers to debug. `--max-concurrent ` : Max number of tests to run concurrently !!! example ```console $ goss serve & $ curl http://localhost:8080/healthz # JSON endpoint $ goss serve --format json & $ curl localhost:8080/healthz # rspecish output format in response via content negotiation goss serve --format json & curl -H "Accept: application/vnd.goss-rspecish" localhost:8080/healthz ``` The `application/vnd.goss-{output format}` media type can be used in the `Accept` request header to determine the response's content-type. You can also `Accept: application/json` to get back `application/json`. ### `validate` !!! abstract "Validate the system" ```console goss validate [...] goss v [...] ``` `validate` runs the goss test suite on your server. Prints an rspec-like (by default) output of test results. Exits with status 0 on success, non-0 otherwise. `--format `, `-f ` : Output format. Can be one of: - `documentation` - Verbose test results - `json` - Detailed test result on a single line (See `pretty` format option) - `junit` - `nagios` - Nagios/Sensu compatible output /w exit code 2 for failures - `rspecish` **(default)** - Similar to rspec output - `tap` - `prometheus` - Prometheus compatible output. - `silent` - No output. Avoids exposing system information (e.g. when serving tests as a healthcheck endpoint) `--format-options`, `-o` : Output format option: - `perfdata` - Outputs Nagios "performance data". Applies to `nagios` output - `verbose` - Gives verbose output. Applies to `nagios` and `prometheus` output - `pretty` - Pretty printing for the `json` output - `sort` - Sorts the results `--loglevel `, `-L ` : Goss logging verbosity level (default: `INFO`). Lower levels of tracing include all upper levels traces also (ie. `INFO` includes `WARN`, `ERROR` and `FATAL` outputs). `level` can be one of : - `TRACE` - Print details for each check, successful or not and all incoming healthchecks - `DEBUG` - Print details of summary response to healthchecks including remote IP address, return code and full body - `INFO` - Print summary when all checks run OK - `WARN` - Print summary and corresponding checks when encountering some failures - `ERROR` - Not used for now (will not print anything) - `FATAL` - Not used for now (will not print anything) `--max-concurrent ` : Max number of tests to run concurrently `--color`/`--no-color` : Force color or disable color `--retry-timeout `, `-r ` : Retry on failure so long as elapsed + sleep time is less than this *default: `0`* `--sleep `, `-s ` : Time to sleep between retries *default: `1s`* !!! example ```console $ goss validate --format documentation File: /etc/hosts: exists: matches expectation: [true] DNS: localhost: resolvable: matches expectation: [true] [...] Total Duration: 0.002s Count: 10, Failed: 2, Skipped: 0 $ curl -s https://static/or/dynamic/goss.json | goss validate ...F.F [...] Total Duration: 0.002s Count: 6, Failed: 2, Skipped: 0 $ goss render | ssh remote-host 'goss -g - validate' ...... Total Duration: 0.002s Count: 6, Failed: 0, Skipped: 0 $ goss validate --format nagios -o verbose -o perfdata GOSS CRITICAL - Count: 76, Failed: 1, Skipped: 0, Duration: 1.009s|total=76 failed=1 skipped=0 duration=1.009s Fail 1 - DNS: localhost: addrs: doesn't match, expect: [["127.0.0.1","::1"]] found: [["127.0.0.1"]] $ echo $? 2 ``` goss-0.4.9/docs/container_image.md000066400000000000000000000017611467505051300171100ustar00rootroot00000000000000# Goss container image ## Dockerfiles * [latest](https://github.com/goss-org/goss/blob/master/Dockerfile) ## Using the base image This is a simple alpine image with Goss preinstalled on it. Can be used as a base image for your projects to allow for easy health checking. ### Mount example Create the container ```sh docker run --name goss ghcr.io/goss-org/goss goss ``` Create your container and mount goss ```sh docker run --rm -it --volumes-from goss --name weby nginx ``` Run goss inside your container ```sh docker exec weby /goss/goss autoadd nginx ``` ### HEALTHCHECK example ```dockerfile FROM ghcr.io/goss-org/goss:latest COPY goss/ /goss/ HEALTHCHECK --interval=1s --timeout=6s CMD goss -g /goss/goss.yaml validate # your stuff.. ``` ### Startup delay example ```dockerfile FROM ghcr.io/goss-org/goss:latest COPY goss/ /goss/ # Alternatively, the -r option can be set # using the GOSS_RETRY_TIMEOUT env variable CMD goss -g /goss/goss.yaml validate -r 5m && exec real_comand.. ``` goss-0.4.9/docs/containers/000077500000000000000000000000001467505051300156025ustar00rootroot00000000000000goss-0.4.9/docs/containers/docker-compose.md000066400000000000000000000001111467505051300210270ustar00rootroot00000000000000 --8<-- "extras/dcgoss/README.md" goss-0.4.9/docs/containers/docker.md000066400000000000000000000001101467505051300173630ustar00rootroot00000000000000 --8<-- "extras/dgoss/README.md" goss-0.4.9/docs/containers/kubernetes.md000066400000000000000000000001101467505051300202630ustar00rootroot00000000000000 --8<-- "extras/kgoss/README.md" goss-0.4.9/docs/contributing.md000066400000000000000000000001111467505051300164570ustar00rootroot00000000000000 --8<-- ".github/CONTRIBUTING.md" goss-0.4.9/docs/goss.yaml000066400000000000000000000105041467505051300152740ustar00rootroot00000000000000# This sample goss.yaml file is used to test the json schema goss-json-schema.yaml addr: tcp: reachable: true timeout: 500 local-address: 127.0.0.1 command: version: # required attributes exit-status: 0 # defaults to hash key exec: "go version" # optional attributes stdout: - go version go1.6 linux/amd64 stderr: [] timeout: 10000 # in milliseconds skip: false dns: localhost: addrs: - ::1 resolvable: true file: /ect/password/static: exists: true mode: "0644" size: 2118 owner: root group: root filetype: file contains: - hrll md5: 7c9bb14b3bf178e82c00c2a4398c93cd sha256: 7f78ce27859049f725936f7b52c6e25d774012947d915e7b394402cfceb70c4c sha512: cb71b1940dc879a3688bd502846bff6316dd537bbe917484964fe0f098e9245d80958258dc3bd6297bf42d5bd978cbe2c03d077d4ed45b2b1ed9cd831ceb1bd0 linked-to: /usr/sbin/sendmail.sendmail skip: false # Inline templates are not valid json/yaml that could be validated as-in by json schema # Instead, run `goss --vars ./vars.yaml render > rendered_goss.yaml` to render them # and apply the schema validation on the rendered file. {{- range mkSlice "/etc/passwd" "/etc/group"}} {{.}}: exists: true mode: "0644" owner: root group: root filetype: file {{end}} gossfile: myapplication: file: myapp_gossfile.yaml skip: false group: nfsnobody: exists: true skip: false gid: 65534 nobody: exists: true http: https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 1000 username: "" password: "" ca-file: "" cert-file: "" key-file: "" proxy: "" skip: false method: GET interface: eth0: exists: true addrs: - " 1" mtu: 1500 kernel-param: kernel.ostype: value: Linux matching: check_instance_count: content: {{ .Vars.instance_count }} matches: gt: 0 check_failure_count_from_all_instance: content: {{ .Vars.failures }} matches: 0 check_status: content: {{ .Vars.status }} matches: - not: FAIL example: content: - 1.0.1 - 1.9.9 matches: semver-constraint: ">1.0.0 <2.0.0 !=1.5.0" has_substr: # friendly test name content: some string matches: match-regexp: some str has_2: content: - 2 matches: contain-element: 2 has_foo_bar_and_baz: content: foo: bar baz: bing matches: and: - have-key-with-value: foo: bar - have-key: baz # TODO: sprig syntax fails to accept upper block below # https://github.com/goss-org/goss/blob/master/docs/manual.md#examples-2 # sping_basic: # content: { { "hello!" | upper | repeat 5 }} # matches: # match-regexp: "HELLO!HELLO!HELLO!HELLO!HELLO!" mount: /home: exists: true timeout: 1000 opts: - rw source: /dev/mapper/fedora-home filesystem: xfs usage: lt: 95 package: httpd: installed: true versions: - "2.1" skip: false # https://github.com/goss-org/goss/blob/master/README.md#manually-editing-goss-files kernel: installed: true versions: and: - have-len: 3 - not: contain-element: "4.1.0" port: tcp:22: listening: true ip: - "1" skip: false process: chrome: # required attributes running: true skip: false service: sshd: enabled: true skip: false running: true user: nfsbody: exists: true uid: 65534 gid: 65534 groups: - nfsnobody home: /var/lib/nfs shell: /sbin/nologin skip: false nobody: exists: true uid: lt: 500 groups: consist-of: [nobody] sshd: title: UID must be between 50-100, GID doesn't matter. home is flexible meta: desc: Ensure sshd is enabled and running since it's needed for system management sev: 5 exists: true uid: # Validate that UID is between 50 and 100 and: gt: 50 lt: 100 home: # Home can be any of the following or: - /var/empty/sshd - /var/run/sshd # https://github.com/goss-org/goss/blob/master/README.md#manually-editing-goss-files goss-0.4.9/docs/gossfile.md000066400000000000000000000631741467505051300156050ustar00rootroot00000000000000# The `gossfile` configuration ## Goss test creation Goss tests can be created by using either of following methods. 1. `goss autoadd ` 2. `goss add ` 3. manually create YAML/JSON test file by hand. To customize the parameters generated by `goss add` and `goss autoadd` YAML file you need to manually edit it. `goss add package nginx` will generate below YAML ```yaml package: nginx: installed: true versions: - 1.17.8 ``` To test uninstall scenario you would need to manually edit it and set it as below. ```yaml package: nginx: installed: false ``` ## Important note about goss file format It is important to note that both YAML and JSON are formats that describe a nested data structure. ```yaml title="WRONG way to write a goss file" file: /etc/httpd/conf/httpd.conf: exists: true service: httpd: enabled: true running: true file: /var/www/html: filetype: directory exists: true ``` If you try to validate this file, it will **only** run the second `file` test: ```console $ goss validate --format documentation File: /var/www/html: exists: matches expectation: [true] File: /var/www/html: filetype: matches expectation: ["directory"] Service: httpd: enabled: matches expectation: [true] Service: httpd: running: matches expectation: [true] Total Duration: 0.014s Count: 8, Failed: 0, Skipped: 0 ``` As you can see, the first `file` check has not been run because the second `file` entry *overwrites* the previous one. You need to make sure all the entries of the same type are under the same declaration. ```yaml title="This is the CORRECT way to write a goss file" file: /etc/httpd/conf/httpd.conf: exists: true /var/www/html: filetype: directory exists: true service: httpd: enabled: true running: true ``` Running validate with this configuration will correctly check both files: ```console $ goss validate --format documentation File: /var/www/html: exists: matches expectation: [true] File: /var/www/html: filetype: matches expectation: ["directory"] File: /etc/httpd/conf/httpd.conf: exists: matches expectation: [true] Service: httpd: enabled: matches expectation: [true] Service: httpd: running: matches expectation: [true] Total Duration: 0.014s Count: 10, Failed: 0, Skipped: 0 ``` Please note that using the `goss add` and `goss autoadd` command will create a valid file, but if you're writing your files by hand you'll save a lot of time by taking this in consideration. If you want to keep your tests in separate files, the best way to obtain a single, valid, file is to create a main goss file that includes the others with the [gossfile](#gossfile) directive and then [render](cli.md#render) it. ### Schema A [Json draft 7 schema](https://github.com/json-schema-org/json-schema-spec/blob/draft-07/schema.json) available at makes it easier to edit simple goss.yaml files in IDEs, providing usual coding assistance such as inline documentation, completion and static analysis. See #793 for screenshots. For example, to configure the Json schema in JetBrains intellij IDEA, follow [documented instructions](https://www.jetbrains.com/help/idea/json.html#ws_json_schema_add_custom), with arguments such as: * `schema url=https://goss.rocks/schemas/gossfile.yaml` * `schema version=Json schema version 7` * `file path pattern=*/goss.yaml` ## Available tests * [addr](#addr) * [command](#command) * [dns](#dns) * [file](#file) * [gossfile](#gossfile) * [group](#group) * [http](#http) * [interface](#interface) * [kernel-param](#kernel-param) * [matching](#matching) * [mount](#mount) * [package](#package) * [port](#port) * [process](#process) * [service](#service) * [user](#user) ### addr Validates if a remote `address:port` are accessible. ```yaml addr: tcp://ip-address-or-domain-name:80: # required attributes reachable: true # optional attributes # defaults to hash key address: "tcp://ip-address-or-domain-name:80" timeout: 500 local-address: 127.0.0.1 ``` ### command Validates the exit-status and output of a command. This can be used in combination with the [gjson](#gjson) matcher to create powerful goss custom tests. ```yaml command: 'go version': # required attributes exit-status: 0 # optional attributes # defaults to hash key exec: "go version" stdout: - go version go1.6 linux/amd64 stderr: [] timeout: 10000 # in milliseconds skip: false ``` `stdout` and `stderr` can be a string or [pattern](#patterns) The `exec` attribute is the command to run; this defaults to the name of the hash for backwards compatibility ### dns Validates that the provided address is resolvable and the addrs it resolves to. ```yaml dns: localhost: # required attributes resolvable: true # optional attributes # defaults to hash key resolve: localhost addrs: - 127.0.0.1 - ::1 server: 8.8.8.8 # Also supports server:port timeout: 500 # in milliseconds (Only used when server attribute is provided) ``` It is possible to validate the following types of DNS records, but requires the ```server``` attribute be set: * `A` * `AAAA` * `CAA` * `CNAME` * `MX` * `NS` * `PTR` * `SRV` * `TXT` To validate specific DNS address types, prepend the hostname with the type and a colon, a few examples: ```yaml dns: # Validate a CNAME record CNAME:c.dnstest.io: resolvable: true server: 208.67.222.222 addrs: - "a.dnstest.io." # Validate a PTR record PTR:8.8.8.8: resolvable: true server: 8.8.8.8 addrs: - "dns.google." # Validate and SRV record SRV:_https._tcp.dnstest.io: resolvable: true server: 208.67.222.222 addrs: - "0 5 443 a.dnstest.io." - "10 10 443 b.dnstest.io." ``` Please note that if you want `localhost` to **only** resolve `127.0.0.1` you'll need to use [Advanced Matchers](#advanced-matchers) ```yaml dns: localhost: resolvable: true addrs: consist-of: [127.0.0.1] timeout: 500 # in milliseconds ``` ### file Validates the state of a file, directory, socket, or symbolic link ```yaml file: /etc/passwd: # required attributes exists: true # optional attributes # defaults to hash key path: /etc/passwd mode: "0644" size: 2118 # in bytes owner: root group: root filetype: file # file, symlink, directory, socket contents: [] # Check file content for these patterns md5: 7c9bb14b3bf178e82c00c2a4398c93cd # md5 checksum of file # A stronger checksum alternatives to md5 (recommended) sha256: 7f78ce27859049f725936f7b52c6e25d774012947d915e7b394402cfceb70c4c sha512: cb71b1940dc879a3688bd502846bff6316dd537bbe917484964fe0f098e9245d80958258dc3bd6297bf42d5bd978cbe2c03d077d4ed45b2b1ed9cd831ceb1bd0 /etc/alternatives/mta: # required attributes exists: true # optional attributes filetype: symlink # file, symlink, directory, socket linked-to: /usr/sbin/sendmail.sendmail skip: false ``` `contents` can be a string or a [pattern](#patterns) ### gossfile Import other gossfiles from this one. This is the best way to maintain a large number of tests, and/or create profiles. See [render](cli.md#render) for more examples. Glob patterns can be also be used to specify matching gossfiles. ```yaml gossfile: myapplication: file: myapp_gossfile.yaml skip: false *.yaml: skip: true goss_httpd.yaml: {} /etc/goss.d/*.yaml: {} ``` You can specify the gossfile(s) either as the resource key, or using the 'file' attribute. If the 'skip' attribute is true, then the file is not processed. If the filename is a glob pattern, then none of the matching files are processed. Note that this is not the same as skipping the contained resources; any overrides in the referenced gossfile will not be processed, and the resource count will not be incremented. Skipping a gossfile include is the same as omitting the gossfile resource entirely. ### group Validates the state of a group ```yaml group: nfsnobody: # required attributes exists: true # optional attributes # defaults to hash key groupname: /etc/passwd gid: 65534 skip: false ``` ### http Validates HTTP response status code and content. ```yaml http: https://www.google.com: # required attributes status: 200 # optional attributes # defaults to hash key url: https://www.google.com allow-insecure: false no-follow-redirects: false # Setting this to true will NOT follow redirects timeout: 1000 request-headers: # Set request header values - "Content-Type: text/html" headers: [] # Check http response headers for these patterns (e.g. "Content-Type: text/html") request-body: '{"key": "value"}' # request body body: [] # Check http response content for these patterns username: "" # username for basic auth password: "" # password for basic auth ca-file: "" # CA root certs pem file, ex: /etc/ssl/cert.pem cert-file: "" # certificate file to use for authentication (used with key-file) key-file: "" # private-key file to use for authentication (used with cert-file) proxy: "" # proxy server to proxy traffic through. Proxy can also be set with environment variables http_proxy. skip: false method: PUT # http method ``` !!! note only the first `Host` header will be used to set the `Request.Host` value if multiple are provided. ### interface Validates network interface values ```yaml interface: eth0: # required attributes exists: true # optional attributes # defaults to hash key name: eth0 addrs: - 172.17.0.2/16 - fe80::42:acff:fe11:2/64 mtu: 1500 ``` ### kernel-param Validates kernel param (sysctl) value. ```yaml kernel-param: kernel.ostype: # required attributes value: Linux # optional attributes # defaults to hash key name: kernel.ostype ``` To see the full list of current values, run `sysctl -a`. ### mount Validates mount point attributes. ```yaml mount: /home: # required attributes exists: true # optional attributes # defaults to hash key timeout: 1000 mountpoint: /home opts: - rw - relatime # This maps to the per-superblock options, see: # https://man7.org/linux/man-pages/man5/proc.5.html # https://man7.org/linux/man-pages/man2/mount.2.html vfs-opts: - rw source: /dev/mapper/fedora-home filesystem: xfs usage: #% of blocks used in this mountpoint lt: 95 ``` ### matching Validates specified content against a matcher. Best used with [Templates](#templates). #### With [Templates](#templates) Let's say we have a `data.json` file that gets generated as part of some testing pipeline: ```json { "instance_count": 14, "failures": 3, "status": "FAIL" } ``` This could then be passed into goss: `goss --vars data.json validate` And then validated against: ```yaml+jinja matching: check_instance_count: # Make sure there is at least one instance content: {{ .Vars.instance_count }} matches: gt: 0 check_failure_count_from_all_instance: # expect no failures content: {{ .Vars.failures }} matches: 0 check_status: content: {{ .Vars.status }} matches: - not: FAIL ``` #### Without [Templates](#templates) ```yaml matching: has_substr: # friendly test name content: some string matches: match-regexp: some str has_2: content: - 2 matches: contain-element: 2 has_foo_bar_and_baz: content: foo: bar baz: bing matches: and: - have-key: baz ``` ### package Validates the state of a package ```yaml package: httpd: # required attributes installed: true # optional attributes # defaults to hash key name: httpd versions: - 2.2.15 skip: false ``` !!! note this check uses the `--package ` parameter passed on the command line. ### port Validates the state of a local port. !!! note Goss might consider your port to be listening on `tcp6` rather than `tcp`, try running `goss add port ..` to see how goss detects it. ([explanation](https://github.com/goss-org/goss/issues/149)) ```yaml port: # {tcp,tcp6,udp,udp6}:port_num tcp:22: # required attributes listening: true # optional attributes # defaults to hash key port: 'tcp:22' ip: # what IP(s) is it listening on - 0.0.0.0 skip: false ``` ### process Validates if a process is running. ```yaml process: chrome: # required attributes running: true # optional attributes # defaults to hash key comm: chrome skip: false ``` !!! note This check is inspecting the name of the binary, not the name of the process. For example, a process with the name `nginx: master process /usr/sbin/nginx` would be checked with the process `nginx`. To discover the binary of a pid run `cat -E /proc//comm`. ### service Validates the state of a service. ```yaml service: sshd: # Optional attributes # defaults to hash key name: sshd enabled: true running: true runlevels: ["3", "4", "5"] # Alpine example, runlevels: ["default"] skip: false ``` `runlevels` is only supported on Alpine init, sysv init, and upstart !!! note This will **not** automatically check if the process is alive, it will check the status from `systemd`/`upstart`/`init`. ### user Validates the state of a user ```yaml user: nfsnobody: # required attributes exists: true # optional attributes # defaults to hash key username: nfsnobody uid: 65534 gid: 65534 groups: - nfsnobody home: /var/lib/nfs shell: /sbin/nologin skip: false ``` !!! note This check is inspecting the contents of local passwd file `/etc/passwd`, this does not validate remote users (e.g. LDAP). ## Matchers ### Default Matchers Default matchers are determined by the attribute value received from the system. #### Bool, Strings, Integers Bool, Strings and integers are compared using equality, for example: ```yaml matching: basic_string: content: 'foo' matches: 'foo' user: nfsnobody: exists: true uid: 65534 ``` #### Arrays Arrays are treated as a [contains-elements](#array-matchers) by default, this validates that the expected test is a subset of the returned system state. ```yaml matching: basic_array: content: - 'group1' - 'group2' - 'group3' matches: - 'group1' - 'group2' # This fails, since the returned result and it's no longer a subset basic_array_failing: content: - 'group1' - 'group2' - 'group3' matches: - 'group1' - 'group2' - 'group2' # this 2nd group2 is not in the returned content ``` #### io.Readers This is the most magical matcher for goss. It remains a default for historic and performance reasons. Some attributes return an io.Reader that is read line by line (ex. file content, command, http body). This allows goss to validate large files/content efficiently. Each pattern is checked against the attribute output, the type of patterns are: * `"foo"` - checks if any line contains `foo` * `"!foo"` - inverse of above, checks that no line contains `foo` * `"\\!foo"` - escape sequence, check if any line contains `!string` * `"/[Rr]egex/"` - verifies that line matches regex * `"!/[Rr]egex/"` - inverse of above, checks that no line matches regex !!! note Regex support is based on Golang's regex engine documented [here](https://golang.org/pkg/regexp/syntax/) !!! warning You will **need** the double backslash (`\\`) escape for Regex special entities, for example `\\s` for blank spaces. !!! example ```yaml file: /tmp/test.txt: exists: true contents: - "foo" - "!bar" - "/[Gg]oss/" ``` The above can be expressed as: ```yaml file: /tmp/test.txt: exists: true contents: and: - contain-element: "foo" - not: {contain-element: "bar"} - contain-element: {match-regexp: "[Gg]oss"} ``` ### Transforms If the system state type and the expected type don't match, goss will attempt to transform the system state type before matching it. For example, kernel-param attribute returns a string, however, it can be tested using numeric comparisons: !!! example "kernel-param test" ```yaml kernel-param: net.core.somaxconn: value: "128" ``` !!! example "(failing) kernel-param test with transform" ```yaml kernel-param: net.core.somaxconn: value: {gt: 200} ``` When a transformed test fails, it will detail the transformers used, the `-o exclude_raw` option can be used to exclude the raw, untransformed attribute value: ```coonsole $ goss v F Failures/Skipped: KernelParam: net.core.somaxconn: value: Expected 128 to be > 200 the transform chain was [{"to-numeric":{}}] the raw value was "128" Total Duration: 0.001s Count: 1, Failed: 1, Skipped: 0 $ goss v -o exclude_raw F Failures/Skipped: KernelParam: net.core.somaxconn: value: Expected 128 to be > 200 the transform chain was [{"to-numeric":{}}] Total Duration: 0.001s Count: 1, Failed: 1, Skipped: 0 ``` ### Advanced Matchers Goss supports advanced matchers by converting YAML input to [gomega](https://onsi.github.io/gomega/) matchers. #### String Matchers These will convert the system attribute to a string prior to matching. * `'55'` - Checks that the numeric is "55" when converted to string * `have-prefix: pre` - Checks if string starts with "pre" * `have-suffix: suf` - Checks if string ends with "suf" * `match-regexp: '.*'` - Checks if string matches regexp * `contain-substring: '2'` - Checks if string contains "2" * `'55'` - Checks that the numeric is "55" when converted to string * `have-prefix: pre` - Checks if string starts with "pre" * `have-suffix: suf` - Checks if string ends with "suf" * `match-regexp: '.*'` - Checks if string matches regexp * `contain-substring: '2'` - Checks if string contains "2" !!! example ```yaml matching: example: content: 42 matches: and: - '42' - have-prefix: '4' - have-suffix: '2' - match-regexp: '\d{2}' - contain-substring: '2' ``` #### Numeric matchers These will convert the system attribute to a numeric prior to matching. * `42` - If the expected type is a number * `gt, ge, lt, le` - Greater than, greater than or equal, less than, etc.. !!! example ```yaml matching: example: content: "42" matches: and: - 42 - 42.0 - gt: 40 - lt: 45 ``` #### Array matchers These will convert the system attribute to an array prior to matching. Strings are split on "\n" * `contain-element: matcher` - Checks if the array contains an element that passes the matcher * `contain-elements: [matcher, ...]` - checks if the array is a superset of the provided matchers * `[matcher, ...]` - same as above * `equal: [value, ...]` - Checks if the array is exactly equal to provided array * `consist-of: [matcher, ...]` - Checks if the array consists of the provided matchers (order does not matter) !!! example ```yaml matching: example: content: [foo, bar, moo] matches: and: - contain-elements: [foo, bar] - [foo, bar] # same as above - equal: [foo, bar, moo] # order matters, exact match - consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers - contain-element: have-prefix: b ``` #### Misc matchers These matchers don't really fall into any of the above categories, or span multiple categories. * `equal` - Useful when needing to override a default matcher * `have-len: 3` - Checks if the array/string/map has length of 3 * `have-key: "foo"` - Checks if key exists in map, useful with `gjson` * `not: matcher` - Checks that a matcher does not match * `and: [matcher, ..]` - Checks that all matchers match * `or: [matcher, ..]` - Checks that any matchers match !!! note When system returns a string it is converted into a one element array and matched See the following for examples: [link..]fixme ##### semver-constraint Checks that all versions match semver constraint or range syntax. This uses [semver](https://github.com/blang/semver) under the hood, however, wildcards (e.g. `1.X` are not officially supported and may go away in a future release). !!! example ```yaml matching: semver: content: - 1.0.1 - 1.9.9 matches: semver-constraint: ">1.0.0 <2.0.0 !=1.5.0" semver2: content: - 1.0.1 - 1.5.0 - 1.9.9 matches: not: semver-constraint: ">1.0.0 <2.0.0 !=1.5.0" semver3: content: 1.0.1 matches: semver-constraint: ">5.0.0 || < 1.5.0" ``` ##### gjson Checks extracted [gjson](https://gjson.dev/) passes the matcher Example: ```yaml matching: example: content: '{"foo": "bar", "moo" {"nested": "cow"}, "count": "15"}' matches: gjson: moo.nested: cow foo: {have-prefix: b} count: {le: 25} '@this': {have-key: "foo"} moo: and: - {have-key: "nested"} - {not: {have-key: "nested2"}} ``` ## Templates Goss test files can leverage golang's [text/template](https://golang.org/pkg/text/template/) to allow for dynamic or conditional tests. Available variables: * `{{.Env}}` - Containing environment variables * `{{.Vars}}` - Containing the values defined in [--vars](#global-options) file Available functions: * [built-in text/template functions](https://golang.org/pkg/text/template/#hdr-Functions) * [Sprig functions](https://masterminds.github.io/sprig/) * Custom functions: `mkSlice "ARG1" "ARG2"` : Returns a slice of all the arguments. See examples below for usage. `getEnv "var" ["default"]` : A more forgiving env var lookup. If key is missing either "" or default (if provided) is returned. `readFile "fileName"` : Reads file content into a string, trims whitespace. Useful when a file contains a token. !!! note Goss will error out during the parsing phase if the file does not exist, no tests will be executed. `regexMatch "(some)?reg[eE]xp"` : Tests the piped input against the regular expression argument. `toLower` : Changes piped input to lowercase `toUpper` : Changes piped input to UPPERCASE `findStringSubmatch regex string` : Returns map[string]interface{} with the names of the parenthesized subexpressions, like `(?P[a-z])` {{ $regexDBrc := "\\'mysql:\\/\\/(?P[a-z0-9]+):(?P[a-z0-9]+)@localhost\\/(?Proundcube_[a-z0-9]+)\\';"}} {{ $rcConf := readFile /home/user/roundcube/config.inc.php | findStringSubmatch $regexDBrc }} {{ $UserDBrc := get $rcConf "login" }} {{ $PassDBrc := get $rcConf "password" }} {{ $DBrc := get $rcConf "database" }} If not exists named parenthesized subexps, returns stringfied array string: {{ $regexDBrc := "\\'mysql:\\/\\/([a-z0-9]+):([a-z0-9]+)@localhost\\/(roundcube_[a-z0-9]+)\\';"}} {{ $rcConf := readFile /home/user/roundcube/config.inc.php | findStringSubmatch $regexDBrc }} {{ $UserDBrc := get $rcConf "1" }} {{ $PassDBrc := get $rcConf "2" }} {{ $DBrc := get $rcConf "3" }} NOTE: stringfied string array begins with "1" ("0" is all the string matched) !!! warning gossfiles containing text/template `{{}}` controls will no longer work with `goss add/autoadd`. One way to get around this is to split your template and static goss files and use [gossfile](#gossfile) to import. !!! note Some of Sprig functions have the same name as the older Custom Goss functions. The Sprig functions are overwritten by the custom functions for backwards compatibility. ### Examples Using [puppetlabs/facter](https://github.com/puppetlabs/facter) or [chef/ohai](https://github.com/chef/ohai) as external tools to provide vars. ```bash goss --vars <(ohai) validate goss --vars <(facter -j) validate ``` Using `mkSlice` to define a loop locally. ```yaml+jinja file: {{- range mkSlice "/etc/passwd" "/etc/group"}} {{.}}: exists: true mode: "0644" owner: root group: root filetype: file {{end}} ``` Using `upper` function from Sprig. ```yaml+jinja matching: sping_basic: content: {{ "hello!" | upper | repeat 5 }} matches: match-regexp: "HELLO!HELLO!HELLO!HELLO!HELLO!" ``` Using Env variables and a vars file: ```yaml title="vars.yaml" centos: packages: kernel: - "4.9.11-centos" - "4.9.11-centos2" debian: packages: kernel: - "4.9.11-debian" - "4.9.11-debian2" users: - user1 - user2 ``` ```yaml+jinja title="goss.yaml" package: # Looping over a variables defined in a vars.yaml using $OS environment variable as a lookup key {{range $name, $vers := index .Vars .Env.OS "packages"}} {{$name}}: installed: true versions: {{range $vers}} - {{.}} {{end}} {{end}} # This test is only when the OS environment variable matches the pattern {{if .Env.OS | regexMatch "[Cc]ent(OS|os)"}} libselinux: installed: true {{end}} # Loop over users user: {{range .Vars.users}} {{.}}: exists: true groups: - {{.}} home: /home/{{.}} shell: /bin/bash {{end}} package: {{if eq .Env.OS "centos"}} # This test is only when $OS environment variable is set to "centos" libselinux: installed: true {{end}} ``` ```console title="Rendered results" # To validate: $ OS=centos goss --vars vars.yaml validate # To render: $ OS=centos goss --vars vars.yaml render # To render with debugging enabled: $ OS=centos goss --vars vars.yaml render --debug ``` goss-0.4.9/docs/index.md000066400000000000000000000001351467505051300150650ustar00rootroot00000000000000# Goss - Quick and Easy server validation --8<-- "README.md:intro" --8<-- "README.md:about" goss-0.4.9/docs/installation.md000066400000000000000000000000531467505051300164560ustar00rootroot00000000000000# Installation --8<-- "README.md:install" goss-0.4.9/docs/license.md000066400000000000000000000000711467505051300153770ustar00rootroot00000000000000 --8<-- "LICENSE" goss-0.4.9/docs/migrations.md000066400000000000000000000021301467505051300161270ustar00rootroot00000000000000# Migration guide ## v4 migration ### Array matchers (e.g. user.groups) no longer allows duplicates Goss v0.3.X allowed: ```yaml user: root: exists: true groups: - root - root - root ``` Goss v0.4.x, will fail with the above as group "root" is only in the slice once. However, with goss v0.4.x the array may contain matchers. The test below is valid for v0.4.x but not valid for v0.3.x ```yaml user: root: exists: true groups: - have-prefix: r ``` ## rpm now contains the full EVR version To enable the ability to compare RPM versions in the future, The version matching of rpm has changed from: ```console rpm -q --nosignature --nohdrchk --nodigest --qf '%{VERSION}\n' package_name ``` to: ```console rpm -q --nosignature --nohdrchk --nodigest --qf '%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\n' package_name ``` ## `file.contains` -> `file.contents` File contains attribute has been renamed to file.contents from: ```yaml file: /tmp/foo: exists: true contains: [] ``` to: ```yaml file: /tmp/foo: exists: true contents: [] ``` goss-0.4.9/docs/myapp_gossfile.yaml000066400000000000000000000001421467505051300173370ustar00rootroot00000000000000# This is a sample file referenced by goss.yaml # Used for render test and Json schema validation.goss-0.4.9/docs/platforms.md000066400000000000000000000322101467505051300157640ustar00rootroot00000000000000--- render_macros: true fully_supported: :fontawesome-solid-circle-check:{ .green title="Fully supported/tested" } community_supported: :fontawesome-solid-circle-check:{ .blue title="Fully working/tested, community support" } not_automated: :fontawesome-solid-circle-pause:{ .green title="Works but is not covered by automated tests" } work_partially: :fontawesome-solid-circle-minus:{ .orange title="Works partially / partially tested" } not_implemented: :fontawesome-solid-circle-xmark:{ .red title="Not implemented / needs implementation" } broken: :material-image-broken-variant:{ .red title="Currently broken" } n_a: :fontawesome-regular-circle:{ .grey title="Not applicable for this platform" } no_data: :fontawesome-regular-circle-question:{ .grey title="Not yet tried, no data" } --- # Platform feature-parity macOS and Windows binaries are new and considered alpha-quality. Some functionality may be missing, some may be broken. (Enhancements and bug-reports welcome, please see #551) To clearly signal that, goss emits a log message on every invocation saying so, linking here, then exits with a clear error. To try out the alpha functionality, you must do one of: * pass `--use-alpha=1` to the root command (e.g. `goss --use-alpha=1 validate`). * set an environment variable `GOSS_USE_ALPHA=1`. The macOS and Windows support is community driven; there is no commitment to adding features / fixing bugs for those platforms. [See thread](https://github.com/goss-org/goss/pull/585#discussion_r429968540). This matrix attempts to track parity across platforms. ## Legend | Symbol | Meaning | |:-----------------------:|----------------------------------------| | {{ fully_supported }} | Fully supported/tested | | {{community_supported}} | Full working/tested, community support | | {{ not_automated }} | Works but without automated tests | | {{ work_partially }} | Works partially / partially tested | | {{ not_implemented }} | Not implemented / needs implementation | | {{ n_a }} | Not applicable for this platform | | {{ no_data }} | Not yet tried, no data | !!! note "About partial support" This is ambiguous. Where you see this, check into the test coverage within `integration-tests/goss/{darwin|windows}/{test}.goss.yaml` for more detail. It might be that not all features work as on `linux`, it might be that not all features are covered by automated tests. ## Tests/assertions support matrix | Test | Option | Linux | macOS | Windows | |:--------------------|:--------------------|:-----------------------:|:----------------------:|:-----------------------:| | **addr** | | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | reachable | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | local-address | {{ fully_supported }} | {{ no_data }} | {{ work_partially }} | | | timeout | {{ fully_supported }} | {{ not_automated }} | {{ not_automated }} | | **command** | | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | exit-status | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | stdout | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | stderr | {{ fully_supported }} | {{ not_automated }} | {{ not_automated }} | | | timeout | {{ fully_supported }} | {{ not_automated }} | {{ not_automated }} | | **dns** | | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | resolvable | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | addrs | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | server | {{ fully_supported }} | {{ no_data }} | {{ work_partially }} | | | timeout | {{ fully_supported }} | {{ not_automated }} | {{ work_partially }} | | **file** | | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | exists | {{ fully_supported }} | {{ work_partially }} | {{community_supported}} | | | mode | {{ fully_supported }} | {{ work_partially }} | {{ n_a }} | | | size | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | owner | {{ fully_supported }} | {{ broken }} | {{ n_a }} | | | group | {{ fully_supported }} | {{ broken }} | {{ n_a }} | | | filetype | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | contains | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | md5 | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | sha256 | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | linked-to | {{ fully_supported }} | {{ no_data }} | {{ no_data }} | | **gossfile** | | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | **group** | | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | exists | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | gid | {{ fully_supported }} | {{ not_implemented }} | {{ n_a }} | | **http** | | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | status | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | allow-insecure | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | no-follow-redirects | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | timeout | {{ fully_supported }} | {{ not_automated }} | {{ work_partially }} | | | request-headers | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | headers | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | body | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | username | {{ fully_supported }} | {{ not_automated }} | {{ work_partially }} | | | password | {{ fully_supported }} | {{ not_automated }} | {{ work_partially }} | | **interface** | | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | exists | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | addrs | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | mtu | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | **kernel-param** | | {{ fully_supported }} | {{ n_a }} | {{ n_a }} | | | value | {{ fully_supported }} | {{ n_a }} | {{ n_a }} | | **mount** | | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | exists | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | opts | {{ fully_supported }} | {{ not_implemented }} | {{ n_a }} | | | source | {{ fully_supported }} | {{ not_implemented }} | {{ n_a }} | | | filesystem | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | usage | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | **matching** | | {{ fully_supported }} | {{ no_data }} | {{ no_data }} | | **package** | | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | installed | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | versions | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | **port** | | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | listening | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | ip | {{ fully_supported }} | {{ no_data }} | {{ no_data }} | | **process** | | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | | running | {{ fully_supported }} | {{ work_partially }} | {{ work_partially }} | | **service** | | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | enabled | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | running | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | **user** | | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | exists | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | uid | {{ fully_supported }} | {{ not_implemented }} | {{ n_a }} | | | gid | {{ fully_supported }} | {{ not_implemented }} | {{ n_a }} | | | groups | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | home | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | shell | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | ## Commands support matrix | Test | Linux | macOS | Windows | |:-----------|------------------------|---------------------|----------------------| | `add` | {{ fully_supported }} | {{ no_data }} | {{ work_partially }} | | `autoadd` | {{ fully_supported }} | {{ no_data }} | {{ no_data }} | | `help` | {{ fully_supported }} | {{ no_data }} | {{ work_partially }} | | `render` | {{ fully_supported }} | {{ no_data }} | {{ no_data }} | | `serve` | {{ fully_supported }} | {{ not_automated }} | {{ no_data }} | | `validate` | {{ fully_supported }} | {{ not_automated }} | {{ work_partially }} | ### `command` testing notes Run all of the `darwin`/`windows` integration tests: ```bash make test-int-validate-darwin-amd64 make test-int-validate-windows-amd64 ``` The script finds all goss spec files within `integration-tests` then filters to just ones matching the passed OS-name, then runs `validate` against them. ### Command: `serve` This is a special-case test since it requires a persistent process, then to make the http request, then to tear down the process. #### macOS `serve` ```bash make "test-int-serve-darwin-amd64" ``` #### Windows `serve` ```bash make "test-int-serve-windows-amd64" ``` ## Contributing The current integration test approach is only appropriate for validating `linux` binaries against `linux` OS/arch combinations. Validating `macOS` and `Windows` binaries requires adding coverage that runs on those platforms within Travis, but since Travis does not support containerised builds for either platform, assertions are limited to assert against the state of the CI hosts, where we're relying on that to predictable. You can find goss-files that are used to populate this matrix within `integration-tests/goss/{darwin|windows}/{test}.goss.yaml`. Where a feature does note work the same as linux, it is commented. The intent is to end up with a set of running-and-passing tests. goss-0.4.9/docs/quickstart.md000066400000000000000000000000551467505051300161510ustar00rootroot00000000000000# Quick start --8<-- "README.md:quickstart" goss-0.4.9/docs/rendered_goss.yaml000066400000000000000000000056771467505051300171630ustar00rootroot00000000000000file: /ect/password/static: exists: true mode: "0644" size: 2118 owner: root group: root linked-to: /usr/sbin/sendmail.sendmail filetype: file contains: - hrll md5: 7c9bb14b3bf178e82c00c2a4398c93cd sha256: 7f78ce27859049f725936f7b52c6e25d774012947d915e7b394402cfceb70c4c sha512: cb71b1940dc879a3688bd502846bff6316dd537bbe917484964fe0f098e9245d80958258dc3bd6297bf42d5bd978cbe2c03d077d4ed45b2b1ed9cd831ceb1bd0 /etc/group: exists: true mode: "0644" owner: root group: root filetype: file contains: [] /etc/passwd: exists: true mode: "0644" owner: root group: root filetype: file contains: [] package: httpd: installed: true versions: - "2.1" kernel: installed: true versions: and: - have-len: 3 - not: contain-element: 4.1.0 addr: tcp: local-address: 127.0.0.1 reachable: true timeout: 500 port: tcp:22: listening: true ip: - "1" service: sshd: enabled: true running: true user: nfsbody: exists: true uid: 65534 gid: 65534 groups: - nfsnobody home: /var/lib/nfs shell: /sbin/nologin nobody: exists: true uid: lt: 500 groups: consist-of: - nobody sshd: title: UID must be between 50-100, GID doesn't matter. home is flexible meta: desc: Ensure sshd is enabled and running since it's needed for system management sev: 5 exists: true uid: and: gt: 50 lt: 100 home: or: - /var/empty/sshd - /var/run/sshd group: nfsnobody: exists: true gid: 65534 nobody: exists: true command: version: exec: go version exit-status: 0 stdout: - go version go1.6 linux/amd64 stderr: [] timeout: 10000 dns: localhost: resolvable: true addrs: - ::1 timeout: 0 process: chrome: running: true kernel-param: kernel.ostype: value: Linux mount: /home: exists: true timeout: 1000 opts: - rw source: /dev/mapper/fedora-home filesystem: xfs usage: lt: 95 interface: eth0: exists: true addrs: - ' 1' mtu: 1500 http: https://www.google.com: method: GET status: 200 allow-insecure: false no-follow-redirects: false timeout: 1000 body: [] matching: check_failure_count_from_all_instance: content: 0 matches: 0 check_instance_count: content: 1 matches: gt: 0 check_status: content: PASS matches: - not: FAIL example: content: - 1.0.1 - 1.9.9 matches: semver-constraint: '>1.0.0 <2.0.0 !=1.5.0' has_2: content: - 2 matches: contain-element: 2 has_foo_bar_and_baz: content: baz: bing foo: bar matches: and: - have-key-with-value: foo: bar - have-key: baz has_substr: content: some string matches: match-regexp: some str goss-0.4.9/docs/requirements.txt000066400000000000000000000002311467505051300167150ustar00rootroot00000000000000mkdocs-material==9.5.23 mkdocs-macros-plugin==1.0.5 mkdocs-awesome-pages-plugin==2.9.2 mkdocs-exclude==1.0.2 mdx-breakless-lists==1.0.1 pygments==2.18.0 goss-0.4.9/docs/schema.yaml000066400000000000000000001132041467505051300155620ustar00rootroot00000000000000$id: "https://goss.rocks/schema.yaml" # Note: this schema was authored using intellij support for Json schema version 7 # providing coding assistance # This schema is based on content from https://github.com/goss-org/goss/blob/master/docs/manual.md and # and https://github.com/goss-org/goss/blob/master/README.md # It was tested in intellij against https://github.com/goss-org/goss/blob/master/docs/goss.yaml # both for completion and code analysis use-cases. # Limitations / missing support # - patterns # - advanced matchers # - templates $schema: http://json-schema.org/draft-07/schema# title: "Goss-file-schema" definitions: title: type: string default: "UID must be between 50-100, GID doesn't matter. home is flexible" description: title attribute is persisted when adding other resources with goss add meta: description: meta (arbitrary data) attributes are persisted when adding other resources with goss add type: object addrTest: required: - reachable - timeout properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } reachable: type: boolean default: true timeout: type: integer default: 500 examples: - 500 # optional attributes local-address: type: string default: 127.0.0.1 commandTest: type: object properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } exit-status: type: integer description: "Validates the exit-status and output of a command" exec: description: "command to execute, defaults to the hash key" type: string stdout: type: array description: "can be a string or pattern, see https://goss.rocks/gossfile#patterns" items: type: string stderr: type: array description: "can be a string or pattern, see https://goss.rocks/gossfile#patterns" items: type: string timeout: type: integer description: "in milliseconds" skip: type: boolean default: false required: - exit-status dnsTest: required: - resolvable properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } resolvable: type: boolean default: true # optional attributes addrs: description: | list of addresses e.g. ["127.0.0.1", "::1"] type: array items: type: string default: ["127.0.0.1", "::1"] server: description: "Eg 8.8.8.8. Also supports server:port " type: string default: 8.8.8.8 timeout: type: integer default: 500 description: in milliseconds (Only used when server attribute is provided) fileTest: required: - exists properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } exists: type: boolean default: true mode: type: string default: "0644" size: type: integer default: 2118 description: in bytes owner: type: string default: root uid: type: integer group: type: string default: root gid: type: integer filetype: type: string default: file enum: - file - symlink - directory contains: type: array description: Check file content for these patterns. can be a string or a pattern items: type: string description: string or patterns md5: type: string default: 7c9bb14b3bf178e82c00c2a4398c93cd description: md5 checksum of file sha256: type: string default: 7f78ce27859049f725936f7b52c6e25d774012947d915e7b394402cfceb70c4c description: "A stronger checksum alternatives to md5 (recommended)" sha512: type: string default: cb71b1940dc879a3688bd502846bff6316dd537bbe917484964fe0f098e9245d80958258dc3bd6297bf42d5bd978cbe2c03d077d4ed45b2b1ed9cd831ceb1bd0 description: "A stronger checksum alternatives to md5 (recommended)" linked-to: type: string default: /usr/sbin/sendmail.sendmail skip: type: boolean default: false gossfileTest: properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } file: type: string default: myapp_gossfile.yaml skip: type: boolean default: false groupTest: required: - exists properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } exists: type: boolean default: true uid: type: integer default: 65534 gid: type: integer default: 65534 groups: type: object skip: type: boolean default: false httpTest: required: - status properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } status: type: integer default: 200 allow-insecure: type: boolean default: false no-follow-redirects: type: boolean default: false description: Setting this to true will NOT follow redirects timeout: type: integer default: 1000 request-headers: description: | Set request header values, e.g. [ "Content-Type: text/html" ] type: array items: type: string default: [ "Content-Type: text/html" ] headers: type: array description: | Check http response headers for these patterns (e.g. "Content-Type: text/html") NOTE: only the first Host header will be used to set the Request.Host value if multiple are provided. items: type: string default: [ ] request-body: type: object default: '{"key": "value"}' description: request body body: type: array description: Check http response content for these patterns username: type: string description: username for basic auth default: "" password: type: string description: password for basic auth default: "" ca-file: type: string default: "" description: | CA root certs pem file, ex: /etc/ssl/cert.pem cert-file: type: string default: "" description: | certificate file to use for authentication (used with key-file) key-file: type: string default: "" description: | private-key file to use for authentication (used with cert-file) proxy: type: string default: "" description: | proxy server to proxy traffic through. Proxy can also be set with environment variables http_proxy. skip: type: boolean default: false method: type: string default: PUT description: http method enum: # See https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods # Note: not yet clear whether goss supports all methods - GET - PUT - HEAD - POST - DELETE - PATCH - CONNECT - OPTIONS - TRACE interfaceTest: required: - exists properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } exists: type: boolean default: true addrs: type: array items: type: string default: - 172.17.0.2/16 - fe80::42:acff:fe11:2/64 mtu: type: integer default: 1500 kernelParamTest: description: | To see the full list of current values, run sysctl -a. required: - value properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } value: type: string default: Linux matchingTest: properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } content: anyOf: - type: string default: "some string" - type: array default: - 2 - type: object default: foo: bar baz: ring matches: anyOf: - type: integer - type: object - type: array mountTest: required: - exists properties: timeout: type: integer title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } exists: type: boolean default: true opts: type: array items: type: string default: - rw - relatime source: type: string default: /dev/mapper/fedora-home filesystem: type: string default: xfs usage: description: | % of blocks used in this mountpoint type: object properties: lt: type: integer default: 95 packageTest: required: - installed properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } installed: type: boolean default: true versions: oneOf: - type: object # matcher - type: array items: type: string default: - 2.2.15 skip: type: boolean default: false portTest: required: - listening properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } listening: type: boolean default: true ip: description: what IP(s) is it listening on type: array items: type: string default: - 0.0.0.0 skip: type: boolean default: false processTest: required: - running properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } running: type: boolean default: true serviceTest: required: - enabled - running - skip properties: title: { "$ref":"#/definitions/title" } meta: { "$ref":"#/definitions/meta" } enabled: type: boolean default: true running: type: boolean default: true skip: type: boolean default: false userTest: required: - exists properties: title: { "$ref": "#/definitions/title" } meta: { "$ref": "#/definitions/meta" } exists: type: boolean default: true # optional attributes uid: anyOf: - { "$ref": "#/definitions/matchingTest" } - type: integer default: 65534 gid: anyOf: - { "$ref": "#/definitions/matchingTest" } - type: integer default: 65534 groups: anyOf: - { "$ref": "#/definitions/matchingTest" } - type: array items: type: string default: - nfsnobody home: anyOf: - { "$ref": "#/definitions/matchingTest" } - type: string default: /var/lib/nfs shell: type: string default: /sbin/nologin skip: type: boolean default: false description: "A file describing a series of tests" type: "object" properties: addr: type: object description: "Validates if a remote address:port are accessible." examples: - tcp://ip-address-or-domain-name:80: reachable: true timeout: 500 local-address: 127.0.0.1 additionalProperties: $ref: "#/definitions/addrTest" command: type: object description: "test executing a command" additionalProperties: $ref: "#/definitions/commandTest" dns: type: object description: "Validates that the provided address is resolvable and the addrs it resolves to." x-intellij-html-description: |

Validates that the provided address is resolvable and the addrs it resolves to.

dns:
          localhost:
            # required attributes
            resolvable: true
            # optional attributes
            addrs:
            - 127.0.0.1
            - ::1
            server: 8.8.8.8 # Also supports server:port
            timeout: 500 # in milliseconds (Only used when server attribute is provided)

It is possible to validate the following types of DNS records, but requires the server attribute be set:

  • A
  • AAAA
  • CAA
  • CNAME
  • MX
  • NS
  • PTR
  • SRV
  • TXT

To validate specific DNS address types, prepend the hostname with the type and a colon, a few examples:

dns:
          # Validate a CNAME record
          CNAME:c.dnstest.io:
            resolvable: true
            server: 208.67.222.222
            addrs:
            - "a.dnstest.io."

          # Validate a PTR record
          PTR:8.8.8.8:
            resolvable: true
            server: 8.8.8.8
            addrs:
            - "dns.google."

          # Validate and SRV record
          SRV:_https._tcp.dnstest.io:
            resolvable: true
            server: 208.67.222.222
            addrs:
            - "0 5 443 a.dnstest.io."
            - "10 10 443 b.dnstest.io."

Please note that if you want localhost to only resolve 127.0.0.1 you'll need to use Advanced Matchers

dns:
          localhost:
            resolvable: true
            addrs:
              consist-of: [127.0.0.1]
            timeout: 500 # in milliseconds
      additionalProperties:
        $ref: "#/definitions/dnsTest"

  file:
      type: object
      description: "Validates the state of a file, directory, or symbolic link"
      additionalProperties:
        $ref: "#/definitions/fileTest"

  gossfile:
      type: object
      description: |
        Import other gossfiles from this one. This is the best way to maintain a large number of tests, and/or create profiles. See render for more examples. Glob patterns can be also be used to specify matching gossfiles.
        You can specify the gossfile(s) either as the resource key, or using the 'file' attribute.

        If the 'skip' attribute is true, then the file is not processed. If the filename is a glob pattern, then none of the matching files are processed. Note that this is not the same as skipping the contained resources; any overrides in the referenced gossfile will not be processed, and the resource count will not be incremented. Skipping a gossfile include is the same as omitting the gossfile resource entirely.
      x-intellij-html-description: |
        Import other gossfiles from this one. This is the best way to maintain a large number of tests, and/or create profiles. See render for more examples. Glob patterns can be also be used to specify matching gossfiles.

gossfile:
          myapplication:
            file: myapp_gossfile.yaml
            skip: false
          *.yaml:
            skip: true
          goss_httpd.yaml: {}
          /etc/goss.d/*.yaml: {}

You can specify the gossfile(s) either as the resource key, or using the 'file' attribute.

If the 'skip' attribute is true, then the file is not processed. If the filename is a glob pattern, then none of the matching files are processed. Note that this is not the same as skipping the contained resources; any overrides in the referenced gossfile will not be processed, and the resource count will not be incremented. Skipping a gossfile include is the same as omitting the gossfile resource entirely.

additionalProperties: $ref: "#/definitions/gossfileTest" group: type: object description: "Validates the state of a group" additionalProperties: $ref: "#/definitions/groupTest" http: type: object description: "description: Validates network interface values" additionalProperties: $ref: "#/definitions/httpTest" interface: type: object description: "test " additionalProperties: $ref: "#/definitions/interfaceTest" kernel-param: type: object description: "test " additionalProperties: $ref: "#/definitions/kernelParamTest" matching: type: object description: "Validates specified content against a matcher. Best used with Templates." x-intellij-html-description: |

Validates specified content against a matcher. Best used with Templates.

With Templates:

Let's say we have a data.json file that gets generated as part of some testing pipeline:

{
          "instance_count": 14,
          "failures": 3,
          "status": "FAIL"
        }

This could then be passed into goss: goss --vars data.json validate

And then validated against:

matching:
          check_instance_count: # Make sure there is at least one instance
            content: {{ .Vars.instance_count }}
            matches:
              gt: 0

          check_failure_count_from_all_instance: # expect no failures
            content: {{ .Vars.failures }}
            matches: 0

          check_status:
            content: {{ .Vars.status }}
            matches:
              - not: FAIL

Without Templates:

matching:
          has_substr: # friendly test name
            content: some string
            matches:
              match-regexp: some str
          has_2:
            content:
              - 2
            matches:
              contain-element: 2
          has_foo_bar_and_baz:
            content:
              foo: bar
              baz: bing
            matches:
              and:
                - have-key-with-value:
                    foo: bar
                - have-key: baz
additionalProperties: $ref: "#/definitions/matchTest" mount: type: object description: "Validates mount point attributes." additionalProperties: $ref: "#/definitions/mountTest" package: type: object description: | Validates the state of a package" NOTE: this check uses the --package parameter passed on the command line. additionalProperties: $ref: "#/definitions/packageTest" port: type: object description: | Validates the state of a local port. Note: Goss might consider your port to be listening on tcp6 rather than tcp, try running goss add port .. to see how goss detects it. (explanation) x-intellij-html-description: |

Validates the state of a local port.

Note: Goss might consider your port to be listening on tcp6 rather than tcp, try running goss add port .. to see how goss detects it. (explanation)

port:
          # {tcp,tcp6,udp,udp6}:port_num
          tcp:22:
            # required attributes
            listening: true
            # optional attributes
            ip: # what IP(s) is it listening on
            - 0.0.0.0
            skip: false
      additionalProperties:
        $ref: "#/definitions/portTest"

  process:
      type: object
      description: "Validates if a process is running."
      additionalProperties:
        $ref: "#/definitions/processTest"

  service:
      type: object
      description: "Validates the state of a service."
      additionalProperties:
        $ref: "#/definitions/serviceTest"

  user:
      type: object
      description: |
        Validates the state of a user"
        NOTE: This check is inspecting the contents of local passwd file /etc/passwd, this does not validate remote users (e.g. LDAP).
      additionalProperties:
        $ref: "#/definitions/userTest"


goss-0.4.9/docs/style.css000066400000000000000000000001631467505051300153070ustar00rootroot00000000000000.green {
    color: green;
}

.blue {
    color: cyan;
}

.orange {
    color: orange;
}

.red {
    color: red;
}
goss-0.4.9/docs/vars.yaml000066400000000000000000000002151467505051300152720ustar00rootroot00000000000000# Sample vars file used in goss.yaml#matching
# Used for render test and Json schema validation.
instance_count: 1
failures: 0
status: "PASS"goss-0.4.9/examples/000077500000000000000000000000001467505051300143235ustar00rootroot00000000000000goss-0.4.9/examples/goss.yaml000066400000000000000000000005561467505051300161700ustar00rootroot00000000000000gossfile:
  goss_awesome_gomega.yaml: {}

file:
  test.txt:
    exists: true
    contains: |
      test file
      second line

command:
  echo '15':
    exit-status: 0
    stdout:
      and:
        - gt: 10
        - lt: 50
        - match-regexp: '\d{2}'
    timeout: 10000

http:
  https://ifconfig.me:
    status: 200
    timeout: 5000
    body: '{{.Vars.Ip}}'
goss-0.4.9/examples/goss_awesome_gomega.yaml000066400000000000000000000050161467505051300212230ustar00rootroot00000000000000matching:
  # Basic matchers
  basic_string:
    content: 'this is a test'
    matches: 'this is a test'

  basic_int:
    content: 42
    matches: 42

  basic_array:
    content:
      - 'group1'
      - 'group2'
      - 'group3'
    matches:
      - 'group1'
      - 'group2'

  basic_reader:
    as-reader: true
    content: |
      foo bar
      moo cow
    matches:
      - 'foo'
      - '/^m.*w$/'
      - '!wtf'
      - '!/^ERROR:/'

  # Transformers
  basic_reader_as_array:
    as-reader: true
    content: |
      foo bar
      moo cow
    matches:
      and:
        - contain-element: {contain-substring: 'foo'}
        - contain-element: {match-regexp: '^m.*w$'}
        - not: {contain-substring: 'wtf'}
        - not: {match-regexp: '^ERROR:'}

  test_numeric_string:
    content: 128
    matches:
      and:
        - '128'
        - have-prefix: '1'
        - have-suffix: '8'
        - match-regexp: '\d{3}'

  test_string_numeric:
    content: '128'
    matches:
      and:
        - 128
        - 128.0
        - le: 128
        - gt: 120

  test_string_float:
    content: '128.3'
    matches:
      and:
        - 128.3
        - le: 129
        - gt: 120.2

  test_array:
    content:
      - '45'
      - '46'
      - '47'
    matches:
      - contain-element: {match-regexp: "4."}
      - '45'
      - and: [{ge: 46}, {le: 50}]

  test_reader_using_string_matchers:
    content: |
      foo bar
      15
      moo cow
    as-reader: true
    matches:
      and:
        - have-len: 19
        - |
          foo bar
          15
          moo cow
        - have-prefix: 'foo'
        - have-suffix: "cow\n"
        - contain-element:
            have-prefix: 'moo'
        - contain-elements:
          - not: 'this_doesnt_exist'
          - lt: 20
          - have-prefix: 'moo'


  test_reader_as_single_string:
    content: 'cool'
    as-reader: true
    matches: 'cool'

  test_reader_using_int_matchers:
    content: '40'
    as-reader: true
    matches:
      and:
       - le: 250
       - ge: 20


  test_gjson_transform:
    content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
    as-reader: true
    matches:
      gjson:
        moo.nested: cow
        foo: {have-prefix: b}
        count: {le: 25}
        '@this': {have-key: "foo"}
        moo:
          and:
            - {have-key: "nested"}
            - {not: {have-key: "nested2"}}

  test_gjson_using_this_and_equal:
    content: '{"foo": "bar", "baz": "bing"}'
    matches:
      gjson:
        '@this':
          equal:
            foo: bar
            baz: bing
goss-0.4.9/examples/readme.md000066400000000000000000000001331467505051300160770ustar00rootroot00000000000000# How to run this

Basically, run the following: `goss --vars-inline "Ip: $EXTERNAL_IP" v`
goss-0.4.9/examples/test.txt000066400000000000000000000000261467505051300160410ustar00rootroot00000000000000test file
second line
goss-0.4.9/extras/000077500000000000000000000000001467505051300140135ustar00rootroot00000000000000goss-0.4.9/extras/dcgoss/000077500000000000000000000000001467505051300152755ustar00rootroot00000000000000goss-0.4.9/extras/dcgoss/README.md000066400000000000000000000070341467505051300165600ustar00rootroot00000000000000# dcgoss

dcgoss is a convenience wrapper around goss that aims to bring the simplicity of goss to docker-compose managed
containers. It is based on `dgoss`.

## Usage

`dcgoss [run|edit] `

### Run

Run is used to validate a docker container defined in `docker-compose.yml`. It expects both a `docker-compose.yml`
and `goss.yaml` file to exist in the directory it was invoked from. Container configuration is used from the
compose file, for example:

**run:**

`docker-compose up db`

**test:**

`dcgoss run db`

`dcgoss run` will do the following:

* Start the container as defined in `docker-compose.yml`
* Stream the containers log output into the container as `/goss/docker_output.log`
    * This allows writing tests or waits against the docker output
* (optional) Run `goss` with `$GOSS_WAIT_OPTS` if `./goss_wait.yaml` file exists in the current dir
* Run `goss` with `$GOSS_OPTS` using `./goss.yaml`

### Edit

Edit will launch a docker container, install goss, and drop the user into an interactive shell.
Once the user quits the interactive shell, any `goss.yaml` or `goss_wait.yaml` are copied out into the current directory.
This allows the user to leverage the `goss add|autoadd` commands to write tests as they would on a regular machine.

**Example:**

`dcgoss edit db`

### Environment vars and defaults

The following environment variables can be set to change the behavior of dcgoss.

#### DEBUG

Enables debug output of `dcgoss`.

When running in debug mode, the tmp dir with the container output will not be cleaned up.

**Default:** empty

**Example:**

`DEBUG=true dcgoss edit db`

#### GOSS_PATH

Location of the goss binary to use.

**Default:** `$(which goss)`

#### GOSS_OPTS

Options to use for the goss test run.

**Default:** `--color --format documentation`

#### GOSS_WAIT_OPTS

Options to use for the goss wait run, when `./goss_wait.yaml` exists. (Default: `-r 30s -s 1s > /dev/null`)

#### GOSS_SLEEP

Time to sleep after running container (and optionally `goss_wait.yaml`) and before running tests.

**Default:** `0.2`

#### GOSS_FILES_PATH

Location of the goss yaml files.

**Default:** `.`

**Example:**

`GOSS_FILES_PATH=db dcgoss edit db`

#### GOSS_FILE

Allows to specify a differing name for `goss.yaml`. Useful when the same image is started for different configurations.

**Example:**

`GOSS_FILE=goss_config1.yaml dcgoss run db`

#### GOSS_VARS

The name of the variables file relative to `GOSS_FILES_PATH` to copy into the
docker container and use for valiation (i.e. `dcgoss run`) and copy out of the
docker container when writing tests (i.e. `dcgoss edit`). If set, the
`--vars` flag is passed to `goss validate` commands inside the container.
If unset (or empty), the `--vars` flag is omitted, which is the normal behavior.

**Default:** `''`

#### GOSS_FILES_STRATEGY

Strategy used for copying goss files into the docker container.
If set to `'mount'` a volume with goss files is mounted and log output is streamed into the container as
`/goss/docker_output.log` file.
Other strategy is `'cp'` which uses `'docker cp'` command to copy goss files into docker container.
With the `'cp'` strategy you lose the ability to write tests or waits against the docker output.
The `'cp'` strategy is required especially when docker daemon is not on the local machine.

**Default:** `'mount'`

## Debugging test runs

When debugging test execution its beneficual to set both `DEBUG=true` and `GOSS_WAIT_OPTS=-r 60s -s 5s`
(without the redirect to `/dev/null`).

**Example:**

`DEBUG=true GOSS_FILES_PATH=db GOSS_WAIT_OPTS="-r 60s -s 5s" dcgoss run db`
goss-0.4.9/extras/dcgoss/dcgoss000077500000000000000000000112531467505051300165070ustar00rootroot00000000000000#!/bin/bash

set -e
[ "$DEBUG" ] && set -x

USAGE="USAGE: $(basename "$0") [run|edit] "
GOSS_FILES_PATH="${GOSS_FILES_PATH:-.}"

info() {
    echo -e "INFO: $*" >&2;
}
error() {
    echo -e "ERROR: $*" >&2;
    [[ -e "$service" ]] && docker logs "$service"
    exit 1;
}

cleanup() {
    set +e
    { kill "$log_pid" && wait "$log_pid"; } 2> /dev/null
    [ "$DEBUG" ] || rm -rf "$tmp_dir"
    if [[ -n "$service" ]]; then
        info "Stopping container"
        docker-compose stop > /dev/null
    fi
}

run(){
    # Copy in goss
    cp "${GOSS_PATH}" "$tmp_dir/goss"
    chmod 755 "$tmp_dir/goss"
    [[ -e "${GOSS_FILES_PATH}/${GOSS_FILE:-goss.yaml}" ]] && install -m ugo+rw "${GOSS_FILES_PATH}/${GOSS_FILE:-goss.yaml}" "$tmp_dir"
    [[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]] && install -m ugo+rw "${GOSS_FILES_PATH}/goss_wait.yaml" "$tmp_dir"
    [[ -n "${GOSS_VARS}" ]] && [[ -e "${GOSS_FILES_PATH}/${GOSS_VARS}" ]] && install -m ugo+rw "${GOSS_FILES_PATH}/${GOSS_VARS}" "$tmp_dir"

    # Switch between mount or cp files strategy
    GOSS_FILES_STRATEGY=${GOSS_FILES_STRATEGY:="mount"}
    case "$GOSS_FILES_STRATEGY" in
      mount)
        info "Starting docker container"
        docker-compose run -d -T --name "$service" -v "$tmp_dir:/goss" --rm "${@:2}"
        ;;
      cp)
        info "Creating docker container"
        docker-compose run -d -T --name "$service" --rm "${@:2}" > /dev/null
        info "Copy goss files into container"
        docker cp "$tmp_dir/." "$service:/goss"
        ;;
      *) error "Wrong goss files strategy used! Correct options are \"mount\" or \"cp\"."
    esac

    docker logs -f "$service" > "$tmp_dir/docker_output.log" 2>&1 &
    log_pid="$!"
    info "Container name: $service"
}

get_docker_file() {
    set +e
    if docker exec "$service" sh -c "test -e $1" > /dev/null; then
        mkdir -p "${GOSS_FILES_PATH}"
        info "Copied '$1' from container to '${GOSS_FILES_PATH}'"
        docker cp "$service:$1" "${GOSS_FILES_PATH}"
    fi
    set -e
}

# Main
tmp_dir=$(mktemp -d /tmp/tmp.XXXXXXXXXX)
chmod 777 "$tmp_dir"
# shellcheck disable=SC2154
trap 'ret=$?; cleanup; info "Test ran for a total of $SECONDS seconds"; exit $ret' EXIT
service="$2"

if [[ ! -f docker-compose.yaml && ! -f docker-compose.yml ]]; then
    echo "no docker-compose file found in ."
    exit 1
fi

state=$(docker inspect --format '{{.State.Status}}' "$service" 2> /dev/null || true)
if [[ "$state" == running ]]; then
    docker rm -f "$service" > /dev/null
elif [[ "$state" == exited ]]; then
    docker rm "$service" > /dev/null
fi

GOSS_PATH="${GOSS_PATH:-$(command -v goss 2> /dev/null || true)}"
[[ "$GOSS_PATH" ]] || { error "Couldn't find goss installation, please set GOSS_PATH to it"; }
[[ "${GOSS_OPTS+x}" ]] || GOSS_OPTS="--color --format documentation"
[[ "${GOSS_WAIT_OPTS+x}" ]] || GOSS_WAIT_OPTS="-r 30s -s 1s > /dev/null"
GOSS_SLEEP="${GOSS_SLEEP:-0.2}"

case "$1" in
    run)
        run "$@"
        [[ "$GOSS_SLEEP" ]] && { info "Sleeping for $GOSS_SLEEP"; sleep "$GOSS_SLEEP"; }
        if [[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]]; then
            info "Found goss_wait.yaml, waiting for it to pass before running tests"
            if [[ -z "${GOSS_VARS}" ]]; then
                if ! docker exec "$service" sh -c "/goss/goss -g /goss/goss_wait.yaml render | /goss/goss -g - validate $GOSS_WAIT_OPTS"; then
                    error "goss_wait.yaml never passed"
                fi
            else
                if ! docker exec "$service" sh -c "/goss/goss -g /goss/goss_wait.yaml --vars='/goss/${GOSS_VARS}' render | /goss/goss -g - validate $GOSS_WAIT_OPTS"; then
                    error "goss_wait.yaml never passed"
                fi
            fi
        fi
        info "Container health"
        if [ "true" != "$(docker inspect -f '{{.State.Running}}' $service)" ]; then
            error "The container failed to start"
        fi
        info "Running Tests"
        if [[ -z "${GOSS_VARS}" ]]; then
            docker exec "$service" sh -c "/goss/goss -g /goss/${GOSS_FILE:-goss.yaml} render | /goss/goss -g - validate $GOSS_OPTS"
        else
            docker exec "$service" sh -c "/goss/goss -g /goss/${GOSS_FILE:-goss.yaml} --vars='/goss/${GOSS_VARS}' render | /goss/goss -g - validate $GOSS_OPTS"
        fi
        ;;
    edit)
        run "$@"
        info "Run goss add/autoadd to add resources"
        docker exec -it "$service" sh -c 'cd /goss; PATH="/goss:$PATH" exec sh'
        [[ -n "${GOSS_FILE}" ]] && get_docker_file "/goss/${GOSS_FILE}"
        get_docker_file "/goss/goss.yaml"
        get_docker_file "/goss/goss_wait.yaml"
        [[ -n "${GOSS_VARS}" ]] && get_docker_file "/goss/${GOSS_VARS}"
        ;;
    *)
        error "$USAGE"
esac
goss-0.4.9/extras/dcgoss/docker-compose.yml000066400000000000000000000011171467505051300207320ustar00rootroot00000000000000version: '3.3'

services:
   db:
     image: mysql:5.7
     volumes:
       - db_data:/var/lib/mysql
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: somewordpress
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: wordpress

   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     ports:
       - "8000:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD: wordpress
       WORDPRESS_DB_NAME: wordpress
volumes:
    db_data: {}
goss-0.4.9/extras/dgoss/000077500000000000000000000000001467505051300151325ustar00rootroot00000000000000goss-0.4.9/extras/dgoss/README.md000066400000000000000000000124661467505051300164220ustar00rootroot00000000000000# dgoss

dgoss is a convenience wrapper around goss that aims to bring the simplicity of goss to containers.

## Examples and Tutorials

* [blog tutorial](https://medium.com/@aelsabbahy/tutorial-how-to-test-your-docker-image-in-half-a-second-bbd13e06a4a9) -
Introduction to dgoss tutorial
* [video tutorial](https://youtu.be/PEHz5EnZ-FM) - Same as above, but in video format
* [dgoss-examples](https://github.com/aelsabbahy/dgoss-examples) - Repo containing examples of using dgoss to validate
container images

## Installation

### Linux

Follow the goss [installation instructions](https://github.com/goss-org/goss#installation)

### Mac OSX

Since goss runs on the target container, dgoss can be used on a Mac OSX system by doing the following:

```shell
# Install dgoss
curl -L https://raw.githubusercontent.com/goss-org/goss/master/extras/dgoss/dgoss -o /usr/local/bin/dgoss
chmod +rx /usr/local/bin/dgoss

# Download desired goss version to your preferred location (e.g. v0.4.8)
curl -L https://github.com/goss-org/goss/releases/download/v0.4.8/goss-linux-amd64 -o ~/Downloads/goss-linux-amd64

# Set your GOSS_PATH to the above location
export GOSS_PATH=~/Downloads/goss-linux-amd64

# Set DGOSS_TEMP_DIR to the tmp directory in your home, since /tmp is private on Mac OSX
export DGOSS_TEMP_DIR=~/tmp

# Use dgoss
dgoss edit ...
dgoss run ...
```

## Usage

`dgoss [run|edit] `

### Run

Run is used to validate a container.
It expects a `./goss.yaml` file to exist in the directory it was invoked from.
In most cases one can just substitute the runtime command (`docker` or `podman`)
for the dgoss command, for example:

**run:**

`docker run -e JENKINS_OPTS="--httpPort=8080 --httpsPort=-1" -e JAVA_OPTS="-Xmx1048m" jenkins:alpine`

**test:**

`dgoss run -e JENKINS_OPTS="--httpPort=8080 --httpsPort=-1" -e JAVA_OPTS="-Xmx1048m" jenkins:alpine`

`dgoss run` will do the following:

* Run the container with the flags you specified.
* Stream the containers log output into the container as `/goss/docker_output.log`
    * This allows writing tests or waits against the container output
* (optional) Run `goss` with `$GOSS_WAIT_OPTS` if `./goss_wait.yaml` file exists in the current dir
* Run `goss` with `$GOSS_OPTS` using `./goss.yaml`

### Edit

Edit will launch a container, install goss, and drop the user into an interactive shell.
Once the user quits the interactive shell, any `goss.yaml` or `goss_wait.yaml` are copied out into the current directory.
This allows the user to leverage the `goss add|autoadd` commands to write tests as they would on a regular machine.

**Example:**

`dgoss edit -e JENKINS_OPTS="--httpPort=8080 --httpsPort=-1" -e JAVA_OPTS="-Xmx1048m" jenkins:alpine`

### Environment vars and defaults

The following environment variables can be set to change the behavior of dgoss.

#### GOSS_PATH

Location of the goss binary to use. (Default: `$(which goss)`)

#### GOSS_FILE

Name of the goss file to use. (Default: `goss.yaml`)

#### GOSS_OPTS

Options to use for the goss test run. (Default: `--color --format documentation`)

#### GOSS_WAIT_OPTS

Options to use for the goss wait run, when `./goss_wait.yaml` exists. (Default: `-r 30s -s 1s > /dev/null`)

#### GOSS_SLEEP

Time to sleep after running container (and optionally `goss_wait.yaml`) and before running tests. (Default: `0.2`)

#### GOSS_FILES_PATH

Location of the goss yaml files. (Default: `.`)

#### GOSS_ADDITIONAL_COPY_PATH

Colon-seperated list of additional directories to copy to container.

By default dgoss copies `goss.yaml` from the current working directory and
nothing else. You may need other files like scripts and configurations copied
as well. Specify `GOSS_ADDITIONAL_COPY_PATH` similar to `$PATH` as colon seperated
list of directories for each additional directory you'd like to recursively copy.
These will be copied as directories next to `goss.yaml` in the temporary
directory `DGOSS_TEMP_DIR`. (Default: `''`)

#### GOSS_VARS

The name of the variables file relative to `GOSS_FILES_PATH` to copy into the
container and use for valiation (i.e. `dgoss run`) and copy out of the
container when writing tests (i.e. `dgoss edit`). If set, the
`--vars` flag is passed to `goss validate` commands inside the container.
If unset (or empty), the `--vars` flag is omitted, which is the normal behavior.
(Default: `''`).

#### GOSS_FILES_STRATEGY

Strategy used for copying goss files into the container. If set to `'mount'` a volume with goss files is mounted
and log output is streamed into the container as `/goss/docker_output.log` file. Other strategy is `'cp'` which uses
`'docker cp'` command to copy goss files into container. With the `'cp'` strategy you lose the ability to write
tests or waits against the container output. The `'cp'` strategy is required especially when container daemon is not on the
local machine.
(Default `'mount'`)

#### CONTAINER_LOG_OUTPUT

Location of the file that contains tested container logs. Logs are retained only if the variable is set to a non-empty
string. (Default `''`)

#### DGOSS_TEMP_DIR

Location of the temporary directory used by dgoss. (Default `'$(mktemp -d /tmp/tmp.XXXXXXXXXX)'`)

#### CONTAINER_RUNTIME

Container runtime to use - `docker` or `podman`. Defaults to `docker`. Note that `podman` requires a run command to keep
the container running. This defaults to `sleep infinity` in case only an image is passed to `dgoss` commands.
goss-0.4.9/extras/dgoss/dgoss000077500000000000000000000124331467505051300162020ustar00rootroot00000000000000#!/bin/bash

set -e

USAGE="USAGE: $(basename "$0") [run|edit] "
GOSS_FILES_PATH="${GOSS_FILES_PATH:-.}"

# Container runtime
CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-docker}"

info() {
    echo -e "INFO: $*" >&2;
}
error() {
    echo -e "ERROR: $*" >&2;
    exit 1;
}

cleanup() {
    set +e
    { kill "$log_pid" && wait "$log_pid"; } 2> /dev/null
    if [ -n "$CONTAINER_LOG_OUTPUT" ]; then
        cp "$tmp_dir/docker_output.log" "$CONTAINER_LOG_OUTPUT"
    fi
    rm -rf "$tmp_dir"
    if [[ $id ]];then
        info "Deleting container"
        $CONTAINER_RUNTIME rm -vf "$id" > /dev/null
    fi
}

run(){
    # Copy in goss
    cp "${GOSS_PATH}" "$tmp_dir/goss"
    chmod 755 "$tmp_dir/goss"
    [[ -e "${GOSS_FILES_PATH}/${GOSS_FILE:-goss.yaml}" ]] && cp "${GOSS_FILES_PATH}/${GOSS_FILE:-goss.yaml}" "$tmp_dir/goss.yaml" && chmod 644 "$tmp_dir/goss.yaml"
    [[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]] && cp "${GOSS_FILES_PATH}/goss_wait.yaml" "$tmp_dir" && chmod 644 "$tmp_dir/goss_wait.yaml"
    [[ -n "${GOSS_VARS}" ]] && [[ -e "${GOSS_FILES_PATH}/${GOSS_VARS}" ]] && cp "${GOSS_FILES_PATH}/${GOSS_VARS}" "$tmp_dir" && chmod 644 "$tmp_dir/${GOSS_VARS}"
    if [ -n "$GOSS_ADDITIONAL_COPY_PATH" ]; then
        for dir in "$(echo "$GOSS_ADDITIONAL_COPY_PATH" | sed 's/:/ /g')"; do
            cp -r ${dir} "${tmp_dir}/"
            chmod -R 755 "$tmp_dir/$(basename ${dir})"
        done
    fi

    # Switch between mount or cp files strategy
    GOSS_FILES_STRATEGY=${GOSS_FILES_STRATEGY:="mount"}
    case "$GOSS_FILES_STRATEGY" in
      mount)
        info "Starting $CONTAINER_RUNTIME container"
        if [ "$CONTAINER_RUNTIME" == "podman" -a $# == 2 ]; then
            id=$($CONTAINER_RUNTIME run -d -v "$tmp_dir:/goss:z" "${@:2}" sleep infinity)
        else
            id=$($CONTAINER_RUNTIME run -d -v "$tmp_dir:/goss:z" "${@:2}")
        fi
        ;;
      cp)
        info "Creating $CONTAINER_RUNTIME container"
        id=$($CONTAINER_RUNTIME create "${@:2}")
        info "Copy goss files into container"
        $CONTAINER_RUNTIME cp "$tmp_dir/." "$id:/goss"
        info "Starting $CONTAINER_RUNTIME container"
        $CONTAINER_RUNTIME start "$id" > /dev/null
        ;;
      *) error "Wrong goss files strategy used! Correct options are \"mount\" or \"cp\"."
    esac

    $CONTAINER_RUNTIME logs -f "$id" > "$tmp_dir/docker_output.log" 2>&1 &
    log_pid=$!
    info "Container ID: ${id:0:8}"
}

get_docker_file() {
    local cid=$1  # Docker container ID
    local src=$2  # Source file path (in the container)
    local dst=$3  # Destination file path

    if $CONTAINER_RUNTIME exec "${cid}" sh -c "test -e ${src}" > /dev/null; then
        mkdir -p "${GOSS_FILES_PATH}"
        $CONTAINER_RUNTIME cp "${cid}:${src}" "${dst}"
        info "Copied '${src}' from container to '${dst}'"
    fi
}

# Main
tmp_dir=$(mktemp -d ${DGOSS_TEMP_DIR:-/tmp}/tmp.XXXXXXXXXX)
chmod 777 "$tmp_dir"
trap 'ret=$?;cleanup;exit $ret' EXIT

GOSS_PATH="${GOSS_PATH:-$(which goss 2> /dev/null || true)}"
[[ $GOSS_PATH ]] || { error "Couldn't find goss installation, please set GOSS_PATH to it"; }
[[ ${GOSS_OPTS+x} ]] || GOSS_OPTS="--color --format documentation"
[[ ${GOSS_WAIT_OPTS+x} ]] || GOSS_WAIT_OPTS="-r 30s -s 1s > /dev/null"
GOSS_SLEEP=${GOSS_SLEEP:-0.2}

[[ $CONTAINER_RUNTIME =~ ^(docker|podman)$ ]] || { error "Runtime must be one of docker or podman"; }

case "$1" in
    run)
        run "$@"
        if [[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]]; then
            info "Found goss_wait.yaml, waiting for it to pass before running tests"
            if [[ -z "${GOSS_VARS}" ]]; then
                if ! $CONTAINER_RUNTIME exec "$id" sh -c "/goss/goss -g /goss/goss_wait.yaml validate $GOSS_WAIT_OPTS"; then
                    $CONTAINER_RUNTIME logs "$id" >&2
                    error "goss_wait.yaml never passed"
                fi
            else
                if ! $CONTAINER_RUNTIME exec "$id" sh -c "/goss/goss -g /goss/goss_wait.yaml --vars='/goss/${GOSS_VARS}' validate $GOSS_WAIT_OPTS"; then
                    $CONTAINER_RUNTIME logs "$id" >&2
                    error "goss_wait.yaml never passed"
                fi
            fi
        fi
        [[ $GOSS_SLEEP ]] && { info "Sleeping for $GOSS_SLEEP"; sleep "$GOSS_SLEEP"; }
        info "Container health"
        if [ "true" != "$($CONTAINER_RUNTIME inspect -f '{{.State.Running}}' "$id")" ]; then
            $CONTAINER_RUNTIME logs "$id" >&2
            error "the container failed to start"
        fi
        info "Running Tests"
        if [[ -z "${GOSS_VARS}" ]]; then
            $CONTAINER_RUNTIME exec "$id" sh -c "/goss/goss -g /goss/goss.yaml validate $GOSS_OPTS"
        else
            $CONTAINER_RUNTIME exec "$id" sh -c "/goss/goss -g /goss/goss.yaml --vars='/goss/${GOSS_VARS}' validate $GOSS_OPTS"
        fi
        ;;
    edit)
        run "$@"
        info "Run goss add/autoadd to add resources"
        $CONTAINER_RUNTIME exec -it "$id" sh -c 'cd /goss; PATH="/goss:$PATH" exec sh'
        get_docker_file "$id" "/goss/goss.yaml" "${GOSS_FILES_PATH}/${GOSS_FILE:-goss.yaml}"
        get_docker_file "$id" "/goss/goss_wait.yaml" "${GOSS_FILES_PATH}/goss_wait.yaml"
        if [[ -n "${GOSS_VARS}" ]]; then
            get_docker_file "$id" "/goss/${GOSS_VARS}" "${GOSS_FILES_PATH}/${GOSS_VARS}"
        fi
        ;;
    *)
        error "$USAGE"
esac
goss-0.4.9/extras/kgoss/000077500000000000000000000000001467505051300151415ustar00rootroot00000000000000goss-0.4.9/extras/kgoss/README.md000066400000000000000000000126131467505051300164230ustar00rootroot00000000000000# kgoss

kgoss is a wrapper for goss that aims to bring the simplicity of testing
with goss to containers running in pods in Kubernetes.

kgoss is a script which when invoked copies and runs goss (the binary) within a
Linux container. goss itself is only supported on Linux, but since it need only
run in the target container, the kgoss script can be used from any
bash-compatible shell, including Terminal on Mac and git-bash on Windows. On
Windows, [winpty][] is used for interactive connections to the pod under test.

[winpty]: https://github.com/rprichard/winpty

## Install

Installing kgoss requires copying the kgoss file to a directory in your PATH
and copying the goss file to your home folder (or a path set as `GOSS_PATH`),
as follows.

### Manual / UI

You can manually install kgoss and goss by going through the Web UI, getting
the files and putting them in the right path. To get each of them:

* **kgoss**: Run `curl -sSLO
  https://raw.githubusercontent.com/goss-org/goss/master/extras/kgoss/kgoss`.
* **goss**: Download the `goss-linux-amd64` asset from
   and rename it `goss`. Place it
  in your HOME directory, e.g. `C:\Users\` on Windows; or set the
  environment variable `GOSS_PATH` to its path.

### Automatic / CLI

To install from the command line or automatically, use the following commands.
[jq][] is required to parse the API response and find the release asset's
download URL.

[jq]: https://stedolan.github.io/jq

First get a GitHub personal access token for accessing the GitHub API from
. Input it in the first
line below. Set `dest_dir` to a directory in your `PATH` env var.

```shell
token=
username=$(whoami)
dest_dir=${HOME}/bin

host=raw.githubusercontent.com
repo=goss-org/goss
# for private repos, replace:
# host=github.yourcompany.com
# repo=org-name/goss

## install kgoss
curl -sSL -u "${username}:${token}" -H 'Accept: application/vnd.github.v3.raw' -o "${dest_dir}/kgoss" \
  https://${host}/api/v3/repos/${repo}/contents/extras/kgoss/kgoss
chmod a+rx "${dest_dir}/kgoss"

## install goss
if [[ ! $(which jq) ]]; then echo "jq is required, get from https://stedolan.github.io/jq"; fi
version=v0.4.8
arch=amd64
host=github.com
# for private repos, leave `host` blank or same as above:
# host=github.yourcompany.com
dl_url=$(curl -sSL -u "${username}:${token}" https://${host}/api/v3/repos/${repo}/releases \
  | jq -r ".[] | select (.name == \"${version}\") | .assets[] | select (.name == \"goss-linux-${arch}\") | .url")
curl -sSL -u "${username}:${token}" -H 'Accept: application/octet-stream' -o "${dest_dir}/goss" $dl_url
chmod a+rx "${dest_dir}/goss"

# If `goss` is not in your path, export a GOSS_PATH variable:
export GOSS_PATH=${dest_dir}/goss

# Now you can use kgoss as described below:
# kgoss edit ...
# kgoss run ...
```

## Use

`kgoss [run|edit] -i  [-p | -c "command to run" | -a "args to pass"] [-d "directory to include"]* [-e "k=v"]*`

If none of `-p|-c|-a` are specified the container is run with its configured entry point.

`-d` and `-e` can be specified multiple (or zero) times to add additional
directories and env vars.

By default kgoss copies `goss.yaml` from the current working directory and
nothing else. You may need other files like scripts and configurations copied
as well. Specify `-d ` for each additional directory you'd like
to recursively copy. These will be copied as directories next to `goss.yaml`
in the target container's `GOSS_CONTAINER_PATH`.

To find `goss.yaml` in another directory specify that directory's path in `GOSS_FILES_PATH`.

### Run

The `run` command is used to validate a container. It expects a
`./goss.yaml` file to exist in the directory it was invoked from.

**Example:**

`kgoss run -e JENKINS_OPTS="--httpPort=8080 --httpsPort=-1" -e JAVA_OPTS="-Xmx1048m" -i jenkins:alpine`

`kgoss run` will do the following:

* Run the container with the start commands specified by `-c`, `-a`, or `-p`.
* Run `goss` with `$GOSS_WAIT_OPTS` if `./goss_wait.yaml` file exists in the current dir.
* Run `goss` with `$GOSS_OPTS` using `./goss.yaml` from `GOSS_FILES_PATH`.

### Edit

Edit will launch a container, install goss, and drop the user into an
interactive shell. Once the user quits the interactive shell, any `goss.yaml`
or `goss_wait.yaml` are copied out into the current directory. This allows the
user to leverage the `goss add|autoadd` commands to write tests as they would
on a regular machine.

**Example:**

`kgoss edit -e JENKINS_OPTS="--httpPort=8080 --httpsPort=-1" -e JAVA_OPTS="-Xmx1048m" -i jenkins:alpine`

## Environment variables

The following environment variables effect the behavior of kgoss.

Variable | Description | Default
---------|-------------|--------
GOSS\_PATH | Local location of a compatible goss binary to use in container | `$(which goss)`
GOSS\_FILES\_PATH | Location of the goss yaml files | `.`
GOSS\_KUBECTL\_BIN | Kubenetes client tool to use | `$(which kubectl)`
GOSS\_KUBECTL\_OPTS | Options to inject more options such as "--namespace=default" | ""
GOSS\_OPTS | Options to use for the goss test run. | `--color --format documentation`
GOSS\_WAIT\_OPTS | Options to use for the goss wait run, when `./goss_wait.yaml` exists. | `-r 30s -s 1s > /dev/null`
GOSS\_VARS | Variables file relative to `GOSS_FILES_PATH` to copy and use | ""
GOSS\_CONTAINER\_PATH | Path within container to put goss binary and YAML files | `/tmp/goss`
goss-0.4.9/extras/kgoss/kgoss000077500000000000000000000220241467505051300162150ustar00rootroot00000000000000#! /usr/bin/env bash

set -eo pipefail

info() {
    echo -e "[INFO]: $*" >&2
}

error() {
    echo -e "[ERROR]: $*" >&2
    exit 1
}

usage() {
>&2 cat <<-'EOF'
Usage: $(basename $0) [command] [options]

## Commands:

* `run` executes goss in the pod/container with ./goss.yaml as input (by
default).
* `edit` opens a prompt inside the container to run `goss add ...`
and copies out files when complete.

## Options:

-i="image_url:tag" - full URL of container image
-d="additional directories to copy to container" - may be specified zero to
    many times
-e="envvar_key=value" - may be specified zero to many times
-p - (flag) pause container on entry
-c="cmd to run" - command to execute as container entry point
-a="args to entrypoint"

If -p, -c and -a are not specified, container will run its ENTRYPOINT.

-e and -d can be specified multiple times.

## Environment variables and default values:

GOSS_KUBECTL_BIN="$(which kubectl)": location of kubectl-compatible binary
GOSS_KUBECTL_OPTS="": hook to inject more options such as "--namespace=default"
GOSS_PATH="$(which goss)": location of goss binary
GOSS_FILES_PATH=".": location of goss.yaml and other configuration files
GOSS_VARS="": path to a goss.vars file
GOSS_OPTS="--color --format documentation": options passed to goss
GOSS_WAIT_OPTS="-r 30s -s 1s > /dev/null": options passed to goss
GOSS_CONTAINER_PATH="/tmp/goss": path to copy files in container, and working dir for tests
EOF

exit 2
}

# GOSS_PATH
if [[ -z "${GOSS_PATH}" ]]; then
    if [[ $(which goss 2> /dev/null) ]]; then
        GOSS_PATH=$(which goss 2> /dev/null)
    elif [[ -e "${HOME}/goss" ]]; then
        GOSS_PATH="${HOME}/goss"
    elif [[ -e "${HOME}/bin/goss" ]]; then 
        GOSS_PATH="${HOME}/bin/goss"
    else
        error "Couldn't find goss, please set GOSS_PATH to it"
    fi
fi

# GOSS_KUBECTL_BIN
GOSS_KUBECTL_BIN=${GOSS_KUBECTL_BIN:-$(which kubectl 2> /dev/null || true)}
if [[ -z "$GOSS_KUBECTL_BIN" ]]; then error "kgoss requires kubectl in your PATH"; fi
k=${GOSS_KUBECTL_BIN}

GOSS_FILES_PATH="${GOSS_FILES_PATH:-.}"
GOSS_OPTS=${GOSS_OPTS:-"--color --format documentation"}
GOSS_WAIT_OPTS=${GOSS_WAIT_OPTS:-"-r 30s -s 1s > /dev/null"}
GOSS_CONTAINER_PATH=${GOSS_CONTAINER_PATH:-/tmp/goss}
GOSS_KUBECTL_OPTS=${GOSS_KUBECTL_OPTS:-""}

kgoss_cmd=run
image=
pause=0
cmd=''
args=''
to_exec=''
envs=''
include_goss_files_dir=0
dirs_array=()

cleanup() {
    set +ex
    rm -rf "$tmp_dir"
    if [[ -n "$id" ]]; then
        info "Deleting pod/container"
        ${k} delete pod "$id" ${GOSS_KUBECTL_OPTS} > /dev/null
    fi
}

# parse checks for a bare `-d` flag and if set includes GOSS_FILES_PATH in dirs
# to upload to pod
parse() {
  # handle deprecated bare `-d`
  i=0
  original_args=("$@")
  new_args=()
  re='^-'
  for arg in "${original_args[@]}"; do
    if [[ "${arg}" == '-d' ]]; then
      # check if next word starts with '-'
      if [[ "${original_args[$(($i+1))]}" =~ $re ]]; then
        # since it does, mark to copy whole dir and remove this arg
        include_goss_files_dir=1
        i=$(($i+1))
        continue
      fi
    fi
    i=$(($i+1))
    new_args+=("${arg}")
  done
  # end handle `-d`

  # now call original parse_internal func
  parse_internal "${new_args[@]}"
}

parse_internal() {
  info "Parsing command line"
  kgoss_cmd=$1; shift
  if [[ ( ! "${kgoss_cmd}" == "run" ) && ( ! "${kgoss_cmd}" == 'edit' ) ]]; then usage; fi
  envs_array=()
  while getopts 'i:pc::a::d::e::' arg; do
    case $arg in
      i)
        image="${OPTARG}"
        info "using image: $image"
        ;;
      p)
        pause=1
        ;;
      c)
        cmd="${OPTARG}"
        ;;
      a)
        args="${OPTARG}"
        ;;
      d)
        dirs_array+=("${OPTARG}")
        ;;
      e)
        envs_array+=("${OPTARG}")
        ;;
      *)
        info "invalid option specified"
        usage
        ;;
    esac
  done

  for envvar in "${envs_array[@]}"; do
    envs+=" --env=${envvar}"
  done

  # if -p (pause) is set, then -c (command) and -a (args) should be empty and
  # we inject a pause
  if [[ $pause == 1 ]]; then
    if [[ ! ( -z "$cmd" && -z "$args" ) ]]; then
      error "cannot specify -p and -c or -a"
    fi
    to_exec="--command -- sleep 1h"
  else
    # if not -p (pause), then either:
    #   * one of -c (command) or -a (args) should be set
    #   * neither should be set and we default to entrypoint
    if [[ -n "$cmd" && -n "$args" ]]; then
      error "cannot specify both -c and -a"
    fi
    if [[ -n "$cmd" ]]; then
      to_exec="--command -- $cmd"
    fi
    if [[ -n $"args" ]]; then
      to_exec="-- $args"
    fi
  fi
  info "going to execute (may be blank): ${to_exec}"
}

# initialize starts the pod to be tested and copies goss files into it
initialize () {
    info "Preparing files to copy into container"
    cp "${GOSS_PATH}" "$tmp_dir/goss" && chmod 0775 "$tmp_dir/goss"
    [[ -e "${GOSS_FILES_PATH}/goss.yaml" ]] && cp "${GOSS_FILES_PATH}/goss.yaml" "$tmp_dir"
    [[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]] && cp "${GOSS_FILES_PATH}/goss_wait.yaml" "$tmp_dir"
    [[ ! -z "${GOSS_VARS}" ]] && [[ -e "${GOSS_FILES_PATH}/${GOSS_VARS}" ]] && cp "${GOSS_FILES_PATH}/${GOSS_VARS}" "$tmp_dir"
    if [[ ${include_goss_files_dir} == 1 ]]; then cp -r ${GOSS_FILES_PATH}/* "${tmp_dir}"; fi
    for dir in "${dirs_array[@]}"; do
      cp -r ${dir} "${tmp_dir}/"
    done

    GOSS_FILES_STRATEGY=${GOSS_FILES_STRATEGY:="cp"}
    case "$GOSS_FILES_STRATEGY" in
      cp)
        info "Creating Kubernetes pod/container to test"
        test_pod_name=kgoss-tester-${RANDOM}
        set -x
        id=$(${k} run ${GOSS_KUBECTL_OPTS}  $test_pod_name --image-pull-policy=Always --restart=Never \
          --labels='app=kgoss-test' --output=jsonpath={.metadata.name} ${envs} \
          --image=${image} ${to_exec} )
        set +x
        info "Waiting for container to be ready"
        ${k} wait pod/${test_pod_name} --for=condition=Ready --timeout=60s  ${GOSS_KUBECTL_OPTS}
        info "Copying goss files into pod/container"
        ${k} cp ${GOSS_KUBECTL_OPTS} $tmp_dir/. ${id}:${GOSS_CONTAINER_PATH}/
        info "Marking copied files as executable"
         ${k} exec ${GOSS_KUBECTL_OPTS} "$id" -- sh -c "chmod -R a+x ${GOSS_CONTAINER_PATH}/"
        ;;
      *) error "Wrong kgoss files strategy used! Only \"cp\" is supported."
    esac

    info "Using pod/container: ${id}"
}

# get_pod_file copies the specified file from the pod to a local path
get_pod_file() {
    if  ${k} exec ${GOSS_KUBECTL_OPTS} "$id" -- sh -c "test -e ${GOSS_CONTAINER_PATH}/$1" &> /dev/null; then
        mkdir -p "${GOSS_FILES_PATH}"
        info "Copied '$1' from pod/container to '${GOSS_FILES_PATH}'"
        ${k} cp ${GOSS_KUBECTL_OPTS} "${id}:${GOSS_CONTAINER_PATH}/$1" "${GOSS_FILES_PATH}/$1"
    fi
}

main() {
    kernel="$(uname -s)"
    case "${kernel}" in
        MINGW*) prefix="winpty" ;;
        *)      prefix="" ;;
    esac

    tmp_dir=$(mktemp -d /tmp/tmp.XXXXXXXXXX)
    chmod 777 "$tmp_dir"
    trap 'ret=$?; cleanup; exit $ret' EXIT

    parse "$@"
    initialize

    # execute
    case $kgoss_cmd in
        run)
            # wait for goss_wait.yaml if present
            if [[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]]; then
                info "Found goss_wait.yaml, waiting for it to pass before running tests"
                if [[ -z "${GOSS_VARS}" ]]; then
                    if !  ${k} exec ${GOSS_KUBECTL_OPTS} "$id" -- sh -c "${GOSS_CONTAINER_PATH}/goss -g ${GOSS_CONTAINER_PATH}/goss_wait.yaml validate $GOSS_WAIT_OPTS" ; then
                        error "goss_wait.yaml never passed"
                    fi
                else
                    if !  ${k} exec ${GOSS_KUBECTL_OPTS} "$id" -- sh -c "${GOSS_CONTAINER_PATH}/goss -g ${GOSS_CONTAINER_PATH}/goss_wait.yaml --vars='${GOSS_CONTAINER_PATH}/${GOSS_VARS}' validate $GOSS_WAIT_OPTS" ; then
                        error "goss_wait.yaml never passed"
                    fi
                fi
            fi

            # running tests in pod/container
            info "Running tests within pod/container"
            if [[ -z "${GOSS_VARS}" ]]; then
                 ${k} exec ${GOSS_KUBECTL_OPTS} "$id" -- sh -c "cd ${GOSS_CONTAINER_PATH}; ${GOSS_CONTAINER_PATH}/goss -g ${GOSS_CONTAINER_PATH}/goss.yaml validate $GOSS_OPTS"
            else
                 ${k} exec ${GOSS_KUBECTL_OPTS} "$id" -- sh -c "cd ${GOSS_CONTAINER_PATH}; ${GOSS_CONTAINER_PATH}/goss -g ${GOSS_CONTAINER_PATH}/goss.yaml --vars='${GOSS_CONTAINER_PATH}/${GOSS_VARS}' validate $GOSS_OPTS"
            fi
            ;;
        edit)
            info "When prompt appears you can run \`goss add\` to add resources"
            ${prefix}  ${k} exec ${GOSS_KUBECTL_OPTS} -it "$id" -- sh -c "cd ${GOSS_CONTAINER_PATH}; PATH=\"${GOSS_CONTAINER_PATH}:$PATH\" exec sh" || true
            echo "Copying goss.yaml and goss_wait.yaml files back to local dir"
            get_pod_file "goss.yaml"
            get_pod_file "goss_wait.yaml"
            [[ ! -z "${GOSS_VARS}" ]] && get_pod_file "${GOSS_VARS}"
            ;;
        *)
            echo "invalid kgoss command, valid commands are 'run' and 'edit'"
            usage
            ;;
    esac
}

main "$@"
goss-0.4.9/go.mod000066400000000000000000000044541467505051300136220ustar00rootroot00000000000000module github.com/goss-org/goss

go 1.22

require (
	github.com/Masterminds/sprig/v3 v3.3.0
	github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2
	github.com/blang/semver/v4 v4.0.0
	github.com/cheekybits/genny v1.0.0
	github.com/fatih/color v1.17.0
	github.com/goss-org/GOnetstat v0.0.0-20230101144325-22be0bd9e64d
	github.com/goss-org/go-ps v0.0.0-20230609005227-7b318e6a56e5
	github.com/hashicorp/logutils v1.0.0
	github.com/miekg/dns v1.1.61
	github.com/moby/sys/mountinfo v0.7.1
	github.com/oleiade/reflections v1.0.1
	github.com/onsi/gomega v1.33.1
	github.com/patrickmn/go-cache v2.1.0+incompatible
	github.com/pmezard/go-difflib v1.0.0
	github.com/prometheus/client_golang v1.19.1
	github.com/prometheus/common v0.55.0
	github.com/samber/lo v1.46.0
	github.com/stretchr/testify v1.9.0
	github.com/tidwall/gjson v1.17.1
	github.com/urfave/cli v1.22.14
	gopkg.in/yaml.v3 v3.0.1
	gotest.tools/v3 v3.5.1
)

require (
	dario.cat/mergo v1.0.1 // indirect
	github.com/Masterminds/goutils v1.1.1 // indirect
	github.com/Masterminds/semver/v3 v3.3.0 // indirect
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/google/go-cmp v0.6.0 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/huandu/xstrings v1.5.0 // indirect
	github.com/mattn/go-colorable v0.1.13 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/mitchellh/copystructure v1.2.0 // indirect
	github.com/mitchellh/reflectwalk v1.0.2 // indirect
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
	github.com/prometheus/client_model v0.6.1 // indirect
	github.com/prometheus/procfs v0.15.1 // indirect
	github.com/russross/blackfriday/v2 v2.1.0 // indirect
	github.com/shopspring/decimal v1.4.0 // indirect
	github.com/spf13/cast v1.7.0 // indirect
	github.com/tidwall/match v1.1.1 // indirect
	github.com/tidwall/pretty v1.2.1 // indirect
	golang.org/x/crypto v0.26.0 // indirect
	golang.org/x/mod v0.19.0 // indirect
	golang.org/x/net v0.27.0 // indirect
	golang.org/x/sync v0.8.0 // indirect
	golang.org/x/sys v0.23.0 // indirect
	golang.org/x/text v0.17.0 // indirect
	golang.org/x/tools v0.23.0 // indirect
	google.golang.org/protobuf v1.34.2 // indirect
)
goss-0.4.9/go.sum000066400000000000000000000301321467505051300136370ustar00rootroot00000000000000dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2 h1:NYoPVh1XuUB5VBWLXRKoqzQhl4bajIxh+XuURbJ0uwc=
github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2/go.mod h1:DCNKSpXhum14Y258jSbRmJvcesbzEdBPincz7yJUx3k=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/goss-org/GOnetstat v0.0.0-20230101144325-22be0bd9e64d h1:50mlZKtg8BUvBtFs0ioVpSgMMwcKaJefg/2pZ+lQf98=
github.com/goss-org/GOnetstat v0.0.0-20230101144325-22be0bd9e64d/go.mod h1:MBdRlloGIbpQVDuH5Gxg3hjqwZBCZsmFqbYPaeR6r0M=
github.com/goss-org/go-ps v0.0.0-20230609005227-7b318e6a56e5 h1:NW0Jo4leMIrQxNOyOkBu4yBnygI37m0Ey0EUUgvzr+8=
github.com/goss-org/go-ps v0.0.0-20230609005227-7b318e6a56e5/go.mod h1:FYj70SLmogHdTTDGnIVaaK0iczROlsxmoMCwfAUuIE8=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g=
github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM=
github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60=
github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g=
github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ=
github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
goss-0.4.9/goss_config.go000066400000000000000000000113301467505051300153320ustar00rootroot00000000000000package goss

import (
	"log"
	"reflect"

	"github.com/goss-org/goss/resource"
)

type GossConfig struct {
	Files        resource.FileMap        `json:"file,omitempty" yaml:"file,omitempty"`
	Packages     resource.PackageMap     `json:"package,omitempty" yaml:"package,omitempty"`
	Addrs        resource.AddrMap        `json:"addr,omitempty" yaml:"addr,omitempty"`
	Ports        resource.PortMap        `json:"port,omitempty" yaml:"port,omitempty"`
	Services     resource.ServiceMap     `json:"service,omitempty" yaml:"service,omitempty"`
	Users        resource.UserMap        `json:"user,omitempty" yaml:"user,omitempty"`
	Groups       resource.GroupMap       `json:"group,omitempty" yaml:"group,omitempty"`
	Commands     resource.CommandMap     `json:"command,omitempty" yaml:"command,omitempty"`
	DNS          resource.DNSMap         `json:"dns,omitempty" yaml:"dns,omitempty"`
	Processes    resource.ProcessMap     `json:"process,omitempty" yaml:"process,omitempty"`
	Gossfiles    resource.GossfileMap    `json:"gossfile,omitempty" yaml:"gossfile,omitempty"`
	KernelParams resource.KernelParamMap `json:"kernel-param,omitempty" yaml:"kernel-param,omitempty"`
	Mounts       resource.MountMap       `json:"mount,omitempty" yaml:"mount,omitempty"`
	Interfaces   resource.InterfaceMap   `json:"interface,omitempty" yaml:"interface,omitempty"`
	HTTPs        resource.HTTPMap        `json:"http,omitempty" yaml:"http,omitempty"`
	Matchings    resource.MatchingMap    `json:"matching,omitempty" yaml:"matching,omitempty"`
}

func NewGossConfig() *GossConfig {
	return &GossConfig{
		Files:        make(resource.FileMap),
		Packages:     make(resource.PackageMap),
		Addrs:        make(resource.AddrMap),
		Ports:        make(resource.PortMap),
		Services:     make(resource.ServiceMap),
		Users:        make(resource.UserMap),
		Groups:       make(resource.GroupMap),
		Commands:     make(resource.CommandMap),
		DNS:          make(resource.DNSMap),
		Processes:    make(resource.ProcessMap),
		Gossfiles:    make(resource.GossfileMap),
		KernelParams: make(resource.KernelParamMap),
		Mounts:       make(resource.MountMap),
		Interfaces:   make(resource.InterfaceMap),
		HTTPs:        make(resource.HTTPMap),
		Matchings:    make(resource.MatchingMap),
	}
}

// Merge consumes all the resources in g2 into c, duplicate resources
// will be overwritten with the ones in g2
func (c *GossConfig) Merge(g2 GossConfig) {
	for k, v := range g2.Files {
		mergeType(c.Files, "file", k, v)
	}

	for k, v := range g2.Packages {
		mergeType(c.Packages, "package", k, v)
	}

	for k, v := range g2.Addrs {
		mergeType(c.Addrs, "addr", k, v)
	}

	for k, v := range g2.Ports {
		mergeType(c.Ports, "port", k, v)
	}

	for k, v := range g2.Services {
		mergeType(c.Services, "service", k, v)
	}

	for k, v := range g2.Users {
		mergeType(c.Users, "user", k, v)
	}

	for k, v := range g2.Groups {
		mergeType(c.Groups, "group", k, v)
	}

	for k, v := range g2.Commands {
		mergeType(c.Commands, "command", k, v)
	}

	for k, v := range g2.DNS {
		mergeType(c.DNS, "dns", k, v)
	}

	for k, v := range g2.Processes {
		mergeType(c.Processes, "process", k, v)
	}

	for k, v := range g2.KernelParams {
		mergeType(c.KernelParams, "kernel-param", k, v)
	}

	for k, v := range g2.Mounts {
		mergeType(c.Mounts, "mount", k, v)
	}

	for k, v := range g2.Interfaces {
		mergeType(c.Interfaces, "interface", k, v)
	}

	for k, v := range g2.HTTPs {
		mergeType(c.HTTPs, "http", k, v)
	}

	for k, v := range g2.Matchings {
		mergeType(c.Matchings, "matching", k, v)
	}
}

func mergeType[V any](m map[string]V, t, k string, v V) {
	if _, ok := m[k]; ok {
		log.Printf("[WARN] Duplicate key detected: '%s: %s'. The value from a later-loaded goss file has overwritten the previous value.", t, k)
	}
	m[k] = v
}

func (c *GossConfig) Resources() []resource.Resource {
	var tests []resource.Resource

	gm := genericConcatMaps(c.Commands,
		c.HTTPs,
		c.Addrs,
		c.DNS,
		c.Packages,
		c.Services,
		c.Files,
		c.Processes,
		c.Users,
		c.Groups,
		c.Ports,
		c.KernelParams,
		c.Mounts,
		c.Interfaces,
		c.Matchings,
	)

	for _, m := range gm {
		for _, t := range m {
			// FIXME: Can this be moved to a safer compile-time check?
			tests = append(tests, t.(resource.Resource))
		}
	}

	return tests
}

func genericConcatMaps(maps ...any) (ret []map[string]any) {
	for _, slice := range maps {
		im := interfaceMap(slice)
		ret = append(ret, im)
	}
	return ret
}

func interfaceMap(slice any) map[string]any {
	m := reflect.ValueOf(slice)
	if m.Kind() != reflect.Map {
		panic("InterfaceSlice() given a non-slice type")
	}

	ret := make(map[string]any)

	for _, k := range m.MapKeys() {
		ret[k.Interface().(string)] = m.MapIndex(k).Interface()
	}

	return ret
}

func mergeGoss(g1, g2 GossConfig) GossConfig {
	g1.Gossfiles = nil

	g1.Merge(g2)

	return g1
}
goss-0.4.9/goss_test.go000066400000000000000000000066401467505051300150540ustar00rootroot00000000000000package goss

import (
	"bytes"
	"encoding/json"
	"os"
	"testing"

	"github.com/goss-org/goss/outputs"
	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
)

func checkErr(t *testing.T, err error, format string, a ...any) {
	t.Helper()
	if err == nil {
		return
	}

	t.Fatalf(format+": "+err.Error(), a...)
}

func TestConfigMerge(t *testing.T) {
	var g1json = `file:
  /etc/passwd:
    exists: true
    mode: "0644"
    size: 1722
    owner: root
    group: root
    filetype: file
    contains: []`

	var g2json = `service:
  sshd:
    enabled: true
    running: true
`

	g1, err := ReadJSONData([]byte(g1json), true)
	checkErr(t, err, "reading g1 failed")
	_, ok := g1.Services["sshd"]
	if ok {
		t.Fatalf("did not expect sshd service")
	}

	g2, err := ReadJSONData([]byte(g2json), true)
	checkErr(t, err, "reading g1 failed")

	g1.Merge(g2)
	_, ok = g1.Files["/etc/passwd"]
	if !ok {
		t.Fatalf("expected passwd file, got none")
	}
	_, ok = g1.Services["sshd"]
	if !ok {
		t.Fatalf("expected sshd service, got none")
	}
}

func TestUseAsPackage(t *testing.T) {
	output := &bytes.Buffer{}

	// temp spec file
	fh, err := os.CreateTemp("", "*.yaml")
	checkErr(t, err, "temp file failed")
	fh.Close()

	// new config that doesnt spam output etc
	cfg, err := util.NewConfig(util.WithFormatOptions("pretty"), util.WithResultWriter(output), util.WithSpecFile(fh.Name()))
	checkErr(t, err, "new config failed")

	// adds the os tmp dir to the goss spec file
	err = AddResources(fh.Name(), "File", []string{os.TempDir()}, cfg)
	checkErr(t, err, "could not add resource %q", os.TempDir())

	// validate and sanity check, compare structured vs direct results etc
	results, err := ValidateResults(cfg)
	checkErr(t, err, "check failed")

	found := 0
	passed := 0
	for rg := range results {
		for _, r := range rg {
			found++

			if r.Result == resource.SUCCESS {
				passed++
			}
		}
	}

	code, err := Validate(cfg)
	checkErr(t, err, "check failed")
	if code != 0 {
		t.Fatalf("check failed, expected 0 got %d", code)
	}

	res := &outputs.StructuredOutput{}
	err = json.Unmarshal(output.Bytes(), res)
	checkErr(t, err, "unmarshal failed")

	if res.Summary.Failed != 0 {
		t.Fatalf("expected 0 failed, got %d", res.Summary.Failed)
	}

	if len(res.Results) != found {
		t.Fatalf("expected %d results for %d", found, len(res.Results))
	}

	okcount := 0
	for _, r := range res.Results {
		if r.Result == resource.SUCCESS {
			okcount++
		}
	}

	if okcount != passed {
		t.Fatalf("expected %d passed but got %d", passed, okcount)
	}
}

func TestSkipResourcesByType(t *testing.T) {
	output := &bytes.Buffer{}

	// temp spec file
	fh, err := os.CreateTemp("", "*.yaml")
	checkErr(t, err, "temp file failed")
	fh.Close()

	// new config that doesnt spam output etc
	cfg, err := util.NewConfig(util.WithFormatOptions("pretty"), util.WithResultWriter(output), util.WithSpecFile(fh.Name()), util.WithDisabledResourceTypes("file"))
	checkErr(t, err, "new config failed")

	// adds the os tmp dir to the goss spec file
	err = AddResources(fh.Name(), "File", []string{os.TempDir()}, cfg)
	checkErr(t, err, "could not add resource %q", os.TempDir())

	// validate and sanity check, compare structured vs direct results etc
	results, err := ValidateResults(cfg)
	checkErr(t, err, "check failed")

	skipped := 0
	for rg := range results {
		for _, r := range rg {
			if r.Skipped {
				skipped++
			}
		}
	}

	if skipped != 5 {
		t.Fatalf("Expected to skip 5 tests, skipped %d", skipped)
	}
}
goss-0.4.9/install.sh000066400000000000000000000026311467505051300145110ustar00rootroot00000000000000#!/bin/sh

{
set -e

LATEST_URL="https://github.com/goss-org/goss/releases/latest"
LATEST_EFFECTIVE=$(curl -s -L -o /dev/null ${LATEST_URL} -w '%{url_effective}')
LATEST=${LATEST_EFFECTIVE##*/}

DGOSS_VER=$GOSS_VER

if [ -z "$GOSS_VER" ]; then
    GOSS_VER=${GOSS_VER:-$LATEST}
    DGOSS_VER='master'
fi
if [ -z "$GOSS_VER" ]; then
    echo "ERROR: Could not automatically detect latest version, set GOSS_VER env var and re-run"
    exit 1
fi
GOSS_DST=${GOSS_DST:-/usr/local/bin}
INSTALL_LOC="${GOSS_DST%/}/goss"
DGOSS_INSTALL_LOC="${GOSS_DST%/}/dgoss"
touch "$INSTALL_LOC" || { echo "ERROR: Cannot write to $GOSS_DST set GOSS_DST elsewhere or use sudo"; exit 1; }

arch=""
if [ "$(uname -m)" = "x86_64" ]; then
    arch="amd64"
elif [ "$(uname -m)" = "aarch32" ]; then
    arch="arm"
elif [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; then
    arch="arm64"
else
    arch="386"
fi

url="https://github.com/goss-org/goss/releases/download/$GOSS_VER/goss-linux-$arch"

echo "Downloading $url"
curl -L "$url" -o "$INSTALL_LOC"
chmod +rx "$INSTALL_LOC"
echo "Goss $GOSS_VER has been installed to $INSTALL_LOC"
echo "goss --version"
"$INSTALL_LOC" --version

dgoss_url="https://raw.githubusercontent.com/goss-org/goss/$DGOSS_VER/extras/dgoss/dgoss"
echo "Downloading $dgoss_url"
curl -L "$dgoss_url" -o "$DGOSS_INSTALL_LOC"
chmod +rx "$DGOSS_INSTALL_LOC"
echo "dgoss $DGOSS_VER has been installed to $DGOSS_INSTALL_LOC"
}
goss-0.4.9/integration-tests/000077500000000000000000000000001467505051300161705ustar00rootroot00000000000000goss-0.4.9/integration-tests/Dockerfile_alpine3000066400000000000000000000006721467505051300216020ustar00rootroot00000000000000FROM alpine:3.19
LABEL org.opencontainers.image.authors="Ahmed"

# install apache2 and remove un-needed services
RUN apk update && \
  apk add --no-cache openrc apache2=2.4.59-r0 bash ca-certificates tinyproxy && \
  sed -i 's/Listen 80/Listen 0.0.0.0:80/g' /etc/apache2/httpd.conf && \
  rc-update add apache2 && \
  rc-update add tinyproxy && \
  rm -rf /etc/init.d/networking /etc/init.d/hwdrivers /var/cache/apk/* /tmp/*
RUN mkfifo /pipe
goss-0.4.9/integration-tests/Dockerfile_alpine3.md5000066400000000000000000000000651467505051300222620ustar00rootroot000000000000003c4e7fbf89cd2edfeae94728e247213d  Dockerfile_alpine3
goss-0.4.9/integration-tests/Dockerfile_arch000066400000000000000000000001671467505051300211630ustar00rootroot00000000000000FROM archlinux:base
MAINTAINER @siddharthist

RUN ln -s /does_not_exist /foo && \
    chmod 700 ~root
RUN mkfifo /pipe
goss-0.4.9/integration-tests/Dockerfile_arch.md5000066400000000000000000000000621467505051300216410ustar00rootroot000000000000008fc3ce0c000f89ab09488cccb3ba8e66  Dockerfile_arch
goss-0.4.9/integration-tests/Dockerfile_centos7000066400000000000000000000016561467505051300216340ustar00rootroot00000000000000FROM centos:7.2.1511
LABEL org.opencontainers.image.authors="Ahmed"

ENV container docker
RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \
    rm -f /lib/systemd/system/multi-user.target.wants/*;\
    rm -f /etc/systemd/system/*.wants/*;\
    rm -f /lib/systemd/system/local-fs.target.wants/*; \
    rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
    rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
    rm -f /lib/systemd/system/basic.target.wants/*;\
    rm -f /lib/systemd/system/anaconda.target.wants/*;
VOLUME [ "/sys/fs/cgroup" ]
CMD ["/usr/sbin/init"]

RUN yum -y --disablerepo='*' --enablerepo=base,extras install httpd epel-release && yum clean all
RUN yum -y --disablerepo='*' --enablerepo=base,epel install tinyproxy && yum clean all

RUN systemctl enable httpd
RUN systemctl enable tinyproxy
RUN chmod 700 ~root
RUN mkfifo /pipe
goss-0.4.9/integration-tests/Dockerfile_centos7.md5000066400000000000000000000000651467505051300223110ustar00rootroot00000000000000148b069bc0a023068cbcdfe8b24fe036  Dockerfile_centos7
goss-0.4.9/integration-tests/Dockerfile_rockylinux9000066400000000000000000000016511467505051300225450ustar00rootroot00000000000000FROM rockylinux:9

ENV container docker

RUN dnf install -y systemd httpd diffutils 'dnf-command(config-manager)' && \
    dnf config-manager --set-enabled crb && \
    dnf install -y epel-release && \
    dnf install -y tinyproxy && \
    dnf remove -y 'dnf-command(config-manager)' epel-release

RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \
    rm -f /lib/systemd/system/multi-user.target.wants/*;\
    rm -f /etc/systemd/system/*.wants/*;\
    rm -f /lib/systemd/system/local-fs.target.wants/*; \
    rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
    rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
    rm -f /lib/systemd/system/basic.target.wants/*;\
    rm -f /lib/systemd/system/anaconda.target.wants/*;

CMD ["/usr/sbin/init"]

RUN systemctl enable httpd
RUN systemctl enable tinyproxy
RUN chmod 700 ~root
RUN mkfifo /pipe
goss-0.4.9/integration-tests/Dockerfile_trusty000066400000000000000000000010561467505051300216160ustar00rootroot00000000000000FROM ubuntu-upstart:trusty
LABEL org.opencontainers.image.authors="Ahmed"

RUN apt-get update && \
    apt-get install -y apache2=2.4.7-1ubuntu4.22 tinyproxy && \
    apt-get remove -y vim-tiny && \
    apt-get clean

RUN sed -i '/reload|force-reload)/i  status) pidof tinyproxy > /dev/null && echo "tinyproxy is running";;' /etc/init.d/tinyproxy
RUN sed -i '/start)/a\        touch /var/log/tinyproxy/tinyproxy.log /var/run/tinyproxy/tinyproxy.pid' /etc/init.d/tinyproxy

RUN update-rc.d apache2 defaults
RUN update-rc.d tinyproxy defaults
RUN mkfifo /pipe
goss-0.4.9/integration-tests/Dockerfile_trusty.md5000066400000000000000000000000641467505051300223000ustar00rootroot000000000000009db0e607ec52f1fd1290785721733180  Dockerfile_trusty
goss-0.4.9/integration-tests/Dockerfile_wheezy000066400000000000000000000010521467505051300215530ustar00rootroot00000000000000FROM debian:wheezy
LABEL org.opencontainers.image.authors="Ahmed"

RUN echo 'deb http://archive.debian.org/debian wheezy main' > /etc/apt/sources.list
RUN echo 'deb http://archive.debian.org/debian-security wheezy/updates main' >> /etc/apt/sources.list

RUN apt-get -o Acquire::Check-Valid-Until=false update && apt-get install --yes --force-yes \
    apache2 apache2-doc apache2-utils chkconfig vim-tiny ca-certificates tinyproxy && \
    apt-get remove -y vim-tiny && apt-get clean

RUN chkconfig apache2 on
RUN chkconfig tinyproxy on
RUN mkfifo /pipe
goss-0.4.9/integration-tests/Dockerfile_wheezy.md5000066400000000000000000000000641467505051300222410ustar00rootroot000000000000003775dbcd23497095da8f5b7ddb62a540  Dockerfile_wheezy
goss-0.4.9/integration-tests/Find-AvailablePort.ps1000066400000000000000000000006731467505051300222260ustar00rootroot00000000000000param(
    # Start port scanning at
    [int] $startAt = 1025,
    # End port scanning at
    [int] $endAt = 65535
)
for ($port=$startAt; $port -lt $endAt; $port++) {
    $listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Any, $port)
    try {
        $listener.Start()
        write-output "$port"
        break
    }
    catch {
        write-host "$port busy"
    }
    finally {
        $listener.Stop()
    }
}
goss-0.4.9/integration-tests/goss/000077500000000000000000000000001467505051300171435ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/alpine3/000077500000000000000000000000001467505051300204765ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/alpine3/goss-aa-expected.yaml000066400000000000000000000001761467505051300245170ustar00rootroot00000000000000package:
  apache2:
    installed: true
    versions:
    - 2.4.59-r0
service:
  apache2:
    enabled: true
    running: true
goss-0.4.9/integration-tests/goss/alpine3/goss-expected-q.yaml000066400000000000000000000042111467505051300243700ustar00rootroot00000000000000file:
  /etc/passwd:
    exists: true
    contents: []
  /tmp/goss/foobar:
    exists: false
    contents: []
package:
  apache2:
    installed: true
  foobar:
    installed: false
  vim-tiny:
    installed: false
addr:
  tcp://httpbin:22:
    reachable: false
    timeout: 1000
  tcp://httpbin:80:
    reachable: true
    timeout: 1000
  udp://8.8.8.8:53:
    reachable: true
    timeout: 1000
port:
  tcp:80:
    listening: true
  tcp:9999:
    listening: false
  tcp6:80:
    listening: false
service:
  apache2:
    enabled: true
    running: true
  foobar:
    enabled: false
    running: false
user:
  foobar:
    exists: false
  www-data:
    exists: false
group:
  foobar:
    exists: false
  www-data:
    exists: true
command:
  echo 'hi':
    exit-status: 0
    stdout: ""
    stderr: ""
    timeout: 10000
  foobar:
    exit-status: 127
    stdout: ""
    stderr: ""
    timeout: 10000
dns:
  CAA:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  CNAME:c.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  MX:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  NS:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  PTR:54.243.154.1:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  TXT:txt._test.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  ip6.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  localhost:
    resolvable: true
    timeout: 1000
process:
  apache2:
    running: false
  foobar:
    running: false
kernel-param:
  kernel.ostype:
    value: Linux
mount:
  /dev:
    exists: true
    timeout: 1000
http:
  http://google.com:
    status: 301
    allow-insecure: false
    no-follow-redirects: true
    timeout: 5000
    body: []
  https://www.apple.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
    proxy: http://127.0.0.1:8888
  https://www.google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
goss-0.4.9/integration-tests/goss/alpine3/goss-expected.yaml000066400000000000000000000055631467505051300241450ustar00rootroot00000000000000file:
  /etc/passwd:
    exists: true
    mode: "0644"
    owner: root
    group: root
    filetype: file
    contents: []
  /tmp/goss/foobar:
    exists: false
    contents: []
package:
  apache2:
    installed: true
    versions:
    - 2.4.59-r0
  foobar:
    installed: false
  vim-tiny:
    installed: false
addr:
  tcp://httpbin:22:
    reachable: false
    timeout: 1000
  tcp://httpbin:80:
    reachable: true
    timeout: 1000
  udp://8.8.8.8:53:
    reachable: true
    timeout: 1000
port:
  tcp:80:
    listening: true
    ip:
    - 0.0.0.0
  tcp:9999:
    listening: false
    ip: []
  tcp6:80:
    listening: false
    ip: []
service:
  apache2:
    enabled: true
    running: true
  foobar:
    enabled: false
    running: false
user:
  foobar:
    exists: false
  www-data:
    exists: false
group:
  foobar:
    exists: false
  www-data:
    exists: true
    gid: 82
command:
  echo 'hi':
    exit-status: 0
    stdout:
    - hi
    stderr: ""
    timeout: 10000
  foobar:
    exit-status: 127
    stdout: ""
    stderr:
    - 'sh: foobar: not found'
    timeout: 10000
dns:
  CAA:dnstest.io:
    resolvable: true
    addrs:
    - 0 issue comodoca.com
    - 0 issue letsencrypt.org
    - 0 issuewild ;
    timeout: 1000
    server: 8.8.8.8
  CNAME:c.dnstest.io:
    resolvable: true
    addrs:
    - a.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  MX:dnstest.io:
    resolvable: true
    addrs:
    - 10 b.dnstest.io.
    - 5 a.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  NS:dnstest.io:
    resolvable: true
    addrs:
    - ns1.dnstest.io.
    - ns2.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  PTR:54.243.154.1:
    resolvable: true
    addrs:
    - ec2-54-243-154-1.compute-1.amazonaws.com.
    timeout: 1000
    server: 8.8.8.8
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    addrs:
    - 0 5 443 a.dnstest.io.
    - 10 10 443 b.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  TXT:txt._test.dnstest.io:
    resolvable: true
    addrs:
    - Hello DNS
    timeout: 1000
    server: 8.8.8.8
  ip6.dnstest.io:
    resolvable: true
    addrs:
    - 2404:6800:4001:807::200e
    timeout: 1000
    server: 8.8.8.8
  localhost:
    resolvable: true
    addrs:
    - 127.0.0.1
    - ::1
    timeout: 1000
process:
  apache2:
    running: false
  foobar:
    running: false
kernel-param:
  kernel.ostype:
    value: Linux
mount:
  /dev:
    exists: true
    opts:
    - rw
    - nosuid
    vfs-opts:
    - rw
    source: tmpfs
    filesystem: tmpfs
    timeout: 1000
http:
  http://google.com:
    status: 301
    allow-insecure: false
    no-follow-redirects: true
    timeout: 5000
    body: []
  https://www.apple.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
    proxy: http://127.0.0.1:8888
  https://www.google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
goss-0.4.9/integration-tests/goss/alpine3/goss.yaml000066400000000000000000000007371467505051300223440ustar00rootroot00000000000000---
service:
  autofs:
    enabled: false
    running: false
user:
  apache:
    exists: true
    uid: 100
    gid: 101
    groups:
    - apache
    home: "/var/www"
group:
  apache:
    exists: true
    gid: 101
process:
  httpd:
    running: true
port:
  tcp:80:
    listening: true
    ip:
    - "0.0.0.0"
addr:
  tcp://127.0.0.1:80:
    reachable: true
    timeout: 500
    local-address: 127.0.0.1
gossfile:
  "../goss-s*.yaml": {}
  bypath:
    file: "../goss-dummy.yaml"

goss-0.4.9/integration-tests/goss/arch/000077500000000000000000000000001467505051300200605ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/arch/goss.yaml000066400000000000000000000004641467505051300217230ustar00rootroot00000000000000---
package:
  curl:
    installed: true
  pacman:
    installed: true
  foobar:
    installed: false
user:
  root:
    exists: true
    uid: 0
    gid: 0
    home: "/root"
file:
  "/foo":
    exists: true
    filetype: symlink
gossfile:
  "../goss-shared.yaml": {}
  bypath:
    file: "../goss-dummy.yaml"

goss-0.4.9/integration-tests/goss/centos7/000077500000000000000000000000001467505051300205255ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/centos7/goss-aa-expected.yaml000066400000000000000000000003421467505051300245410ustar00rootroot00000000000000package:
  httpd:
    installed: true
    versions:
    - 2.4.6-95.el7.centos
port:
  tcp:80:
    listening: true
    ip:
    - 0.0.0.0
service:
  httpd:
    enabled: true
    running: true
process:
  httpd:
    running: true
goss-0.4.9/integration-tests/goss/centos7/goss-expected-q.yaml000066400000000000000000000041751467505051300244300ustar00rootroot00000000000000file:
  /etc/passwd:
    exists: true
    contents: []
  /tmp/goss/foobar:
    exists: false
    contents: []
package:
  foobar:
    installed: false
  httpd:
    installed: true
  vim-tiny:
    installed: false
addr:
  tcp://httpbin:22:
    reachable: false
    timeout: 1000
  tcp://httpbin:80:
    reachable: true
    timeout: 1000
  udp://8.8.8.8:53:
    reachable: true
    timeout: 1000
port:
  tcp:80:
    listening: true
  tcp:9999:
    listening: false
  tcp6:80:
    listening: false
service:
  foobar:
    enabled: false
    running: false
  httpd:
    enabled: true
    running: true
user:
  apache:
    exists: true
  foobar:
    exists: false
group:
  apache:
    exists: true
  foobar:
    exists: false
command:
  echo 'hi':
    exit-status: 0
    stdout: ""
    stderr: ""
    timeout: 10000
  foobar:
    exit-status: 127
    stdout: ""
    stderr: ""
    timeout: 10000
dns:
  CAA:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  CNAME:c.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  MX:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  NS:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  PTR:54.243.154.1:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  TXT:txt._test.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  ip6.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  localhost:
    resolvable: true
    timeout: 1000
process:
  foobar:
    running: false
  httpd:
    running: true
kernel-param:
  kernel.ostype:
    value: Linux
mount:
  /dev:
    exists: true
    timeout: 1000
http:
  http://google.com:
    status: 301
    allow-insecure: false
    no-follow-redirects: true
    timeout: 5000
    body: []
  https://www.apple.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
    proxy: http://127.0.0.1:8888
  https://www.google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
goss-0.4.9/integration-tests/goss/centos7/goss-expected.yaml000066400000000000000000000057361467505051300241760ustar00rootroot00000000000000file:
  /etc/passwd:
    exists: true
    mode: "0644"
    owner: root
    group: root
    filetype: file
    contents: []
  /tmp/goss/foobar:
    exists: false
    contents: []
package:
  foobar:
    installed: false
  httpd:
    installed: true
    versions:
    - 2.4.6-95.el7.centos
  vim-tiny:
    installed: false
addr:
  tcp://httpbin:22:
    reachable: false
    timeout: 1000
  tcp://httpbin:80:
    reachable: true
    timeout: 1000
  udp://8.8.8.8:53:
    reachable: true
    timeout: 1000
port:
  tcp:80:
    listening: true
    ip:
    - 0.0.0.0
  tcp:9999:
    listening: false
    ip: []
  tcp6:80:
    listening: false
    ip: []
service:
  foobar:
    enabled: false
    running: false
  httpd:
    enabled: true
    running: true
user:
  apache:
    exists: true
    uid: 48
    gid: 48
    groups:
    - apache
    home: /usr/share/httpd
    shell: /sbin/nologin
  foobar:
    exists: false
group:
  apache:
    exists: true
    gid: 48
  foobar:
    exists: false
command:
  echo 'hi':
    exit-status: 0
    stdout:
    - hi
    stderr: ""
    timeout: 10000
  foobar:
    exit-status: 127
    stdout: ""
    stderr:
    - 'sh: foobar: command not found'
    timeout: 10000
dns:
  CAA:dnstest.io:
    resolvable: true
    addrs:
    - 0 issue comodoca.com
    - 0 issue letsencrypt.org
    - 0 issuewild ;
    timeout: 1000
    server: 8.8.8.8
  CNAME:c.dnstest.io:
    resolvable: true
    addrs:
    - a.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  MX:dnstest.io:
    resolvable: true
    addrs:
    - 10 b.dnstest.io.
    - 5 a.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  NS:dnstest.io:
    resolvable: true
    addrs:
    - ns1.dnstest.io.
    - ns2.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  PTR:54.243.154.1:
    resolvable: true
    addrs:
    - ec2-54-243-154-1.compute-1.amazonaws.com.
    timeout: 1000
    server: 8.8.8.8
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    addrs:
    - 0 5 443 a.dnstest.io.
    - 10 10 443 b.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  TXT:txt._test.dnstest.io:
    resolvable: true
    addrs:
    - Hello DNS
    timeout: 1000
    server: 8.8.8.8
  ip6.dnstest.io:
    resolvable: true
    addrs:
    - 2404:6800:4001:807::200e
    timeout: 1000
    server: 8.8.8.8
  localhost:
    resolvable: true
    addrs:
    - 127.0.0.1
    - ::1
    timeout: 1000
process:
  foobar:
    running: false
  httpd:
    running: true
kernel-param:
  kernel.ostype:
    value: Linux
mount:
  /dev:
    exists: true
    opts:
    - rw
    - nosuid
    vfs-opts:
    - rw
    source: tmpfs
    filesystem: tmpfs
    timeout: 1000
http:
  http://google.com:
    status: 301
    allow-insecure: false
    no-follow-redirects: true
    timeout: 5000
    body: []
  https://www.apple.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
    proxy: http://127.0.0.1:8888
  https://www.google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
goss-0.4.9/integration-tests/goss/centos7/goss.yaml000066400000000000000000000007371467505051300223730ustar00rootroot00000000000000service:
  autofs:
    enabled: false
    running: false
user:
  apache:
    exists: true
    uid: 48
    gid: 48
    groups:
    - apache
    home: "/usr/share/httpd"
group:
  apache:
    exists: true
    gid: 48
process:
  httpd:
    running: true
port:
  tcp:80:
    listening: true
    ip:
    - '0.0.0.0'
addr:
  tcp://127.0.0.1:80:
    reachable: true
    timeout: 500
    local-address: 127.0.0.1
gossfile:
  "../goss-s*.yaml": {}
  bypath:
    file: "../goss-dummy.yaml"
goss-0.4.9/integration-tests/goss/darwin/000077500000000000000000000000001467505051300204275ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/darwin/commands/000077500000000000000000000000001467505051300222305ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/darwin/commands/add.goss.yaml000066400000000000000000000003601467505051300246150ustar00rootroot00000000000000---
# TODO: coverage for the add {test} permutations
command:
  "add addr 127.0.0.1":
    exit-status: 0
    exec: release/goss-darwin-amd64 --use-alpha=1 add addr 127.0.0.1
    stdout:
    - "timeout: 500"
    stderr: []
    timeout: 5000
goss-0.4.9/integration-tests/goss/darwin/commands/autoadd.goss.yaml000066400000000000000000000004461467505051300255130ustar00rootroot00000000000000---
command:
  "autoadd /Users/travis":
    exit-status: 0
    exec: "release/goss-darwin-amd64 --use-alpha=1 autoadd /Users/travis"
    stdout:
      - 'file:'
      - '  exists: true'
      - '  filetype: directory'
    stderr: []
    timeout: 5000

    # needs implementation
    skip: true
goss-0.4.9/integration-tests/goss/darwin/commands/help.goss.yaml000066400000000000000000000002161467505051300250150ustar00rootroot00000000000000---
command:
  help:
    exit-status: 0
    exec: "release/goss-darwin-amd64 help"
    stdout:
      - alpha
    stderr: []
    timeout: 5000
goss-0.4.9/integration-tests/goss/darwin/commands/validate-input.yaml000066400000000000000000000000601467505051300260360ustar00rootroot00000000000000---
file:
  non-existent.txt:
    exists: false
goss-0.4.9/integration-tests/goss/darwin/commands/validate.goss.yaml000066400000000000000000000005031467505051300256550ustar00rootroot00000000000000---
# TODO: coverage for the add {test} permutations
command:
  "validate":
    exit-status: 0
    exec: "release/goss-darwin-amd64 --use-alpha=1 -g integration-tests/goss/darwin/commands/validate-input.yaml validate"
    stdout:
      - 'Count: 1'
      - 'Failed: 0'
      - 'Skipped: 0'
    stderr: []
    timeout: 5000
goss-0.4.9/integration-tests/goss/darwin/tests/000077500000000000000000000000001467505051300215715ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/darwin/tests/addr.goss.yaml000066400000000000000000000003751467505051300243460ustar00rootroot00000000000000---
addr:
  tcp://google.com:443:
    reachable: true
    timeout: 1000

  # TODO: needs implementation (or figure out a likely listening port on macOS/travis)
  # tcp://127.0.0.1:135:
  #   reachable: true
  #   timeout: 1000
  #   local-address: true
goss-0.4.9/integration-tests/goss/darwin/tests/command.goss.yaml000066400000000000000000000002141467505051300250420ustar00rootroot00000000000000---
command:
  hello world:
    exit-status: 0
    exec: "echo hello world"
    stdout:
    - hello world
    stderr: []
    timeout: 10000
goss-0.4.9/integration-tests/goss/darwin/tests/dns.goss.yaml000066400000000000000000000001431467505051300242110ustar00rootroot00000000000000---
dns:
  localhost:
    resolvable: true
    addrs:
    - "127.0.0.1"
    - ::1
    timeout: 500
goss-0.4.9/integration-tests/goss/darwin/tests/file.goss.yaml000066400000000000000000000006311467505051300243460ustar00rootroot00000000000000---
file:
  integration-tests/goss/testdata/static-file.txt:
    exists: true
    mode: "0644"
    # user: ""  # TODO: not working on Darwin
    # group: ""  # TODO: not working on Darwin
    size: 20
    filetype: file
    md5: 9dcea4037b1439a2a96e4d206eda63a4
    sha256: e73d885411a52a0d29142e830e104e0cc9252fbb1dc3c92a430ef7c369f089ef
    contents:
      - "nothing to see here"
      - "/nothing.*here/"
goss-0.4.9/integration-tests/goss/darwin/tests/gossfile.goss.yaml000066400000000000000000000007511467505051300252450ustar00rootroot00000000000000---
# paths are relative to the goss file that includes the gossfile directive.
gossfile:
  addr.goss.yaml: {}
  command.goss.yaml: {}
  dns.goss.yaml: {}
  file.goss.yaml: {}
  # don't use gossfile; avoid self-referencing
  # gossfile.goss.yaml: {}
  group.goss.yaml: {}
  http.goss.yaml: {}
  interface.goss.yaml: {}
  # kernel-param.na-goss.yaml: {}
  mount.goss.yaml: {}
  package.goss.yaml: {}
  port.goss.yaml: {}
  process.goss.yaml: {}
  service.goss.yaml: {}
  user.goss.yaml: {}
goss-0.4.9/integration-tests/goss/darwin/tests/group.goss.yaml000066400000000000000000000001471467505051300245650ustar00rootroot00000000000000---
group:
  _developers:
    exists: true
    gid: 0

    # TODO: needs implementation
    skip: true
goss-0.4.9/integration-tests/goss/darwin/tests/http.goss.yaml000066400000000000000000000004371467505051300244120ustar00rootroot00000000000000---
http:
  https://google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 10000
    request-headers:
      - "Content-Type: text/html"
    headers:
      - "Content-Type: text/html"
    body:
      - "google"
    username: ""
    password: ""
goss-0.4.9/integration-tests/goss/darwin/tests/interface.goss.yaml000066400000000000000000000002061467505051300253650ustar00rootroot00000000000000---
interface:
  eth0:
    exists: true
    addrs:
      - '127.0.0.1'
    mtu: 1500

    # TODO: needs implementation
    skip: true
goss-0.4.9/integration-tests/goss/darwin/tests/kernel-param.na-goss.yaml000066400000000000000000000002161467505051300264000ustar00rootroot00000000000000---
kernel-param:
  notapplicable.on-darwin:
    value: foobar

    # TODO: need implementation or signal no support on Darwin
    skip: true
goss-0.4.9/integration-tests/goss/darwin/tests/mount.goss.yaml000066400000000000000000000002361467505051300245720ustar00rootroot00000000000000---
mount:
  '/':
    exists: true
    filesystem: hdfs

    opts: []
    source: ''
    usage:
      lt: 95

    # TODO: needs implementation
    skip: true
goss-0.4.9/integration-tests/goss/darwin/tests/package.goss.yaml000066400000000000000000000004031467505051300250170ustar00rootroot00000000000000---
package:
  golang:
    # required attributes
    installed: true
    # optional attributes
    versions:
    - 1.14.1

    # needs implementation
    # needs discussion + design
    # support question for:
    # * homebrew
    # * macports
    skip: true
goss-0.4.9/integration-tests/goss/darwin/tests/port.goss.yaml000066400000000000000000000001601467505051300244100ustar00rootroot00000000000000---
port:
  tcp:135:
    listening: true
    ip:
    - 0.0.0.0

    # TODO: needs implementation
    skip: true
goss-0.4.9/integration-tests/goss/darwin/tests/process.goss.yaml000066400000000000000000000000471467505051300251060ustar00rootroot00000000000000---
process:
  bash:
    running: true
goss-0.4.9/integration-tests/goss/darwin/tests/service.goss.yaml000066400000000000000000000001551467505051300250700ustar00rootroot00000000000000---
service:
  launchd:
    enabled: true
    running: true

    # TODO: needs implementation
    skip: true
goss-0.4.9/integration-tests/goss/darwin/tests/user.goss.yaml000066400000000000000000000003031467505051300244010ustar00rootroot00000000000000---
user:
  travis:
    exists: true
    uid: 65534
    gid: 65534
    groups:
    - _developers
    home: /Users/travis
    shell: /sbin/nologin

    # TODO: needs implementation
    skip: true
goss-0.4.9/integration-tests/goss/generate_goss.sh000077500000000000000000000060271467505051300223340ustar00rootroot00000000000000#!/usr/bin/env bash

SCRIPT_DIR=$(readlink -f $(dirname $0))

OS=$1
ARCH=$2
[[ $3 == "-q" ]] && args=("--exclude-attr" "*")

goss() {
  $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml "$@"
  # Validate that duplicates are ignored
  $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml "$@"
}

rm -f $SCRIPT_DIR/${OS}/goss*generated*-$ARCH.yaml

for x in /etc/passwd /tmp/goss/foobar;do
  goss a "${args[@]}" file $x
done

[[ $OS == "centos7" || $OS == "rockylinux9" ]] && package="httpd" || package="apache2"
[[ $OS == "centos7" || $OS == "rockylinux9" ]] && user="apache" || user="www-data"
goss a "${args[@]}" package $package foobar vim-tiny

goss a "${args[@]}" addr --timeout 1s httpbin:80 httpbin:22

goss a "${args[@]}" addr --timeout 1s udp://8.8.8.8:53

goss a "${args[@]}" port tcp:80 tcp6:80 9999

goss a "${args[@]}" service $package foobar

goss a "${args[@]}" user $user foobar

goss a "${args[@]}" group $user foobar

goss a "${args[@]}" command "echo 'hi'" foobar

goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 CNAME:c.dnstest.io

goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 MX:dnstest.io

goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 NS:dnstest.io

goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 PTR:54.243.154.1

goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 SRV:_https._tcp.dnstest.io

goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 TXT:txt._test.dnstest.io

goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 CAA:dnstest.io

goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 ip6.dnstest.io

goss a "${args[@]}" dns --timeout 1s localhost

goss a "${args[@]}" process $package foobar

goss a "${args[@]}" kernel-param kernel.ostype

goss a "${args[@]}" mount /dev
# Make tests consistent across different docker setups
sed -i '/- seclabel/d' $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml
sed -i '/- size=/d' $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml
sed -i '/- mode=/d' $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml
sed -i '/- inode64/d' $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml

goss a "${args[@]}" http https://www.google.com

goss a "${args[@]}" http https://www.apple.com -x http://127.0.0.1:8888

goss a "${args[@]}" http http://google.com -r

# Auto-add
# Validate that empty configs don't get created
$SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa nosuchresource
if [[ -f $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml ]]
then
  echo "Error! Empty config file exists!" && exit 1
fi
$SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa $package
# Validate that duplicates are ignored
$SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa $package
# Validate that we can aa none existent resources without destroying the file
$SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa nosuchresource

if [[ ! -f $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml ]]
then
  echo "Error! Config file removed by aa!" && exit 1
fi
goss-0.4.9/integration-tests/goss/goss-dummy.yaml000066400000000000000000000001501467505051300221270ustar00rootroot00000000000000
---
command:
  includetest:
    exec: echo 'hi'
    exit-status: 0
    stdout:
    - hi
    stderr: []
goss-0.4.9/integration-tests/goss/goss-serve.yaml000066400000000000000000000001511467505051300221210ustar00rootroot00000000000000---
command:
  hello world:
    exec: echo 'hi'
    exit-status: 0
    stdout:
      - hi
    stderr: []
goss-0.4.9/integration-tests/goss/goss-service.yaml000066400000000000000000000005111467505051300224350ustar00rootroot00000000000000---
service:
  foobar:
    enabled: false
    running: false
{{ if .Env.OS | regexMatch "centos[7]|rockylinux[9]" }}
  httpd:
{{else}}
  apache2:
{{end}}
{{ if .Env.OS | regexMatch "trusty" }}
    enabled: false
{{else}}
    enabled: true
{{end}}
    running: true
  skippable:
    enabled: true
    running: true
    skip: true
goss-0.4.9/integration-tests/goss/goss-shared.yaml000066400000000000000000000134401467505051300222500ustar00rootroot00000000000000---
command:
  echo 'hi':
    exit-status: 0
    stdout:
    - hi
    stderr: []
  foobar:
    exit-status: 127
    stdout: []
    stderr:
    - not found
  command-override:
    exec: true
    exit-status: 0
  commandskip:
    exec: false
    exit-status: 0
    skip: true
file:
{{range mkSlice "/etc/PAsswD" "/etc/group"}}
  {{. | toLower}}:
    exists: true
    mode: '0644'
    owner: root
    uid: 0
    group: root
    gid: 0
    filetype: file
    contents:
    - root
{{end}}
  "/goss/hellogoss.txt":
    exists: true
    md5: 7c9bb14b3bf178e82c00c2a4398c93cd
    sha256: 7f78ce27859049f725936f7b52c6e25d774012947d915e7b394402cfceb70c4c
    sha512: 372864ab83187de41ca57c5c77cd4a99220ccadc8b8ddb18367893fd3e58764193a599edbf63a48c0c44f1e923606a00929b46de3bda1744fd722b9d42829206
  "/tmp/goss/foobar":
    exists: false
    contents: []
  "~root":
    exists: true
    mode: '0700'
  "/tmp":
    exists: true
    mode: '1777'
  "/dev/random":
    exists: true
    filetype: character-device
  "/pipe":
    exists: true
    filetype: pipe
  "/does/not/exist":
    exists: true
    contents:
    - skip-this-test
    skip: true
package:
  foobar:
    installed: false
{{- range $name, $ver := index .Vars .Env.OS "packages"}}
  {{$name}}:
    installed: true
    versions:
    - {{$ver}}
{{end}}
service:
{{- range $name, $runlevels := index .Vars .Env.OS "services"}}
  {{$name}}:
    enabled: true
    running: true
    runlevels: {{toJson $runlevels}}
{{end}}
addr:
  tcp://httpbin:22:
    reachable: false
    timeout: 1000
  tcp://httpbin:80:
    reachable: true
    timeout: 5000
  tcp://httpbin:999:
    reachable: false
    timeout: 5000
    local-address: 127.0.0.1
port:
  tcp:9999:
    listening: false
user:
  root:
    exists: true
  foobar:
    exists: false
group:
  foobar:
    exists: false
dns:
  CAA:dnstest.io:
    resolvable: true
    addrs:
    - 0 issue comodoca.com
    - 0 issue letsencrypt.org
    - 0 issuewild ;
    timeout: 2000
    server: 8.8.8.8
  CNAME:c.dnstest.io:
    resolvable: true
    addrs:
    - a.dnstest.io.
    timeout: 2000
    server: 8.8.8.8
  c.dnstest.io:
    resolvable: true
    addrs:
    - 192.30.252.153
    timeout: 2000
    server: 8.8.8.8:53
  MX:dnstest.io:
    resolvable: true
    addrs:
    - 10 b.dnstest.io.
    - 5 a.dnstest.io.
    timeout: 2000
    server: 8.8.8.8:53
  NS:dnstest.io:
    resolvable: true
    addrs:
    - ns1.dnstest.io.
    - ns2.dnstest.io.
    timeout: 2000
    server: 8.8.8.8
  PTR:54.243.154.1:
    resolvable: true
    addrs:
    - ec2-54-243-154-1.compute-1.amazonaws.com.
    timeout: 2000
    server: 8.8.8.8
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    addrs:
    - 0 5 443 a.dnstest.io.
    - 10 10 443 b.dnstest.io.
    timeout: 2000
    server: 8.8.8.8
  TXT:txt._test.dnstest.io:
    resolvable: true
    addrs:
    - Hello DNS
    timeout: 2000
    server: 8.8.8.8
  ip6.dnstest.io:
    resolvable: true
    addrs:
    - 2404:6800:4001:807::200e
    timeout: 2000
    server: 8.8.8.8
  localhost:
    resolvable: true
    addrs:
    - 127.0.0.1
    timeout: 2000
  dnstest.io:
    resolvable: true
    server: 8.8.8.8
    timeout: 2000
process:
  foobar:
    running: false
kernel-param:
  kernel.ostype:
    value: Linux
mount:
  "/dev":
    exists: true
    timeout: 1000
    opts:
    - rw
    - nosuid
    vfs-opts:
    - mode=755
    source: tmpfs
    filesystem: tmpfs
  "/":
    exists: true
    usage:
      and:
        - lt: 95
        - gt: 0
interface:
  eth0:
    exists: true
    addrs:
      contain-element:
        have-prefix: '172.'
http:
  {{ if index .Vars .Env.OS "proxy" }}
  http://httpbin/anything:
    status: 200
    timeout: 60000
    proxy: {{ index .Vars .Env.OS "proxy" }}
  {{ end }}
  http://httpbin/headers:
    status: 200
    timeout: 60000
    request-headers:
      - "Foo: bar"
    headers: ["Content-Type: application/json"]
    body:
      - '"Foo": "bar"'
      - '/"User-Agent": "goss/v?[0-9]+.[0-9]+.[0-9]+"/'
  http://httpbin/headers?host:
    status: 200
    timeout: 60000
    request-headers:
      # This is causing intermittent errors depending on the httpbin server hit
      # need to see if there's a good way around this, maybe local httpbin?
      - "Host: httpbin"
    headers: ["Content-Type: application/json"]
    body: ['"Host": "httpbin"']
  http://httpbin/basic-auth/username/secret:
    status: 200
    timeout: 60000
    username: username
    password: secret
  http://httpbin/basic-auth/username/secret?failure:
    status: 401
    timeout: 60000
    username: username
    password: wrong
  http://httpbin/put:
    status: 200
    method: PUT
    timeout: 60000
    request-body: '{"key": "value"}'
    body:
      - '"key": "value"'
  anything-with-get:
    url: http://httpbin/anything
    status: 200
    timeout: 60000
    body: []
  anything-with-put:
    url: http://httpbin/anything
    status: 200
    method: GET
    timeout: 60000
    request-body: "request-body"
    body: ["request-body"]
matching:
  has_substr:
    content: some string
    matches:
      match-regexp: some str
  has_2:
    content:
      - 2
    matches:
      contain-element: 2
  has_foo_bar_and_baz:
    content:
      foo: bar
      baz: bing
    matches:
      and:
        - have-key: baz
  semver:
    content:
      - 1.0.1
      - 1.9.9
    matches:
      semver-constraint: ">1.0.0 <2.0.0 !=1.5.0"
  semver2:
    content:
      - 1.0.1
      - 1.5.0
      - 1.9.9
    matches:
      not:
        semver-constraint: ">1.0.0 <2.0.0 !=1.5.0"
  vars_inline_simple:
    content: {{ .Vars.inline }}
    matches:
      match-regexp: bar
  vars_inline_overwrite:
    content: {{ .Vars.overwrite }}
    matches:
      match-regexp: bar
  sping_basic:
    content: {{ "hello!" | upper | repeat 5 }}
    matches:
      match-regexp: "HELLO!HELLO!HELLO!HELLO!HELLO!"
gossfile:
  "nonexistent-file.yaml":
    skip: true
  bypath:
    file: "goss-dummy.yaml"

goss-0.4.9/integration-tests/goss/goss-wait.yaml000066400000000000000000000002021467505051300217360ustar00rootroot00000000000000---
addr:
  tcp://localhost:80:
    reachable: true
    timeout: 500
  tcp://localhost:8888:
    reachable: true
    timeout: 500
goss-0.4.9/integration-tests/goss/hellogoss.txt000066400000000000000000000000151467505051300216770ustar00rootroot00000000000000Goss Rocks!!
goss-0.4.9/integration-tests/goss/rockylinux9/000077500000000000000000000000001467505051300214435ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/rockylinux9/goss-aa-expected.yaml000066400000000000000000000003401467505051300254550ustar00rootroot00000000000000package:
  httpd:
    installed: true
    versions:
    - 2.4.57-11.el9_4.1
port:
  tcp:80:
    listening: true
    ip:
    - 0.0.0.0
service:
  httpd:
    enabled: true
    running: true
process:
  httpd:
    running: true
goss-0.4.9/integration-tests/goss/rockylinux9/goss-expected-q.yaml000066400000000000000000000041751467505051300253460ustar00rootroot00000000000000file:
  /etc/passwd:
    exists: true
    contents: []
  /tmp/goss/foobar:
    exists: false
    contents: []
package:
  foobar:
    installed: false
  httpd:
    installed: true
  vim-tiny:
    installed: false
addr:
  tcp://httpbin:22:
    reachable: false
    timeout: 1000
  tcp://httpbin:80:
    reachable: true
    timeout: 1000
  udp://8.8.8.8:53:
    reachable: true
    timeout: 1000
port:
  tcp:80:
    listening: true
  tcp:9999:
    listening: false
  tcp6:80:
    listening: false
service:
  foobar:
    enabled: false
    running: false
  httpd:
    enabled: true
    running: true
user:
  apache:
    exists: true
  foobar:
    exists: false
group:
  apache:
    exists: true
  foobar:
    exists: false
command:
  echo 'hi':
    exit-status: 0
    stdout: ""
    stderr: ""
    timeout: 10000
  foobar:
    exit-status: 127
    stdout: ""
    stderr: ""
    timeout: 10000
dns:
  CAA:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  CNAME:c.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  MX:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  NS:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  PTR:54.243.154.1:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  TXT:txt._test.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  ip6.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  localhost:
    resolvable: true
    timeout: 1000
process:
  foobar:
    running: false
  httpd:
    running: true
kernel-param:
  kernel.ostype:
    value: Linux
mount:
  /dev:
    exists: true
    timeout: 1000
http:
  http://google.com:
    status: 301
    allow-insecure: false
    no-follow-redirects: true
    timeout: 5000
    body: []
  https://www.apple.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
    proxy: http://127.0.0.1:8888
  https://www.google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
goss-0.4.9/integration-tests/goss/rockylinux9/goss-expected.yaml000066400000000000000000000057441467505051300251130ustar00rootroot00000000000000file:
  /etc/passwd:
    exists: true
    mode: "0644"
    owner: root
    group: root
    filetype: file
    contents: []
  /tmp/goss/foobar:
    exists: false
    contents: []
package:
  foobar:
    installed: false
  httpd:
    installed: true
    versions:
    - 2.4.57-11.el9_4.1
  vim-tiny:
    installed: false
addr:
  tcp://httpbin:22:
    reachable: false
    timeout: 1000
  tcp://httpbin:80:
    reachable: true
    timeout: 1000
  udp://8.8.8.8:53:
    reachable: true
    timeout: 1000
port:
  tcp:80:
    listening: true
    ip:
    - 0.0.0.0
  tcp:9999:
    listening: false
    ip: []
  tcp6:80:
    listening: false
    ip: []
service:
  foobar:
    enabled: false
    running: false
  httpd:
    enabled: true
    running: true
user:
  apache:
    exists: true
    uid: 48
    gid: 48
    groups:
    - apache
    home: /usr/share/httpd
    shell: /sbin/nologin
  foobar:
    exists: false
group:
  apache:
    exists: true
    gid: 48
  foobar:
    exists: false
command:
  echo 'hi':
    exit-status: 0
    stdout:
    - hi
    stderr: ""
    timeout: 10000
  foobar:
    exit-status: 127
    stdout: ""
    stderr:
    - 'sh: line 1: foobar: command not found'
    timeout: 10000
dns:
  CAA:dnstest.io:
    resolvable: true
    addrs:
    - 0 issue comodoca.com
    - 0 issue letsencrypt.org
    - 0 issuewild ;
    timeout: 1000
    server: 8.8.8.8
  CNAME:c.dnstest.io:
    resolvable: true
    addrs:
    - a.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  MX:dnstest.io:
    resolvable: true
    addrs:
    - 10 b.dnstest.io.
    - 5 a.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  NS:dnstest.io:
    resolvable: true
    addrs:
    - ns1.dnstest.io.
    - ns2.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  PTR:54.243.154.1:
    resolvable: true
    addrs:
    - ec2-54-243-154-1.compute-1.amazonaws.com.
    timeout: 1000
    server: 8.8.8.8
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    addrs:
    - 0 5 443 a.dnstest.io.
    - 10 10 443 b.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  TXT:txt._test.dnstest.io:
    resolvable: true
    addrs:
    - Hello DNS
    timeout: 1000
    server: 8.8.8.8
  ip6.dnstest.io:
    resolvable: true
    addrs:
    - 2404:6800:4001:807::200e
    timeout: 1000
    server: 8.8.8.8
  localhost:
    resolvable: true
    addrs:
    - 127.0.0.1
    - ::1
    timeout: 1000
process:
  foobar:
    running: false
  httpd:
    running: true
kernel-param:
  kernel.ostype:
    value: Linux
mount:
  /dev:
    exists: true
    opts:
    - rw
    - nosuid
    vfs-opts:
    - rw
    source: tmpfs
    filesystem: tmpfs
    timeout: 1000
http:
  http://google.com:
    status: 301
    allow-insecure: false
    no-follow-redirects: true
    timeout: 5000
    body: []
  https://www.apple.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
    proxy: http://127.0.0.1:8888
  https://www.google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
goss-0.4.9/integration-tests/goss/rockylinux9/goss.yaml000066400000000000000000000007371467505051300233110ustar00rootroot00000000000000service:
  autofs:
    enabled: false
    running: false
user:
  apache:
    exists: true
    uid: 48
    gid: 48
    groups:
    - apache
    home: "/usr/share/httpd"
group:
  apache:
    exists: true
    gid: 48
process:
  httpd:
    running: true
port:
  tcp:80:
    listening: true
    ip:
    - '0.0.0.0'
addr:
  tcp://127.0.0.1:80:
    reachable: true
    timeout: 500
    local-address: 127.0.0.1
gossfile:
  "../goss-s*.yaml": {}
  bypath:
    file: "../goss-dummy.yaml"
goss-0.4.9/integration-tests/goss/testdata/000077500000000000000000000000001467505051300207545ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/testdata/static-file.txt000066400000000000000000000000241467505051300237150ustar00rootroot00000000000000nothing to see here
goss-0.4.9/integration-tests/goss/trusty/000077500000000000000000000000001467505051300205155ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/trusty/goss-aa-expected.yaml000066400000000000000000000003461467505051300245350ustar00rootroot00000000000000package:
  apache2:
    installed: true
    versions:
    - 2.4.7-1ubuntu4.22
port:
  tcp:80:
    listening: true
    ip:
    - 0.0.0.0
service:
  apache2:
    enabled: true
    running: true
process:
  apache2:
    running: true
goss-0.4.9/integration-tests/goss/trusty/goss-expected-q.yaml000066400000000000000000000042071467505051300244140ustar00rootroot00000000000000file:
  /etc/passwd:
    exists: true
    contents: []
  /tmp/goss/foobar:
    exists: false
    contents: []
package:
  apache2:
    installed: true
  foobar:
    installed: false
  vim-tiny:
    installed: false
addr:
  tcp://httpbin:22:
    reachable: false
    timeout: 1000
  tcp://httpbin:80:
    reachable: true
    timeout: 1000
  udp://8.8.8.8:53:
    reachable: true
    timeout: 1000
port:
  tcp:80:
    listening: true
  tcp:9999:
    listening: false
  tcp6:80:
    listening: false
service:
  apache2:
    enabled: true
    running: true
  foobar:
    enabled: false
    running: false
user:
  foobar:
    exists: false
  www-data:
    exists: true
group:
  foobar:
    exists: false
  www-data:
    exists: true
command:
  echo 'hi':
    exit-status: 0
    stdout: ""
    stderr: ""
    timeout: 10000
  foobar:
    exit-status: 127
    stdout: ""
    stderr: ""
    timeout: 10000
dns:
  CAA:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  CNAME:c.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  MX:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  NS:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  PTR:54.243.154.1:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  TXT:txt._test.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  ip6.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  localhost:
    resolvable: true
    timeout: 1000
process:
  apache2:
    running: true
  foobar:
    running: false
kernel-param:
  kernel.ostype:
    value: Linux
mount:
  /dev:
    exists: true
    timeout: 1000
http:
  http://google.com:
    status: 301
    allow-insecure: false
    no-follow-redirects: true
    timeout: 5000
    body: []
  https://www.apple.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
    proxy: http://127.0.0.1:8888
  https://www.google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
goss-0.4.9/integration-tests/goss/trusty/goss-expected.yaml000066400000000000000000000057371467505051300241670ustar00rootroot00000000000000file:
  /etc/passwd:
    exists: true
    mode: "0644"
    owner: root
    group: root
    filetype: file
    contents: []
  /tmp/goss/foobar:
    exists: false
    contents: []
package:
  apache2:
    installed: true
    versions:
    - 2.4.7-1ubuntu4.22
  foobar:
    installed: false
  vim-tiny:
    installed: false
addr:
  tcp://httpbin:22:
    reachable: false
    timeout: 1000
  tcp://httpbin:80:
    reachable: true
    timeout: 1000
  udp://8.8.8.8:53:
    reachable: true
    timeout: 1000
port:
  tcp:80:
    listening: true
    ip:
    - 0.0.0.0
  tcp:9999:
    listening: false
    ip: []
  tcp6:80:
    listening: false
    ip: []
service:
  apache2:
    enabled: true
    running: true
  foobar:
    enabled: false
    running: false
user:
  foobar:
    exists: false
  www-data:
    exists: true
    uid: 33
    gid: 33
    groups:
    - www-data
    home: /var/www
    shell: /usr/sbin/nologin
group:
  foobar:
    exists: false
  www-data:
    exists: true
    gid: 33
command:
  echo 'hi':
    exit-status: 0
    stdout:
    - hi
    stderr: ""
    timeout: 10000
  foobar:
    exit-status: 127
    stdout: ""
    stderr:
    - 'sh: 1: foobar: not found'
    timeout: 10000
dns:
  CAA:dnstest.io:
    resolvable: true
    addrs:
    - 0 issue comodoca.com
    - 0 issue letsencrypt.org
    - 0 issuewild ;
    timeout: 1000
    server: 8.8.8.8
  CNAME:c.dnstest.io:
    resolvable: true
    addrs:
    - a.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  MX:dnstest.io:
    resolvable: true
    addrs:
    - 10 b.dnstest.io.
    - 5 a.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  NS:dnstest.io:
    resolvable: true
    addrs:
    - ns1.dnstest.io.
    - ns2.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  PTR:54.243.154.1:
    resolvable: true
    addrs:
    - ec2-54-243-154-1.compute-1.amazonaws.com.
    timeout: 1000
    server: 8.8.8.8
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    addrs:
    - 0 5 443 a.dnstest.io.
    - 10 10 443 b.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  TXT:txt._test.dnstest.io:
    resolvable: true
    addrs:
    - Hello DNS
    timeout: 1000
    server: 8.8.8.8
  ip6.dnstest.io:
    resolvable: true
    addrs:
    - 2404:6800:4001:807::200e
    timeout: 1000
    server: 8.8.8.8
  localhost:
    resolvable: true
    addrs:
    - 127.0.0.1
    - ::1
    timeout: 1000
process:
  apache2:
    running: true
  foobar:
    running: false
kernel-param:
  kernel.ostype:
    value: Linux
mount:
  /dev:
    exists: true
    opts:
    - rw
    - nosuid
    vfs-opts:
    - rw
    source: tmpfs
    filesystem: tmpfs
    timeout: 1000
http:
  http://google.com:
    status: 301
    allow-insecure: false
    no-follow-redirects: true
    timeout: 5000
    body: []
  https://www.apple.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
    proxy: http://127.0.0.1:8888
  https://www.google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
goss-0.4.9/integration-tests/goss/trusty/goss.yaml000066400000000000000000000007431467505051300223600ustar00rootroot00000000000000---
service:
  tinyproxy:
    enabled: true
    running: true
user:
  www-data:
    exists: true
    uid: 33
    gid: 33
    groups:
    - www-data
    home: "/var/www"
group:
  www-data:
    exists: true
    gid: 33
process:
  apache2:
    running: true
port:
  tcp:80:
    listening: true
    ip:
    - 0.0.0.0
addr:
  tcp://127.0.0.1:80:
    reachable: true
    timeout: 500
    local-address: 127.0.0.1
gossfile:
  "../goss-s*.yaml": {}
  bypath:
    file: "../goss-dummy.yaml"

goss-0.4.9/integration-tests/goss/vars.yaml000066400000000000000000000011531467505051300210020ustar00rootroot00000000000000---
alpine3:
  proxy: http://127.0.0.1:8888
  packages:
    apache2: "2.4.59-r0"
  services:
    apache2: [sysinit]
arch:
  packages:
centos7:
  proxy: http://127.0.0.1:8888
  packages:
    httpd: "2.4.6-95.el7.centos"
  services:
    httpd: []
rockylinux9:
  proxy: http://127.0.0.1:8888
  packages:
    httpd: "2.4.57-11.el9_4.1"
  services:
    httpd: []
trusty:
  proxy: http://127.0.0.1:8888
  packages:
    apache2: "2.4.7-1ubuntu4.22"
  services:
    apache2: ["3"]
wheezy:
  proxy: http://127.0.0.1:8888
  packages:
    apache2: "2.2.22-13+deb7u13"
  services:
    apache2: ["2", "3", "5", "4"]

overwrite: foo
goss-0.4.9/integration-tests/goss/wheezy/000077500000000000000000000000001467505051300204565ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/wheezy/goss-aa-expected.yaml000066400000000000000000000003461467505051300244760ustar00rootroot00000000000000package:
  apache2:
    installed: true
    versions:
    - 2.2.22-13+deb7u13
port:
  tcp:80:
    listening: true
    ip:
    - 0.0.0.0
service:
  apache2:
    enabled: true
    running: true
process:
  apache2:
    running: true
goss-0.4.9/integration-tests/goss/wheezy/goss-expected-q.yaml000066400000000000000000000042071467505051300243550ustar00rootroot00000000000000file:
  /etc/passwd:
    exists: true
    contents: []
  /tmp/goss/foobar:
    exists: false
    contents: []
package:
  apache2:
    installed: true
  foobar:
    installed: false
  vim-tiny:
    installed: false
addr:
  tcp://httpbin:22:
    reachable: false
    timeout: 1000
  tcp://httpbin:80:
    reachable: true
    timeout: 1000
  udp://8.8.8.8:53:
    reachable: true
    timeout: 1000
port:
  tcp:80:
    listening: true
  tcp:9999:
    listening: false
  tcp6:80:
    listening: false
service:
  apache2:
    enabled: true
    running: true
  foobar:
    enabled: false
    running: false
user:
  foobar:
    exists: false
  www-data:
    exists: true
group:
  foobar:
    exists: false
  www-data:
    exists: true
command:
  echo 'hi':
    exit-status: 0
    stdout: ""
    stderr: ""
    timeout: 10000
  foobar:
    exit-status: 127
    stdout: ""
    stderr: ""
    timeout: 10000
dns:
  CAA:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  CNAME:c.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  MX:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  NS:dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  PTR:54.243.154.1:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  TXT:txt._test.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  ip6.dnstest.io:
    resolvable: true
    timeout: 1000
    server: 8.8.8.8
  localhost:
    resolvable: true
    timeout: 1000
process:
  apache2:
    running: true
  foobar:
    running: false
kernel-param:
  kernel.ostype:
    value: Linux
mount:
  /dev:
    exists: true
    timeout: 1000
http:
  http://google.com:
    status: 301
    allow-insecure: false
    no-follow-redirects: true
    timeout: 5000
    body: []
  https://www.apple.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
    proxy: http://127.0.0.1:8888
  https://www.google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
goss-0.4.9/integration-tests/goss/wheezy/goss-expected.yaml000066400000000000000000000057251467505051300241250ustar00rootroot00000000000000file:
  /etc/passwd:
    exists: true
    mode: "0644"
    owner: root
    group: root
    filetype: file
    contents: []
  /tmp/goss/foobar:
    exists: false
    contents: []
package:
  apache2:
    installed: true
    versions:
    - 2.2.22-13+deb7u13
  foobar:
    installed: false
  vim-tiny:
    installed: false
addr:
  tcp://httpbin:22:
    reachable: false
    timeout: 1000
  tcp://httpbin:80:
    reachable: true
    timeout: 1000
  udp://8.8.8.8:53:
    reachable: true
    timeout: 1000
port:
  tcp:80:
    listening: true
    ip:
    - 0.0.0.0
  tcp:9999:
    listening: false
    ip: []
  tcp6:80:
    listening: false
    ip: []
service:
  apache2:
    enabled: true
    running: true
  foobar:
    enabled: false
    running: false
user:
  foobar:
    exists: false
  www-data:
    exists: true
    uid: 33
    gid: 33
    groups:
    - www-data
    home: /var/www
    shell: /bin/sh
group:
  foobar:
    exists: false
  www-data:
    exists: true
    gid: 33
command:
  echo 'hi':
    exit-status: 0
    stdout:
    - hi
    stderr: ""
    timeout: 10000
  foobar:
    exit-status: 127
    stdout: ""
    stderr:
    - 'sh: 1: foobar: not found'
    timeout: 10000
dns:
  CAA:dnstest.io:
    resolvable: true
    addrs:
    - 0 issue comodoca.com
    - 0 issue letsencrypt.org
    - 0 issuewild ;
    timeout: 1000
    server: 8.8.8.8
  CNAME:c.dnstest.io:
    resolvable: true
    addrs:
    - a.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  MX:dnstest.io:
    resolvable: true
    addrs:
    - 10 b.dnstest.io.
    - 5 a.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  NS:dnstest.io:
    resolvable: true
    addrs:
    - ns1.dnstest.io.
    - ns2.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  PTR:54.243.154.1:
    resolvable: true
    addrs:
    - ec2-54-243-154-1.compute-1.amazonaws.com.
    timeout: 1000
    server: 8.8.8.8
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    addrs:
    - 0 5 443 a.dnstest.io.
    - 10 10 443 b.dnstest.io.
    timeout: 1000
    server: 8.8.8.8
  TXT:txt._test.dnstest.io:
    resolvable: true
    addrs:
    - Hello DNS
    timeout: 1000
    server: 8.8.8.8
  ip6.dnstest.io:
    resolvable: true
    addrs:
    - 2404:6800:4001:807::200e
    timeout: 1000
    server: 8.8.8.8
  localhost:
    resolvable: true
    addrs:
    - 127.0.0.1
    - ::1
    timeout: 1000
process:
  apache2:
    running: true
  foobar:
    running: false
kernel-param:
  kernel.ostype:
    value: Linux
mount:
  /dev:
    exists: true
    opts:
    - rw
    - nosuid
    vfs-opts:
    - rw
    source: tmpfs
    filesystem: tmpfs
    timeout: 1000
http:
  http://google.com:
    status: 301
    allow-insecure: false
    no-follow-redirects: true
    timeout: 5000
    body: []
  https://www.apple.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
    proxy: http://127.0.0.1:8888
  https://www.google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 5000
    body: []
goss-0.4.9/integration-tests/goss/wheezy/goss.yaml000066400000000000000000000007441467505051300223220ustar00rootroot00000000000000---
service:
  autofs:
    enabled: false
    running: false
user:
  www-data:
    exists: true
    uid: 33
    gid: 33
    groups:
    - www-data
    home: "/var/www"
group:
  www-data:
    exists: true
    gid: 33
process:
  apache2:
    running: true
port:
  tcp:80:
    listening: true
    ip:
    - '0.0.0.0'
addr:
  tcp://127.0.0.1:80:
    reachable: true
    timeout: 500
    local-address: 127.0.0.1
gossfile:
  "../goss-s*.yaml": {}
  bypath:
    file: "../goss-dummy.yaml"

goss-0.4.9/integration-tests/goss/windows/000077500000000000000000000000001467505051300206355ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/windows/commands/000077500000000000000000000000001467505051300224365ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/windows/commands/add.goss.yaml000066400000000000000000000003621467505051300250250ustar00rootroot00000000000000---
# TODO: coverage for the add {test} permutations
command:
  "add addr 127.0.0.1":
    exit-status: 0
    exec: release\goss-windows-amd64 --use-alpha=1 add addr 127.0.0.1
    stdout:
    - "timeout: 500"
    stderr: []
    timeout: 5000

goss-0.4.9/integration-tests/goss/windows/commands/autoadd.goss.yaml000066400000000000000000000004141467505051300257140ustar00rootroot00000000000000---
command:
  "autoadd Administrator":
    exit-status: 0
    exec: release\goss-windows-amd64 --use-alpha=1 autoadd Administrator
    stdout:
      - 'user:'
      - '  name: Administrator'
    stderr: []
    timeout: 5000

    # needs implementation
    skip: true
goss-0.4.9/integration-tests/goss/windows/commands/help.goss.yaml000066400000000000000000000002151467505051300252220ustar00rootroot00000000000000---
command:
  help:
    exit-status: 0
    exec: release\goss-windows-amd64 help
    stdout:
      - alpha
    stderr: []
    timeout: 5000
goss-0.4.9/integration-tests/goss/windows/commands/validate-input.yaml000066400000000000000000000000601467505051300262440ustar00rootroot00000000000000---
file:
  non-existent.txt:
    exists: false
goss-0.4.9/integration-tests/goss/windows/commands/validate.goss.yaml000066400000000000000000000005061467505051300260660ustar00rootroot00000000000000---
# TODO: coverage for the add {test} permutations
command:
  "validate":
    exit-status: 0
    exec: "release\\goss-windows-amd64 --use-alpha=1 -g integration-tests/goss/windows/commands/validate-input.yaml validate"
    stdout:
      - 'Count: 1'
      - 'Failed: 0'
      - 'Skipped: 0'
    stderr: []
    timeout: 5000
goss-0.4.9/integration-tests/goss/windows/tests/000077500000000000000000000000001467505051300217775ustar00rootroot00000000000000goss-0.4.9/integration-tests/goss/windows/tests/addr.goss.yaml000066400000000000000000000002361467505051300245500ustar00rootroot00000000000000---
addr:
  tcp://google.com:443:
    reachable: true
    timeout: 1000

  tcp://127.0.0.1:135:
    reachable: true
    timeout: 1000
    local-address: true
goss-0.4.9/integration-tests/goss/windows/tests/command.goss.yaml000066400000000000000000000016441467505051300252600ustar00rootroot00000000000000---
command:
  hello world:
    exit-status: 0
    exec: "echo hello world"
    stdout:
    - hello world
    stderr: []
    timeout: 10000
  wrap a powershell - expect 0 because travis does not restrict anonymous logins:
    exec: powershell -noprofile -noninteractive -command (get-itemproperty -path 'HKLM:/SYSTEM/CurrentControlSet/Control/Lsa/').restrictanonymous
    exit-status: 0
    stdout:
    - "0"
    stderr: []
    timeout: 10000
  wrap a powershell with quotes - expect 0 because travis does not restrict anonymous logins:
    exec: powershell -noprofile -noninteractive -command "(get-itemproperty -path 'HKLM:/SYSTEM/CurrentControlSet/Control/Lsa/').restrictanonymous"
    exit-status: 0
    stdout:
    - "0"
    stderr: []
    timeout: 10000
  powershell with quotes:
    exec: powershell /c "(echo '{"b":2, "a":1}' | ConvertFrom-json).a"
    exit-status: 0
    stdout:
    - "1"
    stderr: []
    timeout: 10000
goss-0.4.9/integration-tests/goss/windows/tests/dns.goss.yaml000066400000000000000000000001431467505051300244170ustar00rootroot00000000000000---
dns:
  localhost:
    resolvable: true
    addrs:
    - "127.0.0.1"
    - ::1
    timeout: 500
goss-0.4.9/integration-tests/goss/windows/tests/file.goss.yaml000066400000000000000000000006641467505051300245620ustar00rootroot00000000000000---
file:
  integration-tests\goss\testdata\static-file.txt:
    exists: true
    # mode: "0000"  # not applicable on Windows
    # user: ""  # not applicable on Windows
    # group: ""  # not applicable on Windows
    size: 21
    filetype: file
    md5: dc9a07ca9789f866d21d544fe5651954
    sha256: aa8b1b4a0d9bf174f5019c8f8a9568858ee2bdf8e0ad16aec54417d49b48df49
    contents:
      - "nothing to see here"
      - "/nothing.*here/"
goss-0.4.9/integration-tests/goss/windows/tests/gossfile.goss.yaml000066400000000000000000000007511467505051300254530ustar00rootroot00000000000000---
# paths are relative to the goss file that includes the gossfile directive.
gossfile:
  addr.goss.yaml: {}
  command.goss.yaml: {}
  dns.goss.yaml: {}
  file.goss.yaml: {}
  # don't use gossfile; avoid self-referencing
  # gossfile.goss.yaml: {}
  group.goss.yaml: {}
  http.goss.yaml: {}
  interface.goss.yaml: {}
  # kernel-param.na-goss.yaml: {}
  mount.goss.yaml: {}
  package.goss.yaml: {}
  port.goss.yaml: {}
  process.goss.yaml: {}
  service.goss.yaml: {}
  user.goss.yaml: {}
goss-0.4.9/integration-tests/goss/windows/tests/group.goss.yaml000066400000000000000000000002021467505051300247630ustar00rootroot00000000000000---
group:
  'Local Users':
    exists: true
    gid: 0  # not applicable on Windows
    skip: true  # TODO: implement on Windows
goss-0.4.9/integration-tests/goss/windows/tests/http.goss.yaml000066400000000000000000000004371467505051300246200ustar00rootroot00000000000000---
http:
  https://google.com:
    status: 200
    allow-insecure: false
    no-follow-redirects: false
    timeout: 10000
    request-headers:
      - "Content-Type: text/html"
    headers:
      - "Content-Type: text/html"
    body:
      - "google"
    username: ""
    password: ""
goss-0.4.9/integration-tests/goss/windows/tests/interface.goss.yaml000066400000000000000000000007241467505051300256000ustar00rootroot00000000000000---
interface:
  'Loopback Pseudo-Interface 1':
    exists: false
    addrs:
      - '127.0.0.1'
    mtu: 1500
    skip: true

# https://docs.microsoft.com/en-us/powershell/module/nettcpip/get-netipinterface?view=win10-ps
# Get-NetIPInterface
# https://docs.microsoft.com/en-us/powershell/module/netadapter/get-netadapter?view=win10-ps
# Get-NetAdapter - and would then need to choose one with a name that will work in CI, and skip this test when running locally, etc
goss-0.4.9/integration-tests/goss/windows/tests/kernel-param.na-goss.yaml000066400000000000000000000001531467505051300266060ustar00rootroot00000000000000---
# Not applicable on Windows
kernel-param:
  notapplicable.on-windows:
    value: foobar
    skip: true
goss-0.4.9/integration-tests/goss/windows/tests/mount.goss.yaml000066400000000000000000000003221467505051300247740ustar00rootroot00000000000000---
mount:
  'c:':
    exists: true
    filesystem: ntfs

    opts: []  #  not applicable on Windows
    source: ''  #  not applicable on Windows
    usage:
      lt: 95

    skip: true  # needs implementation
goss-0.4.9/integration-tests/goss/windows/tests/package.goss.yaml000066400000000000000000000005461467505051300252350ustar00rootroot00000000000000---
package:
  golang:
    # required attributes
    installed: true
    # optional attributes
    versions:
    - 1.14.1

    # needs implementation
    # needs discussion + design
    # support question for:
    # * chocolatey https://chocolatey.org
    # * scoop https://scoop.sh/
    # * winget-cli https://github.com/microsoft/winget-cli
    skip: true
goss-0.4.9/integration-tests/goss/windows/tests/port.goss.yaml000066400000000000000000000001521467505051300246170ustar00rootroot00000000000000---
port:
  tcp:135:
    listening: true
    ip:
    - 0.0.0.0

    # needs implementation
    skip: true
goss-0.4.9/integration-tests/goss/windows/tests/process.goss.yaml000066400000000000000000000002041467505051300253070ustar00rootroot00000000000000---
process:
  'wininit.exe':
    running: true

  # note - must use .exe suffix on Windows currently
  wininit:
    running: false
goss-0.4.9/integration-tests/goss/windows/tests/service.goss.yaml000066400000000000000000000001451467505051300252750ustar00rootroot00000000000000---
service:
  MSDTC:
    enabled: true
    running: true

    # needs implementation
    skip: true
goss-0.4.9/integration-tests/goss/windows/tests/user.goss.yaml000066400000000000000000000003731467505051300246160ustar00rootroot00000000000000---
user:
  Administrator:
    exists: true
    uid: 65534  # not applicable on Windows
    gid: 65534  # not applicable on Windows
    groups:
    - nfsnobody
    home: /var/lib/nfs
    shell: /sbin/nologin

    # needs implementation
    skip: true
goss-0.4.9/integration-tests/run-serve-tests.sh000077500000000000000000000077511467505051300216270ustar00rootroot00000000000000#!/usr/bin/env bash
# shellcheck source=../ci/lib/setup.sh
source "$(dirname "${BASH_SOURCE[0]}")/../ci/lib/setup.sh" || exit 67

platform_spec="${1:?Must supply name of release binary to build e.g. goss-linux-amd64}"

# Split platform_spec into platform/arch segments
IFS='- ' read -r -a segments <<< "${platform_spec}"

os="${segments[0]}"
arch="${segments[1]}"

find_open_port() {
  local startAt="${1:?"Supply start of port range"}"
  local endAt="${2:?"Supply end of port range"}"
  local how_many="${3:-"1"}"

  if [[ "$(go env GOOS)" == "windows" ]]; then
    # ss (see unix implementation below) doesn't exist on Windows, so fall back on just choosing a random number inside the range (since netstat is _slow_).
    # Thanks also to https://blog.netspi.com/15-ways-to-bypass-the-powershell-execution-policy/
    powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "integration-tests/Find-AvailablePort.ps1 -startAt ${startAt} -endAt ${endAt}"
  elif [[ "$(go env GOOS)" == "darwin" ]]; then
    jot -n -r 1 1025 65535
  else
    # Thanks to https://unix.stackexchange.com/questions/55913/whats-the-easiest-way-to-find-an-unused-local-port
    comm -23 \
      <(seq "${startAt}" "${endAt}" | sort) \
      <(ss -tan | tail -n +2 | awk '{print $4}' | cut -d':' -f2 | sort -u) |
      shuf -n "${how_many}" ||
      shuf -i "${startAt}-${endAt}" -n "${how_many}"
  fi
}

cleanup() {
  binary_name="$(basename "${GOSS_BINARY}")"
  log_info "Killing goss serve process to clean up, exit code for tests was ${?}..."
  if [[ "${os}" == "darwin" ]]; then
    killall "${binary_name}"
  elif [[ "${os}" == "linux" ]]; then
    killall "${binary_name}"
  elif [[ "${os}" == "windows" ]]; then
    # Can't use killall, doesn't exist on Windows. Also would interfere with concurrent runs.
    ps -W |
      awk "/${binary_name}/,NF=1" |
      xargs kill
  fi
  exit "${ret:-0}"
}
trap cleanup EXIT

repo_root="$(git rev-parse --show-toplevel)"
export GOSS_BINARY="${repo_root}/release/goss-${platform_spec}"
log_info "Using: '${GOSS_BINARY}', cwd: '$(pwd)'"

export GOSS_USE_ALPHA=1
open_port="$(find_open_port 1025 65335)"
echo "${open_port}"
args=(
  "-g=${repo_root}/integration-tests/goss/goss-serve.yaml"
  "serve"
  "--listen-addr=127.0.0.1:${open_port}"
)
log_action "\nTesting \`${GOSS_BINARY} ${args[*]}\` ...\n"
"${GOSS_BINARY}" "${args[@]}" &
base_url="http://127.0.0.1:${open_port}"
[[ "$(go env GOOS)" == "darwin" ]] && sleep 2

assert_response_contains() {
  local url="${1:?"1st arg: url"}"
  local test_name="${2:?"2nd arg: test name"}"
  local expectation="${3:?"3rd arg: response body match"}"
  local accept_header="${4:-""}"

  curl_args=("--silent")
  [[ -n "${accept_header:-}" ]] && curl_args+=("-H" "Accept: ${accept_header}")
  curl_args+=("${url}")
  log_info "curl ${curl_args[*]}"
  curl="curl"
  [[ "$(go env GOOS)" == "windows" ]] && curl="curl.exe"
  response="$(${curl} "${curl_args[@]}")"
  if grep --quiet "${expectation}" <<<"${response}"; then
    log_success "Passed: ${test_name}"
    return 0
  fi
  log_error "Failed: ${test_name}"
  log_error "  Expected: ${expectation}"
  log_error "  Response: ${response}"
  return 1
}
failure="false"
on_test_failure() {
  failure="true"
}

# /healthz endpoint
assert_response_contains "${base_url}/healthz" "no accept header" "Count: 2, Failed: 0, Skipped: 0" "" || on_test_failure
assert_response_contains "${base_url}/healthz" "tap accept header" "Count: 2, Failed: 0, Skipped: 0" "application/vnd.goss-documentation" || on_test_failure
assert_response_contains "${base_url}/healthz" "json accept header" "\"failed-count\":0" "application/json" || on_test_failure
assert_response_contains "${base_url}/healthz" "prometheus accept header" "goss_tests_outcomes_total" "application/vnd.goss-prometheus" || on_test_failure

# /metrics - specific prometheus metrics endpoint
assert_response_contains "${base_url}/metrics" "prometheus accept header" "goss_tests_outcomes_total" "" || on_test_failure

[[ "${failure}" == "true" ]] && log_fatal "Test(s) failed, check output above."
goss-0.4.9/integration-tests/run-tests-alpha.sh000077500000000000000000000016711467505051300215630ustar00rootroot00000000000000#!/usr/bin/env bash
# shellcheck source=../ci/lib/setup.sh
source "$(dirname "${BASH_SOURCE[0]}")/../ci/lib/setup.sh" || exit 67

platform_spec="${1:?"Must supply name of release binary to build e.g. goss-linux-amd64"}"
# Split platform_spec into platform/arch segments
IFS='- ' read -r -a segments <<< "${platform_spec}"

os="${segments[0]}"
arch="${segments[1]}"
if [[ "${segments[0]}" == "alpha" ]]; then
  os="${segments[1]}"
  arch="${segments[2]}"
fi

repo_root="$(git rev-parse --show-toplevel)"
export GOSS_BINARY="${repo_root}/release/goss-${platform_spec}"
log_info "Using: '${GOSS_BINARY}', cwd: '$(pwd)', os: ${os}"
readarray -t goss_test_files < <(find integration-tests -type f -name "*.goss.yaml" | grep "${os}" | sort | uniq)

export GOSS_USE_ALPHA=1
for file in "${goss_test_files[@]}"; do
  args=(
    "-g=${file}"
    "validate"
  )
  log_action -e "\nTesting \`${GOSS_BINARY} ${args[*]}\` ...\n"
  "${GOSS_BINARY}" "${args[@]}"
done
goss-0.4.9/integration-tests/run-validate-tests.sh000077500000000000000000000024121467505051300222610ustar00rootroot00000000000000#!/usr/bin/env bash
# shellcheck source=../ci/lib/setup.sh
source "$(dirname "${BASH_SOURCE[0]}")/../ci/lib/setup.sh" || exit 67

platform_spec="${1:?"Must supply name of release binary to build e.g. goss-linux-amd64"}"
# Split platform_spec into platform/arch segments
IFS='- ' read -r -a segments <<< "${platform_spec}"

os="${segments[0]}"
arch="${segments[1]}"

if [[ "${os}" == "linux" ]]; then
  echo "OS is ${os}. This script is not for running tests on the different flavours of linux."
  echo "Linux is exercised via the integration-tests/test.sh currently, because linux can be"
  echo "verified via docker containers; macOS and Windows cannot."
  echo "This script is for macOS and Windows, and runs tests that are expected to pass on"
  echo "Travis-CI provided images, running nakedly (no containerisation) on the hosts there."
  exit 1
fi

repo_root="$(git rev-parse --show-toplevel)"
export GOSS_BINARY="${repo_root}/release/goss-${platform_spec}"
log_info "Using: '${GOSS_BINARY}', cwd: '$(pwd)', os: ${os}"

export GOSS_USE_ALPHA=1
for file in `find integration-tests -type f -name "*.goss.yaml" | grep "${os}" | sort | uniq`; do
  args=(
    "-g=${file}"
    "validate"
  )
  log_action "\nTesting \`${GOSS_BINARY} ${args[*]}\` ...\n"
  "${GOSS_BINARY}" "${args[@]}"
done
goss-0.4.9/integration-tests/test.sh000077500000000000000000000062551467505051300175160ustar00rootroot00000000000000#!/usr/bin/env bash
# shellcheck source=../ci/lib/setup.sh
source "$(dirname "${BASH_SOURCE[0]}")/../ci/lib/setup.sh" || exit 67
# preserve current behaviour
set -x

os="${1:?"Need OS as 1st arg. e.g. alpine arch centos7 rockylinux9 trusty wheezy"}"
arch="${2:?"Need arch as 2nd arg. e.g. amd64 386"}"

vars_inline="{inline: bar, overwrite: bar}"
container_repository="aelsabbahy"

# setup places us inside repo-root; this preserves current behaviour with least change.
cd integration-tests

cp "../release/goss-linux-$arch" "goss/$os/"
# Run build if Dockerfile has changed but hasn't been pushed to dockerhub
if ! md5sum -c "Dockerfile_${os}.md5"; then
  docker build -t "$container_repository/goss_${os}:latest" - < "Dockerfile_$os"
# Pull if image doesn't exist locally
elif ! docker images | grep "$container_repository/goss_$os";then
  docker pull "$container_repository/goss_$os"
fi

container_name="goss_int_test_${os}_${arch}"
docker_exec() {
  docker exec "$container_name" "$@"
}

# Cleanup any old containers
if docker ps -a | grep "$container_name";then
  docker rm -vf "$container_name"
fi

# Setup local httbin
# FIXME: this is a quick hack to fix intermittent CI issues
network=goss-test
docker network create --driver bridge  --subnet '172.19.0.0/16' $network
docker run -d --name httpbin --network $network kennethreitz/httpbin
opts=(--env OS=$os --cap-add SYS_ADMIN -v "$PWD/goss:/goss" -d --name "$container_name" --security-opt seccomp:unconfined --security-opt label:disable --privileged)
id=$(docker run "${opts[@]}" --network $network "$container_repository/goss_$os" /sbin/init)
ip=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "$id")
trap "rv=\$?; docker rm -vf $id;docker rm -vf httpbin;docker network rm $network; exit \$rv" INT TERM EXIT
# Give httpd time to start up, adding 1 second to see if it helps with intermittent CI failures
[[ $os != "arch" ]] && docker_exec "/goss/$os/goss-linux-$arch" -g "/goss/goss-wait.yaml" validate -r 10s -s 100ms && sleep 1

#out=$(docker exec "$container_name" bash -c "time /goss/$os/goss-linux-$arch -g /goss/$os/goss.yaml validate")
out=$(docker_exec "/goss/$os/goss-linux-$arch" --vars "/goss/vars.yaml" --vars-inline "$vars_inline" -g "/goss/$os/goss.yaml" validate)
echo "$out"

if [[ $os == "arch" ]]; then
    egrep -q 'Count: 104, Failed: 0, Skipped: 3' <<<"$out"
else
    egrep -q 'Count: 125, Failed: 0, Skipped: 5' <<<"$out"
fi

if [[ ! $os == "arch" ]]; then
  docker_exec /goss/generate_goss.sh "$os" "$arch"

  # docker exec $container_name bash -c "cp /goss/${os}/goss-generated-$arch.yaml /goss/${os}/goss-expected.yaml"
  docker_exec diff -wu "/goss/${os}/goss-expected.yaml" "/goss/${os}/goss-generated-$arch.yaml"

  # docker exec $container_name bash -c "cp /goss/${os}/goss-aa-generated-$arch.yaml /goss/${os}/goss-aa-expected.yaml"
  docker_exec diff -wu "/goss/${os}/goss-aa-expected.yaml" "/goss/${os}/goss-aa-generated-$arch.yaml"

  docker_exec /goss/generate_goss.sh "$os" "$arch" -q

  # docker exec $container_name bash -c "cp /goss/${os}/goss-generated-$arch.yaml /goss/${os}/goss-expected-q.yaml"
  docker_exec diff -wu "/goss/${os}/goss-expected-q.yaml" "/goss/${os}/goss-generated-$arch.yaml"
fi

#docker rm -vf goss_int_test_$os
goss-0.4.9/logs.go000066400000000000000000000016661467505051300140110ustar00rootroot00000000000000package goss

import (
	"fmt"
	"io"
	"log"
	"os"
	"strings"
	"time"

	"github.com/goss-org/goss/util"
	"github.com/hashicorp/logutils"
)

func setLogLevel(c *util.Config) error {
	filter := &logutils.LevelFilter{
		Levels:   []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARN", "ERROR"},
		MinLevel: logutils.LogLevel("INFO"),
		Writer:   os.Stderr,
	}
	log.SetFlags(0) // Turn off standard timestamp flags
	log.SetOutput(×tampedWriter{filter})
	for _, lvl := range filter.Levels {
		cLvl := strings.ToUpper(c.LogLevel)
		if string(lvl) == cLvl {
			filter.MinLevel = lvl
			log.Printf("[DEBUG] Setting log level to %v", cLvl)
			return nil
		}
	}
	return fmt.Errorf("Unsupported log level: %s", c.LogLevel)
}

type timestampedWriter struct {
	wrappedWriter io.Writer
}

func (t *timestampedWriter) Write(b []byte) (int, error) {
	timestamp := time.Now().UTC().Format(time.RFC3339)
	return fmt.Fprintf(t.wrappedWriter, "%s %s", timestamp, b)
}
goss-0.4.9/matcher_test.go000066400000000000000000000036441467505051300155250ustar00rootroot00000000000000//go:build linux

package goss

import (
	"bytes"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"testing"

	"github.com/goss-org/goss/util"
	"github.com/stretchr/testify/assert"
)

var (
	// This will generate the "golden files" prior to running the tests.
	// helpful when the output is changed and a user doesn't want to update every single expectation file by hand
	update = flag.Bool("update", false, "update the golden files of this test")
)

func TestMain(m *testing.M) {
	flag.Parse()
	os.Exit(m.Run())
}

func TestMatchers(t *testing.T) {
	files, err := filepath.Glob(filepath.Join("testdata", "out_matching_*"))
	if err != nil {
		t.Fatal(err)
	}

	for _, outFile := range files {
		outFile := outFile
		parts := strings.Split(outFile, ".")
		specName := fmt.Sprintf("%s.yaml", strings.TrimPrefix(parts[0], "testdata/out_"))
		specFile := filepath.Join("testdata", specName)
		outFormat := parts[2]
		wantCode, err := strconv.Atoi(parts[1])
		if err != nil {
			t.Fatal(err)
		}
		tn := outFile
		t.Run(tn, func(t *testing.T) {
			output := &bytes.Buffer{}

			cfg, err := util.NewConfig(
				util.WithOutputFormat(outFormat),
				util.WithResultWriter(output),
				util.WithSpecFile(specFile),
				util.WithFormatOptions("sort", "pretty"),
			)
			if err != nil {
				t.Fatal(err)
			}
			exitCode, err := Validate(cfg)
			if err != nil {
				t.Fatal(err)
			}
			actualOut := output.String()
			actualOut = sanitizeOutput(actualOut)

			if *update {
				os.WriteFile(outFile, []byte(actualOut), 0644)
			}
			wantOutB, err := os.ReadFile(outFile)
			if err != nil {
				t.Fatal(err)
			}
			wantOut := string(wantOutB)
			if actualOut != wantOut {
				assert.Equal(t, wantOut, actualOut)
			}
			if exitCode != wantCode {
				assert.Equal(t, wantCode, exitCode)
			}
		})
	}
}

func sanitizeOutput(s string) string {
	// Remove duration time
	re := regexp.MustCompile(`\d\.\d\d\ds`)
	return re.ReplaceAllString(s, "")
}
goss-0.4.9/matchers/000077500000000000000000000000001467505051300143135ustar00rootroot00000000000000goss-0.4.9/matchers/and.go000066400000000000000000000020511467505051300154020ustar00rootroot00000000000000package matchers

import (
	"encoding/json"
)

type AndMatcher struct {
	fakeOmegaMatcher
	Matchers []GossMatcher

	// state
	firstFailedMatcher GossMatcher
}

func And(ms ...GossMatcher) GossMatcher {
	return &AndMatcher{Matchers: ms}
}

func (m *AndMatcher) Match(actual interface{}) (success bool, err error) {
	m.firstFailedMatcher = nil
	for _, matcher := range m.Matchers {
		success, err := matcher.Match(actual)
		if !success || err != nil {
			m.firstFailedMatcher = matcher
			return false, err
		}
	}
	return true, nil
}

func (m *AndMatcher) FailureResult(actual interface{}) MatcherResult {
	return m.firstFailedMatcher.FailureResult(actual)
}

func (m *AndMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to satisfy all of these matchers",
		Expected: m.Matchers,
	}
}

func (m *AndMatcher) MarshalJSON() ([]byte, error) {
	if len(m.Matchers) == 1 {
		return json.Marshal(m.Matchers[0])
	}
	j := make(map[string]interface{})
	j["and"] = m.Matchers
	return json.Marshal(j)
}
goss-0.4.9/matchers/be_numerically_matcher.go000066400000000000000000000027611467505051300213450ustar00rootroot00000000000000package matchers

import (
	"encoding/json"
	"fmt"

	"github.com/onsi/gomega/matchers"
)

type BeNumericallyMatcher struct {
	fakeOmegaMatcher
	Comparator string
	CompareTo  []interface{}
}

func BeNumerically(comparator string, compareTo ...interface{}) GossMatcher {
	return &BeNumericallyMatcher{
		Comparator: comparator,
		CompareTo:  compareTo,
	}
}
func (m *BeNumericallyMatcher) Match(actual interface{}) (success bool, err error) {
	comparator, err := strToSymbol(m.Comparator)
	if err != nil {
		return false, err
	}
	matcher := &matchers.BeNumericallyMatcher{
		Comparator: comparator,
		CompareTo:  m.CompareTo,
	}
	return matcher.Match(actual)
}

func (m *BeNumericallyMatcher) FailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  fmt.Sprintf("to be numerically %s", m.Comparator),
		Expected: m.CompareTo[0],
	}
}

func (m *BeNumericallyMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  fmt.Sprintf("not to be numerically %s", m.Comparator),
		Expected: m.CompareTo[0],
	}
}

func (m *BeNumericallyMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j[m.Comparator] = m.CompareTo[0]
	return json.Marshal(j)
}

func strToSymbol(s string) (string, error) {
	comparator, ok := map[string]string{
		"gt": ">",
		"ge": ">=",
		"lt": "<",
		"le": "<=",
		"eq": "==",
	}[s]
	if !ok {
		return "", fmt.Errorf("Unknown comparator: %s", s)
	}
	return comparator, nil
}
goss-0.4.9/matchers/consist_of.go000066400000000000000000000022671467505051300170170ustar00rootroot00000000000000package matchers

import (
	"encoding/json"

	"github.com/onsi/gomega/matchers"
	"github.com/samber/lo"
)

type ConsistOfMatcher struct {
	matchers.ConsistOfMatcher
}

func ConsistOf(elements ...interface{}) GossMatcher {
	return &ConsistOfMatcher{
		matchers.ConsistOfMatcher{
			Elements: elements,
		},
	}
}

func (m *ConsistOfMatcher) FailureResult(actual interface{}) MatcherResult {
	missingElements := getUnexported(m, "missingElements")
	extraElements := getUnexported(m, "extraElements")
	missingEl, ok := missingElements.([]interface{})
	var foundElements any
	if ok {
		foundElements, _ = lo.Difference(m.Elements, missingEl)
	}
	return MatcherResult{
		Actual:          actual,
		Message:         "to consist of",
		Expected:        m.Elements,
		MissingElements: missingElements,
		ExtraElements:   extraElements,
		FoundElements:   foundElements,
	}
}

func (m *ConsistOfMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to consist of",
		Expected: m.Elements,
	}
}

func (m *ConsistOfMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["consist-of"] = m.Elements
	return json.Marshal(j)
}
goss-0.4.9/matchers/contain_element_matcher.go000066400000000000000000000015471467505051300215200ustar00rootroot00000000000000package matchers

import (
	"encoding/json"

	"github.com/onsi/gomega/matchers"
)

type ContainElementMatcher struct {
	matchers.ContainElementMatcher
}

func ContainElement(element interface{}) GossMatcher {
	return &ContainElementMatcher{
		matchers.ContainElementMatcher{
			Element: element,
		},
	}
}

func (m *ContainElementMatcher) FailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "to contain element matching",
		Expected: m.Element,
	}
}

func (m *ContainElementMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to contain element matching",
		Expected: m.Element,
	}
}

func (m *ContainElementMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["contain-element"] = m.Element
	return json.Marshal(j)
}
goss-0.4.9/matchers/contain_elements_matcher.go000066400000000000000000000022651467505051300217010ustar00rootroot00000000000000package matchers

import (
	"encoding/json"

	"github.com/onsi/gomega/matchers"
	"github.com/samber/lo"
)

type ContainElementsMatcher struct {
	matchers.ContainElementsMatcher
}

func ContainElements(elements ...interface{}) GossMatcher {
	return &ContainElementsMatcher{
		matchers.ContainElementsMatcher{
			Elements: elements,
		},
	}
}
func (m *ContainElementsMatcher) FailureResult(actual interface{}) MatcherResult {
	missingElements := getUnexported(m, "missingElements")
	missingEl, ok := missingElements.([]interface{})
	var foundElements any
	if ok {
		foundElements, _ = lo.Difference(m.Elements, missingEl)
	}
	return MatcherResult{
		Actual:          actual,
		Message:         "to contain elements matching",
		Expected:        m.Elements,
		MissingElements: missingElements,
		FoundElements:   foundElements,
	}

}
func (m *ContainElementsMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to contain elements matching",
		Expected: m.Elements,
	}

}

func (m *ContainElementsMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["contain-elements"] = m.Elements
	return json.Marshal(j)
}
goss-0.4.9/matchers/contain_substring_matcher.go000066400000000000000000000016061467505051300221030ustar00rootroot00000000000000package matchers

import (
	"encoding/json"

	"github.com/onsi/gomega/matchers"
)

type ContainSubstringMatcher struct {
	matchers.ContainSubstringMatcher
}

func ContainSubstring(substr string, args ...interface{}) GossMatcher {
	return &ContainSubstringMatcher{
		matchers.ContainSubstringMatcher{
			Substr: substr,
			Args:   args,
		},
	}
}

func (m *ContainSubstringMatcher) FailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "to contain substring",
		Expected: m.Substr,
	}
}

func (m *ContainSubstringMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to contain substring",
		Expected: m.Substr,
	}
}

func (m *ContainSubstringMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["contain-substring"] = m.Substr
	return json.Marshal(j)
}
goss-0.4.9/matchers/equal_matcher.go000066400000000000000000000013001467505051300174460ustar00rootroot00000000000000package matchers

import (
	"encoding/json"

	"github.com/onsi/gomega/matchers"
)

type EqualMatcher struct {
	matchers.EqualMatcher
}

func Equal(element interface{}) GossMatcher {
	return &EqualMatcher{
		matchers.EqualMatcher{
			Expected: element,
		},
	}
}

func (m *EqualMatcher) FailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "to equal",
		Expected: m.Expected,
	}
}

func (m *EqualMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to equal",
		Expected: m.Expected,
	}
}

func (m *EqualMatcher) MarshalJSON() ([]byte, error) {
	return json.Marshal(m.Expected)
}
goss-0.4.9/matchers/have_key_matcher.go000066400000000000000000000014021467505051300201350ustar00rootroot00000000000000package matchers

import (
	"encoding/json"

	"github.com/onsi/gomega/matchers"
)

type HaveKeyMatcher struct {
	matchers.HaveKeyMatcher
}

func HaveKey(key interface{}) GossMatcher {
	return &HaveKeyMatcher{
		matchers.HaveKeyMatcher{
			Key: key,
		},
	}
}

func (m *HaveKeyMatcher) FailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "to have key matching",
		Expected: m.Key,
	}
}

func (m *HaveKeyMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to have key matching",
		Expected: m.Key,
	}
}

func (m *HaveKeyMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["have-key"] = m.Key
	return json.Marshal(j)
}
goss-0.4.9/matchers/have_len_matcher.go000066400000000000000000000013721467505051300201310ustar00rootroot00000000000000package matchers

import (
	"encoding/json"

	"github.com/onsi/gomega/matchers"
)

type HaveLenMatcher struct {
	matchers.HaveLenMatcher
}

func HaveLen(count int) GossMatcher {
	return &HaveLenMatcher{
		matchers.HaveLenMatcher{
			Count: count,
		},
	}
}

func (m *HaveLenMatcher) FailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "to have length",
		Expected: m.Count,
	}
}

func (m *HaveLenMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to have length",
		Expected: m.Count,
	}
}

func (m *HaveLenMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["have-len"] = m.Count
	return json.Marshal(j)
}
goss-0.4.9/matchers/have_patterns.go000066400000000000000000000130411467505051300175040ustar00rootroot00000000000000package matchers

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"

	"github.com/onsi/gomega/format"
)

const (
	maxScanTokenSize = 1024 * 1024
)

type HavePatternsMatcher struct {
	fakeOmegaMatcher

	Elements        interface{}
	missingElements []string
	foundElements   []string
}

func HavePatterns(elements interface{}) GossMatcher {
	return &HavePatternsMatcher{
		Elements: elements,
	}
}

func (m *HavePatternsMatcher) Match(actual interface{}) (success bool, err error) {
	t, ok := m.Elements.([]interface{})
	if !ok {
		return false, fmt.Errorf("HavePatterns matcher expects an array of matchers.  Got:\n%s", format.Object(m.Elements, 1))
	}
	elements := make([]string, len(t))
	for i, v := range t {
		switch v := v.(type) {
		case string:
			elements[i] = v
		default:
			return false, fmt.Errorf("HavePatterns matcher expects patterns to be a string. got: \n%s", format.Object(v, 1))
		}
	}
	notfound, err := sliceToPatterns(elements)
	if err != nil {
		return false, err
	}
	// short circuit
	if len(notfound) == 0 {
		return true, nil
	}
	var fh io.Reader
	switch av := actual.(type) {
	case io.Reader:
		fh = av
	case string:
		fh = strings.NewReader(av)
	case []string:
		fh = strings.NewReader(strings.Join(av, "\n"))
	default:
		err = fmt.Errorf("Incorrect type %T", actual)

	}
	if err != nil {
		return false, err
	}

	defer func() {
		if rc, ok := fh.(io.ReadCloser); ok {
			rc.Close()
		}
	}()

	scanner := bufio.NewScanner(fh)
	scanner.Buffer(nil, maxScanTokenSize)
	var found []patternMatcher
	for scanner.Scan() {
		line := scanner.Text()

		i := 0
		for _, pat := range notfound {
			if pat.Match(line) {
				// Found it, but wasn't supposed to, don't mark it as found, but remove it from search
				if !pat.Inverse() {
					found = append(found, pat)
				}
				continue
			}
			notfound[i] = pat
			i++
		}
		notfound = notfound[:i]
		if len(notfound) == 0 {
			break
		}
	}
	if err := scanner.Err(); err != nil {
		return false, err
	}

	for _, pat := range notfound {
		// Didn't find it, but we didn't want to.. so we mark it as found
		// Empty pattern should match even if input to scanner is empty
		if pat.Inverse() || pat.Pattern() == "" {
			found = append(found, pat)
		}
	}

	foundSlice := patternsToSlice(found)
	m.foundElements = foundSlice
	if len(elements) != len(found) {
		m.missingElements = subtractSlice(elements, foundSlice)
		return false, nil
	}
	return true, nil
}

func (m *HavePatternsMatcher) FailureResult(actual interface{}) MatcherResult {
	var a interface{}
	switch actual.(type) {
	case string, []string:
		a = actual
	default:
		a = fmt.Sprintf("object: %T", actual)
	}
	return MatcherResult{
		Actual:          a,
		Message:         "to have patterns",
		Expected:        m.Elements,
		MissingElements: m.missingElements,
		FoundElements:   m.foundElements,
	}
}

func (m *HavePatternsMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	a, ok := actual.(string)
	if !ok {
		a = fmt.Sprintf("object: %T", actual)
	}
	return MatcherResult{
		Actual:   a,
		Message:  "not to have patterns",
		Expected: m.Elements,
	}
}

type patternMatcher interface {
	Match(string) bool
	Pattern() string
	Inverse() bool
}

type stringPattern struct {
	pattern      string
	cleanPattern string
	inverse      bool
}

func newStringPattern(str string) *stringPattern {
	var inverse bool
	if strings.HasPrefix(str, "!") {
		inverse = true
	}
	cleanPattern := strings.TrimLeft(str, "\\/!")
	return &stringPattern{
		pattern:      str,
		cleanPattern: cleanPattern,
		inverse:      inverse,
	}
}

func (s *stringPattern) Match(str string) bool {
	return strings.Contains(str, s.cleanPattern)
}

func (s *stringPattern) Pattern() string { return s.pattern }
func (s *stringPattern) Inverse() bool   { return s.inverse }

type regexPattern struct {
	pattern string
	re      *regexp.Regexp
	inverse bool
}

func newRegexPattern(str string) (*regexPattern, error) {
	var inverse bool
	cleanStr := str
	if strings.HasPrefix(str, "!") {
		inverse = true
		cleanStr = cleanStr[1:]
	}
	trimLeft := []rune{'\\', '/'}
	for _, r := range trimLeft {
		if rune(cleanStr[0]) == r {
			cleanStr = cleanStr[1:]
			break
		}
	}
	trimRight := []rune{'/'}
	for _, r := range trimRight {
		if rune(cleanStr[len(cleanStr)-1]) == r {
			cleanStr = cleanStr[:len(cleanStr)-1]
			break
		}
	}

	re, err := regexp.Compile(cleanStr)

	return ®exPattern{
		pattern: str,
		re:      re,
		inverse: inverse,
	}, err

}

func (re *regexPattern) Match(str string) bool {
	return re.re.MatchString(str)
}

func (re *regexPattern) Pattern() string { return re.pattern }
func (re *regexPattern) Inverse() bool   { return re.inverse }

func sliceToPatterns(slice []string) ([]patternMatcher, error) {
	var patterns []patternMatcher
	for _, s := range slice {
		if (strings.HasPrefix(s, "/") || strings.HasPrefix(s, "!/")) && strings.HasSuffix(s, "/") {
			pat, err := newRegexPattern(s)
			if err != nil {
				return nil, err
			}
			patterns = append(patterns, pat)
		} else {
			patterns = append(patterns, newStringPattern(s))
		}
	}
	return patterns, nil
}

func patternsToSlice(patterns []patternMatcher) []string {
	var slice []string
	for _, p := range patterns {
		slice = append(slice, p.Pattern())
	}
	return slice
}
func subtractSlice(x, y []string) []string {
	m := make(map[string]bool)

	for _, y := range y {
		m[y] = true
	}

	var ret []string
	for _, x := range x {
		if m[x] {
			continue
		}
		ret = append(ret, x)
	}

	return ret
}

func (matcher *HavePatternsMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["have-patterns"] = matcher.Elements
	return json.Marshal(j)
}
goss-0.4.9/matchers/have_prefix_matcher.go000066400000000000000000000015041467505051300206450ustar00rootroot00000000000000package matchers

import (
	"encoding/json"

	"github.com/onsi/gomega/matchers"
)

type HavePrefixMatcher struct {
	matchers.HavePrefixMatcher
}

func HavePrefix(prefix string, args ...interface{}) GossMatcher {
	return &HavePrefixMatcher{
		matchers.HavePrefixMatcher{
			Prefix: prefix,
			Args:   args,
		},
	}
}

func (m *HavePrefixMatcher) FailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "to have prefix",
		Expected: m.Prefix,
	}
}

func (m *HavePrefixMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to have prefix",
		Expected: m.Prefix,
	}
}

func (m *HavePrefixMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["have-prefix"] = m.Prefix
	return json.Marshal(j)
}
goss-0.4.9/matchers/have_suffix_matcher.go000066400000000000000000000015041467505051300206540ustar00rootroot00000000000000package matchers

import (
	"encoding/json"

	"github.com/onsi/gomega/matchers"
)

type HaveSuffixMatcher struct {
	matchers.HaveSuffixMatcher
}

func HaveSuffix(prefix string, args ...interface{}) GossMatcher {
	return &HaveSuffixMatcher{
		matchers.HaveSuffixMatcher{
			Suffix: prefix,
			Args:   args,
		},
	}
}

func (m *HaveSuffixMatcher) FailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "to have suffix",
		Expected: m.Suffix,
	}
}

func (m *HaveSuffixMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to have suffix",
		Expected: m.Suffix,
	}
}

func (m *HaveSuffixMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["have-suffix"] = m.Suffix
	return json.Marshal(j)
}
goss-0.4.9/matchers/match_regexp_matcher.go000066400000000000000000000015471467505051300210220ustar00rootroot00000000000000package matchers

import (
	"encoding/json"

	"github.com/onsi/gomega/matchers"
)

type MatchRegexpMatcher struct {
	matchers.MatchRegexpMatcher
}

func MatchRegexp(regexp string, args ...interface{}) GossMatcher {
	return &MatchRegexpMatcher{
		matchers.MatchRegexpMatcher{
			Regexp: regexp,
			Args:   args,
		},
	}
}

func (m *MatchRegexpMatcher) FailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "to match regular expression",
		Expected: m.Regexp,
	}
}

func (m *MatchRegexpMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to match regular expression",
		Expected: m.Regexp,
	}
}

func (m *MatchRegexpMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["match-regexp"] = m.Regexp
	return json.Marshal(j)
}
goss-0.4.9/matchers/matchers.go000066400000000000000000000027561467505051300164620ustar00rootroot00000000000000package matchers

import (
	"encoding/json"
	"reflect"
	"unsafe"

	"github.com/onsi/gomega/types"
)

type GossMatcher interface {
	// This is needed due to oMegaMatcher test in some of the GomegaMatcher logic
	types.GomegaMatcher
	//Match(actual interface{}) (success bool, err error)
	FailureResult(actual interface{}) MatcherResult
	NegatedFailureResult(actual interface{}) MatcherResult
	// This doesn't seem to make a difference, maybe not needed
	json.Marshaler
}

type MatcherResult struct {
	Actual             interface{}   `json:"actual"`
	Message            string        `json:"message"`
	Expected           interface{}   `json:"expected"`
	MissingElements    interface{}   `json:"missing-elements"`
	FoundElements      interface{}   `json:"found-elements"`
	ExtraElements      interface{}   `json:"extra-elements"`
	TransformerChain   []Transformer `json:"transform-chain"`
	UntransformedValue interface{}   `json:"untransformed-value"`
}

func getUnexported(i interface{}, field string) interface{} {
	rs := reflect.ValueOf(i).Elem()
	rf := rs.FieldByName(field)
	rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem()
	return rf.Interface()
}

type fakeOmegaMatcher struct{}

// FailureMessage is a stub to honor omegaMatcher interface
func (m *fakeOmegaMatcher) FailureMessage(_ interface{}) (message string) {
	return ""
}

// NegatedFailureMessage is a stub to honor omegaMatcher interface
func (m *fakeOmegaMatcher) NegatedFailureMessage(_ interface{}) (message string) {
	return ""
}
goss-0.4.9/matchers/not.go000066400000000000000000000013521467505051300154430ustar00rootroot00000000000000package matchers

import (
	"encoding/json"
)

type NotMatcher struct {
	fakeOmegaMatcher
	Matcher GossMatcher
}

func Not(matcher GossMatcher) GossMatcher {
	return &NotMatcher{Matcher: matcher}
}

func (m *NotMatcher) Match(actual interface{}) (bool, error) {
	success, err := m.Matcher.Match(actual)
	if err != nil {
		return false, err
	}
	return !success, nil
}

func (m *NotMatcher) FailureResult(actual interface{}) MatcherResult {
	return m.Matcher.NegatedFailureResult(actual)
}

func (m *NotMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	return m.Matcher.FailureResult(actual)
}

func (m *NotMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["not"] = m.Matcher
	return json.Marshal(j)
}
goss-0.4.9/matchers/or.go000066400000000000000000000021511467505051300152610ustar00rootroot00000000000000package matchers

import (
	"encoding/json"
)

type OrMatcher struct {
	fakeOmegaMatcher

	Matchers []GossMatcher

	// state
	firstSuccessfulMatcher GossMatcher
}

func Or(ms ...GossMatcher) GossMatcher {
	return &OrMatcher{Matchers: ms}
}

func (m *OrMatcher) Match(actual interface{}) (success bool, err error) {
	m.firstSuccessfulMatcher = nil
	for _, matcher := range m.Matchers {
		success, err := matcher.Match(actual)
		if err != nil {
			return false, err
		}
		if success {
			m.firstSuccessfulMatcher = matcher
			return true, nil
		}
	}
	return false, nil
}

func (m *OrMatcher) FailureResult(actual interface{}) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "to satisfy at least one of these matchers",
		Expected: m.Matchers,
	}
}

func (m *OrMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	firstSuccessfulMatcher := getUnexported(m, "firstSuccessfulMatcher")
	return firstSuccessfulMatcher.(GossMatcher).NegatedFailureResult(actual)
}

func (m *OrMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]interface{})
	j["or"] = m.Matchers
	return json.Marshal(j)
}
goss-0.4.9/matchers/semver_constraint.go000066400000000000000000000050231467505051300204070ustar00rootroot00000000000000package matchers

import (
	"bytes"
	"encoding/json"
	"fmt"
	"reflect"

	"github.com/blang/semver/v4"
	"github.com/onsi/gomega/format"
)

type BeSemverConstraintMatcher struct {
	fakeOmegaMatcher

	Constraint any
}

func BeSemverConstraint(constraint any) GossMatcher {
	return &BeSemverConstraintMatcher{
		Constraint: constraint,
	}
}
func (m *BeSemverConstraintMatcher) Match(actual any) (success bool, err error) {
	constraint, ok := toConstraint(m.Constraint)
	if !ok {
		return false, fmt.Errorf("Expected a valid semver constraint.  Got:\n%s", format.Object(m.Constraint, 1))
	}

	actualSlice, ok := toVersions(actual)
	if !ok {
		return false, fmt.Errorf("Expected a single or list of semver valid version(s).  Got:\n%s", format.Object(actual, 1))
	}

	for _, v := range actualSlice {
		if !constraint(*v) {
			return false, nil
		}
	}

	return true, nil
}

func (m *BeSemverConstraintMatcher) FailureResult(actual any) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "to satisfy semver constraint",
		Expected: m.Constraint,
	}
}

func (m *BeSemverConstraintMatcher) NegatedFailureResult(actual any) MatcherResult {
	return MatcherResult{
		Actual:   actual,
		Message:  "not to satisfy semver constraint",
		Expected: m.Constraint,
	}
}

func toConstraint(in any) (semver.Range, bool) {
	str, ok := in.(string)
	if !ok {
		return nil, false
	}

	out, err := semver.ParseRange(str)
	return out, err == nil
}

func toVersion(in any) (*semver.Version, bool) {
	str, ok := in.(string)
	if !ok {
		return nil, false
	}

	v, err := semver.Parse(str)
	if err != nil {
		return nil, false
	}

	return &v, true
}

func toVersions(in any) ([]*semver.Version, bool) {
	if v, ok := toVersion(in); ok {
		return []*semver.Version{v}, ok
	}

	if reflect.ValueOf(in).Kind() != reflect.Slice {
		return nil, false
	}

	out := make([]*semver.Version, 0)

	if slice, ok := in.([]any); ok {
		for _, ele := range slice {
			if v, ok := toVersion(ele); ok {
				out = append(out, v)
			} else {
				return nil, false
			}
		}
	} else if slice, ok := in.([]string); ok {
		for _, ele := range slice {
			if v, ok := toVersion(ele); ok {
				out = append(out, v)
			} else {
				return nil, false
			}
		}
	}

	return out, len(out) > 0
}

func (m *BeSemverConstraintMatcher) MarshalJSON() ([]byte, error) {
	j := make(map[string]any)
	j["semver-constraint"] = m.Constraint
	buffer := &bytes.Buffer{}
	encoder := json.NewEncoder(buffer)
	encoder.SetEscapeHTML(false)
	err := encoder.Encode(j)
	if err != nil {
		return nil, nil
	}
	b := buffer.Bytes()
	return b, nil
}
goss-0.4.9/matchers/semver_constraint_test.go000066400000000000000000000165001467505051300214500ustar00rootroot00000000000000package matchers

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"

	"github.com/blang/semver/v4"
)

func TestBeSemverConstraint(t *testing.T) {
	type args struct {
		Constraint any
	}
	tests := []struct {
		name string
		args args
		want GossMatcher
	}{
		{
			name: "sanity",
			args: args{Constraint: "> 1.0.0"},
			want: &BeSemverConstraintMatcher{Constraint: "> 1.0.0"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := BeSemverConstraint(tt.args.Constraint)

			assert.Equal(t, tt.want, got)
		})
	}
}

func TestBeSemverConstraintMatcher_FailureMessage(t *testing.T) {
	type fields struct {
		Constraint any
	}
	type args struct {
		actual any
	}
	tests := []struct {
		name       string
		fields     fields
		args       args
		wantResult MatcherResult
	}{
		{
			name:   "string",
			fields: fields{Constraint: "> 1.1.0"},
			args:   args{actual: "1.0.0"},
			wantResult: MatcherResult{
				Actual:   "1.0.0",
				Message:  "to satisfy semver constraint",
				Expected: "> 1.1.0",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			matcher := &BeSemverConstraintMatcher{
				Constraint: tt.fields.Constraint,
			}
			gotResult := matcher.FailureResult(tt.args.actual)
			assert.Equal(t, tt.wantResult, gotResult)
		})
	}
}

func TestBeSemverConstraintMatcher_Match(t *testing.T) {
	type fields struct {
		Constraint any
	}
	type args struct {
		actual any
	}
	type want struct {
		success    bool
		err        bool
		errMessage string
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		want   want
	}{
		{
			name:   "pre_release_fail",
			fields: fields{Constraint: ">= 4.0.0"},
			args:   args{actual: []string{"4.0.0-rc1"}},
			want: want{
				success: false,
				err:     false,
			},
		},
		{
			name:   "pre_release_valid",
			fields: fields{Constraint: "< 4.0.0"},
			args:   args{actual: []string{"4.0.0-rc1"}},
			want: want{
				success: true,
				err:     false,
			},
		},
		{
			name:   "invalid_version_starting_with_0",
			fields: fields{Constraint: "> 4.0.0"},
			args:   args{actual: []string{"4.4.019-1"}},
			want: want{
				success:    false,
				err:        true,
				errMessage: "Expected a single or list of semver valid version(s).  Got:\n    <[]string | len:1, cap:1>: [\"4.4.019-1\"]",
			},
		},
		{
			name:   "build_fail",
			fields: fields{Constraint: "> 4.0.0"},
			args:   args{actual: []string{"4.4.019+build+build2"}},
			want: want{
				success:    false,
				err:        true,
				errMessage: "Expected a single or list of semver valid version(s).  Got:\n    <[]string | len:1, cap:1>: [\n        \"4.4.019+build+build2\",\n    ]",
			},
		},
		{
			name:   "build_valid",
			fields: fields{Constraint: "> 4.0.0"},
			args:   args{actual: []string{"4.1.0+build"}},
			want: want{
				success: true,
				err:     false,
			},
		},
		{
			name:   "invalid_actual",
			fields: fields{Constraint: nil},
			args:   args{actual: []string{"4.1.0"}},
			want: want{
				success:    false,
				err:        true,
				errMessage: "Expected a valid semver constraint.  Got:\n    : nil",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			matcher := &BeSemverConstraintMatcher{
				Constraint: tt.fields.Constraint,
			}
			gotSuccess, err := matcher.Match(tt.args.actual)

			assert.Equal(t, tt.want.success, gotSuccess, "has success")
			assert.Equal(t, tt.want.err, err != nil, "has error")
			if tt.want.err {
				assert.EqualError(t, err, tt.want.errMessage, "error message")
			}
		})
	}
}

func TestBeSemverConstraintMatcher_NegatedFailureMessage(t *testing.T) {
	type fields struct {
		Constraint any
	}
	type args struct {
		actual any
	}
	tests := []struct {
		name       string
		fields     fields
		args       args
		wantResult MatcherResult
	}{
		{
			name:   "string",
			fields: fields{Constraint: "> 1.1.0"},
			args:   args{actual: "1.0.0"},
			wantResult: MatcherResult{
				Actual:   "1.0.0",
				Message:  "not to satisfy semver constraint",
				Expected: "> 1.1.0",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			matcher := &BeSemverConstraintMatcher{
				Constraint: tt.fields.Constraint,
			}

			gotResult := matcher.NegatedFailureResult(tt.args.actual)
			assert.Equal(t, tt.wantResult, gotResult)
		})
	}
}

func Test_toConstraint(t *testing.T) {
	type args struct {
		in any
	}
	type want struct {
		ok bool
	}
	tests := []struct {
		name string
		args args
		want want
	}{
		{
			name: "simple",
			args: args{in: "> 1.0.0"},
			want: want{ok: true},
		},
		{
			name: "complex",
			args: args{in: "> 1.0.0 < 2.0.0 || > 4.0.0"},
			want: want{ok: true},
		},
		{
			name: "nil",
			args: args{in: nil},
			want: want{ok: false},
		},
		{
			name: "empty",
			args: args{in: ""},
			want: want{ok: false},
		},
		{
			name: "invalid",
			args: args{in: "invalid"},
			want: want{ok: false},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotConstraint, gotOk := toConstraint(tt.args.in)

			assert.Equal(t, tt.want.ok, gotOk, "success")
			if tt.want.ok {
				assert.NotNil(t, gotConstraint, "constraint shouldn't be nil")
				assert.IsType(t, semver.Range(nil), gotConstraint, "constraint type")
			}
		})
	}
}

func Test_toVersion(t *testing.T) {
	type args struct {
		in any
	}
	type want struct {
		ok bool
	}
	tests := []struct {
		name string
		args args
		want want
	}{
		{
			name: "simple",
			args: args{in: "1.0.0"},
			want: want{ok: true},
		},
		{
			name: "pre_release",
			args: args{in: "1.2.3-rc1"},
			want: want{ok: true},
		},
		{
			name: "build",
			args: args{in: "1.2.3+build1"},
			want: want{ok: true},
		},
		{
			name: "pre_release_build",
			args: args{in: "1.2.3+build1"},
			want: want{ok: true},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotVersion, gotOk := toVersion(tt.args.in)

			assert.Equal(t, tt.want.ok, gotOk)
			if tt.want.ok {
				assert.NotNil(t, gotVersion, "version shouldn't be nil")

				if gotVersion != nil {
					assert.Equal(t, fmt.Sprint(tt.args.in), gotVersion.String(), "version")
				}
			}
		})
	}
}

func Test_toVersions(t *testing.T) {
	type args struct {
		in any
	}
	type want struct {
		ok bool
	}
	tests := []struct {
		name string
		args args
		want want
	}{
		{
			name: "single",
			args: args{in: "1.0.0"},
			want: want{ok: true},
		},
		{
			name: "slice_strings",
			args: args{in: []string{"1.0.0"}},
			want: want{ok: true},
		},
		{
			name: "slice_interfaces",
			args: args{in: []any{"1.0.0"}},
			want: want{ok: true},
		},
		{
			name: "invalid_object",
			args: args{in: want{}},
			want: want{ok: false},
		},
		{
			name: "invalid_object_in_slice",
			args: args{in: []any{want{}}},
			want: want{ok: false},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotVersions, gotOk := toVersions(tt.args.in)

			assert.Equal(t, tt.want.ok, gotOk)
			if tt.want.ok {
				assert.NotNil(t, gotVersions, "versions shouldn't be nil")
				assert.NotEmpty(t, gotVersions, "versions shouldn't be empty")

				for i, version := range gotVersions {
					if versions, ok := tt.args.in.([]string); ok {
						assert.Equal(t, fmt.Sprint(versions[i]), version.String())
					} else if versions, ok := tt.args.in.([]any); ok {
						assert.Equal(t, fmt.Sprint(versions[i]), version.String())
					} else {
						assert.Equal(t, fmt.Sprint(tt.args.in), version.String())
					}
				}
			}
		})
	}
}
goss-0.4.9/matchers/type_conversion.go000066400000000000000000000056251467505051300201000ustar00rootroot00000000000000package matchers

import (
	"encoding/json"
	"fmt"
	"io"
	"strconv"
	"strings"

	"github.com/onsi/gomega/format"
	"github.com/tidwall/gjson"
)

type Transformer interface {
	Transform(interface{}) (interface{}, error)
}

type ToNumeric struct{}

func (t ToNumeric) Transform(e interface{}) (interface{}, error) {
	switch v := e.(type) {
	case float64, int:
		return v, nil
	case string:
		return strconv.ParseFloat(strings.TrimSpace(v), 64)
	case []string:
		i, err := ToString{}.Transform(v)
		if err != nil {
			return 0, err
		}
		s := i.(string)
		return strconv.ParseFloat(strings.TrimSpace(s), 64)
	default:
		return 0, fmt.Errorf("Expected numeric, Got:%s", format.Object(e, 1))

	}
}
func (t ToNumeric) MarshalJSON() ([]byte, error) {
	j := map[string]interface{}{
		"to-numeric": map[string]string{},
	}
	return json.Marshal(j)
}

type ToString struct{}

func (t ToString) Transform(e interface{}) (interface{}, error) {
	switch v := e.(type) {
	case []interface{}:
		vs := make([]string, len(v))
		for i, v := range v {
			vs[i] = fmt.Sprintf("%v", v)
		}
		return strings.Join(vs, "\n"), nil
	case []string:
		return strings.Join(v, "\n"), nil
	default:
		return fmt.Sprintf("%v", v), nil
	}
}

func (t ToString) MarshalJSON() ([]byte, error) {
	j := map[string]interface{}{
		"to-string": map[string]string{},
	}
	return json.Marshal(j)
}

type ToArray struct{}

func (t ToArray) Transform(i interface{}) (interface{}, error) {
	switch v := i.(type) {
	case string:
		return strings.Split(v, "\n"), nil
	default:
		return i, nil
	}
}
func (matcher ToArray) MarshalJSON() ([]byte, error) {
	j := map[string]interface{}{
		"to-array": map[string]string{},
	}
	return json.Marshal(j)
}

//type ReaderToStrings struct{}
//
//func (t ReaderToStrings) Transform(i interface{}) (interface{}, error) {
//	r, ok := i.(io.Reader)
//	if !ok {
//		return nil, fmt.Errorf("Expected io.reader, Got:%s", format.Object(i, 1))
//	}
//	var lines []string
//	i, err := ReaderToString{}.Transform(r)
//	if err != nil {
//		return lines, err
//	}
//	s := i.(string)
//	return strings.Split(s, "\n"), nil
//}

type ReaderToString struct{}

func (t ReaderToString) Transform(i interface{}) (interface{}, error) {
	r, ok := i.(io.Reader)
	if !ok {
		return nil, fmt.Errorf("Expected io.reader, Got:%s", format.Object(i, 1))
	}

	b, err := io.ReadAll(r)
	if err != nil {
		return "", err
	}
	return string(b), nil
}

type Gjson struct {
	Path string
}

func (g Gjson) Transform(i interface{}) (interface{}, error) {
	s, ok := i.(string)
	if !ok {
		return nil, fmt.Errorf("Expected string, Got:%s", format.Object(i, 1))
	}
	if !gjson.Valid(s) {
		return nil, fmt.Errorf("Invalid json")
	}
	r := gjson.Get(s, g.Path)
	if !r.Exists() {
		return nil, fmt.Errorf("Path not found: %s", g.Path)
	}

	return r.Value(), nil
}
func (g Gjson) MarshalJSON() ([]byte, error) {
	j := map[string]interface{}{
		"gjson": map[string]string{
			"Path": g.Path,
		},
	}
	return json.Marshal(j)
}
goss-0.4.9/matchers/with_safe_transform.go000066400000000000000000000040731467505051300207120ustar00rootroot00000000000000package matchers

import (
	"encoding/json"
	"fmt"
	"reflect"
)

type WithSafeTransformMatcher struct {
	fakeOmegaMatcher

	// input
	Transform Transformer // must be a function of one parameter that returns one value
	Matcher   GossMatcher

	// state
	transformedValue interface{}
	wasTransformed   bool
}

func WithSafeTransform(transform Transformer, matcher GossMatcher) GossMatcher {

	return &WithSafeTransformMatcher{
		Transform: transform,
		Matcher:   matcher,
	}
}

func (m *WithSafeTransformMatcher) Match(actual interface{}) (bool, error) {
	var err error
	//log.Printf("%#v: input: %v", m.Transform, actual)
	m.transformedValue, err = m.Transform.Transform(actual)
	if !reflect.DeepEqual(actual, m.transformedValue) {
		m.wasTransformed = true
	}
	if err != nil {
		return false, fmt.Errorf("%#v: %s", m.Transform, err)
	}
	//log.Printf("%#v: output: %v", m.Transform, m.transformedValue)
	return m.Matcher.Match(m.transformedValue)
}

func (m *WithSafeTransformMatcher) FailureResult(actual interface{}) MatcherResult {
	tchain, matcher, tvalue := m.getTransformerChainAndMatcher()
	result := matcher.FailureResult(tvalue)
	result.TransformerChain = tchain
	result.UntransformedValue = actual
	return result
}
func (m *WithSafeTransformMatcher) NegatedFailureResult(actual interface{}) MatcherResult {
	tchain, matcher, tvalue := m.getTransformerChainAndMatcher()
	result := matcher.NegatedFailureResult(tvalue)
	result.TransformerChain = tchain
	result.UntransformedValue = actual
	return result
}

func (m *WithSafeTransformMatcher) getTransformerChainAndMatcher() (tchain []Transformer, matcher GossMatcher, tvalue interface{}) {
	matcher = m
	tvalue = m.transformedValue
L:
	for {
		switch v := matcher.(type) {
		case *WithSafeTransformMatcher:
			matcher = v.Matcher
			tvalue = v.transformedValue
			if v.wasTransformed {
				tchain = append(tchain, v.Transform)
			}
		default:
			break L

		}
	}
	return tchain, matcher, tvalue

}

func (m *WithSafeTransformMatcher) MarshalJSON() ([]byte, error) {
	_, matcher, _ := m.getTransformerChainAndMatcher()
	return json.Marshal(matcher)
}
goss-0.4.9/mkdocs.yml000066400000000000000000000044571467505051300145220ustar00rootroot00000000000000site_name: Goss
site_description: Goss is a YAML based serverspec alternative tool for validating a server’s configuration.
site_author: Goss team
site_url: https://goss.readthedocs.io/
repo_url: https://github.com/goss-org/goss
repo_name: goss-org/goss
edit_uri: edit/master/docs/


theme:
  name: material
  palette:
    - media: "(prefers-color-scheme: light)"
      scheme: default
      primary: black
      accent: amber
      toggle:
        icon: material/weather-sunny
        name: Switch to dark mode
    - media: "(prefers-color-scheme: dark)"
      scheme: slate
      primary: black
      accent: indigo
      toggle:
        icon: material/weather-night
        name: Switch to light mode
  features:
    - content.action.edit
    - content.code.copy
    - navigation.footer
    - navigation.instant
    - navigation.instant.progress
    - navigation.top
    - navigation.tracking
    - search.highlight
    - search.share
    - search.suggest
    - toc.follow


extra_css:
  - style.css

plugins:
  - search
  - awesome-pages
  - macros:
      render_by_default: false
  - exclude:
       glob:
         - requirements.txt

markdown_extensions:
  - abbr
  - admonition
  - attr_list
  - def_list
  - md_in_html
  - mdx_breakless_lists
  - tables
  - pymdownx.details
  - pymdownx.emoji:
      emoji_index: !!python/name:material.extensions.emoji.twemoji
      emoji_generator: !!python/name:material.extensions.emoji.to_svg
  - pymdownx.highlight:
      anchor_linenums: true
      line_spans: __span
      pygments_lang_class: true
  - pymdownx.inlinehilite
  - pymdownx.magiclink:
      repo_url_shortener: true
      social_url_shortener: true
      repo_url_shorthand: true
      social_url_shorthand: true
      user: goss-org
      repo: goss
  - pymdownx.snippets:
      base_path:
        - .
        - docs/snippets
      check_paths: true
  - pymdownx.superfences

copyright: Copyright © 2015 - 2024 Ahmed Elsabbahy

extra:
  social:
    - icon: fontawesome/brands/github
      link: https://github.com/goss-org/goss
    - icon: simple/travisci
      link: https://travis-ci.org/goss-org/goss
    - icon: fontawesome/brands/medium
      link: https://medium.com/@aelsabbahy

watch:
  - README.md
  - LICENSE
  - .github/CONTRIBUTING.md
  - extras/dcgoss/README.md
  - extras/dgoss/README.md
  - extras/kgoss/README.md
goss-0.4.9/novendor.sh000077500000000000000000000006651467505051300147050ustar00rootroot00000000000000#!/usr/bin/env bash
set -euo pipefail

# Bash replacement for glide novendor command
# Returns all directories which include go files

DIRS=$(ls -ld */ . | awk {'print $9'} | grep -v vendor)

for DIR in ${DIRS}; do
    GOFILES=$(git ls-files ${DIR} | grep ".*\.go$") || true

    if [[ ${DIR} == "."  ]]; then
        echo "."
        continue
    fi

    if [[ ${GOFILES} != "" ]]; then
        echo "./"${DIR}"..."
    fi
done

exit 0
goss-0.4.9/outputs/000077500000000000000000000000001467505051300142305ustar00rootroot00000000000000goss-0.4.9/outputs/documentation.go000066400000000000000000000036251467505051300174360ustar00rootroot00000000000000package outputs

import (
	"fmt"
	"io"
	"time"

	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
)

type Documentation struct{}

func (r Documentation) ValidOptions() []*formatOption {
	return []*formatOption{
		{name: foSort},
	}
}

func (r Documentation) Output(w io.Writer, results <-chan []resource.TestResult,
	outConfig util.OutputConfig) (exitCode int) {
	includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)

	sort := util.IsValueInList(foSort, outConfig.FormatOptions)
	results = getResults(results, sort)

	var startTime time.Time
	var endTime time.Time
	testCount := 0
	var failedOrSkipped [][]resource.TestResult
	var skipped, failed int
	for resultGroup := range results {
		failedOrSkippedGroup := []resource.TestResult{}
		first := resultGroup[0]
		header := header(first)
		if header != "" {
			fmt.Fprint(w, header)
		}
		for _, testResult := range resultGroup {
			if startTime.IsZero() || testResult.StartTime.Before(startTime) {
				startTime = testResult.StartTime
			}
			if endTime.IsZero() || testResult.EndTime.After(endTime) {
				endTime = testResult.EndTime
			}
			switch testResult.Result {
			case resource.SUCCESS:
				fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw))
			case resource.SKIP:
				fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw))
				failedOrSkippedGroup = append(failedOrSkippedGroup, testResult)
				skipped++
			case resource.FAIL:
				fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw))
				failedOrSkippedGroup = append(failedOrSkippedGroup, testResult)
				failed++
			}
			testCount++
		}
		if len(failedOrSkippedGroup) > 0 {
			failedOrSkipped = append(failedOrSkipped, failedOrSkippedGroup)
		}
	}

	fmt.Fprint(w, "\n\n")
	fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped, includeRaw))

	fmt.Fprint(w, summary(startTime, endTime, testCount, failed, skipped))
	if failed > 0 {
		return 1
	}
	return 0
}
goss-0.4.9/outputs/json.go000066400000000000000000000047631467505051300155420ustar00rootroot00000000000000package outputs

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"time"

	"github.com/fatih/color"
	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
)

type Json struct{}

func (r Json) ValidOptions() []*formatOption {
	return []*formatOption{
		{name: foPretty},
		{name: foSort},
	}
}

func (r Json) Output(w io.Writer, results <-chan []resource.TestResult,
	outConfig util.OutputConfig) (exitCode int) {

	var pretty bool = util.IsValueInList(foPretty, outConfig.FormatOptions)
	includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)

	sort := util.IsValueInList(foSort, outConfig.FormatOptions)
	results = getResults(results, sort)

	var startTime time.Time
	var endTime time.Time
	color.NoColor = true
	testCount := 0
	failed := 0
	skipped := 0
	var resultsOut []map[string]any
	for resultGroup := range results {
		for _, testResult := range resultGroup {
			if startTime.IsZero() || testResult.StartTime.Before(startTime) {
				startTime = testResult.StartTime
			}
			if endTime.IsZero() || testResult.EndTime.After(endTime) {
				endTime = testResult.EndTime
			}
			if testResult.Result == resource.FAIL {
				failed++
				logTrace("TRACE", "FAIL", testResult, true)
			} else {
				logTrace("TRACE", "SUCCESS", testResult, true)
			}
			if testResult.Skipped {
				skipped++
			}
			m := struct2map(testResult)
			m["successful"] = testResult.Result != resource.FAIL
			m["summary-line"] = humanizeResult(testResult, false, includeRaw)
			m["summary-line-compact"] = humanizeResult(testResult, true, includeRaw)
			m["duration"] = testResult.Duration.Nanoseconds()
			resultsOut = append(resultsOut, m)
			testCount++
		}
	}

	summary := make(map[string]any)
	duration := endTime.Sub(startTime)
	summary["test-count"] = testCount
	summary["failed-count"] = failed
	summary["skipped-count"] = skipped
	summary["total-duration"] = duration
	summary["summary-line"] = fmt.Sprintf("Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs", testCount, failed, skipped, duration.Seconds())

	out := make(map[string]any)
	out["results"] = resultsOut
	out["summary"] = summary

	var j []byte
	if pretty {
		j, _ = json.MarshalIndent(out, "", "    ")
	} else {
		j, _ = json.Marshal(out)
	}

	resstr := string(j)
	fmt.Fprintln(w, resstr)

	if failed > 0 {
		log.Printf("[DEBUG] FAIL SUMMARY: %s", resstr)
		return 1
	}

	log.Printf("[DEBUG] OK SUMMARY: %s", resstr)
	return 0
}

func struct2map(i any) map[string]any {
	out := make(map[string]any)
	j, _ := json.Marshal(i)
	json.Unmarshal(j, &out)
	return out
}
goss-0.4.9/outputs/junit.go000066400000000000000000000050421467505051300157110ustar00rootroot00000000000000package outputs

import (
	"bytes"
	"encoding/xml"
	"fmt"
	"io"
	"strconv"
	"time"

	"github.com/fatih/color"
	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
)

type JUnit struct{}

func (r JUnit) ValidOptions() []*formatOption {
	return []*formatOption{
		{name: foSort},
	}
}

func (r JUnit) Output(w io.Writer, results <-chan []resource.TestResult,
	outConfig util.OutputConfig) (exitCode int) {
	includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)

	sort := util.IsValueInList(foSort, outConfig.FormatOptions)
	results = getResults(results, sort)

	color.NoColor = true
	var testCount, failed, skipped int

	// ISO8601 timeformat
	timestamp := time.Now().Format(time.RFC3339)

	var summary map[int]string = make(map[int]string)

	var startTime time.Time
	var endTime time.Time
	for resultGroup := range results {
		for _, testResult := range resultGroup {
			if startTime.IsZero() || testResult.StartTime.Before(startTime) {
				startTime = testResult.StartTime
			}
			if endTime.IsZero() || testResult.EndTime.After(endTime) {
				endTime = testResult.EndTime
			}
			duration := strconv.FormatFloat(testResult.Duration.Seconds(), 'f', 3, 64)
			summary[testCount] = "\n"
			if testResult.Result == resource.FAIL {
				summary[testCount] += "" +
					escapeString(humanizeResult(testResult, true, includeRaw)) +
					"\n"
				summary[testCount] += "" +
					escapeString(humanizeResult(testResult, true, includeRaw)) +
					"\n\n"

				failed++
			} else {
				if testResult.Result == resource.SKIP {
					summary[testCount] += ""
					skipped++
				}
				summary[testCount] += "" +
					escapeString(humanizeResult(testResult, true, includeRaw)) +
					"\n\n"
			}
			testCount++
		}
	}

	duration := endTime.Sub(startTime)
	fmt.Fprintln(w, "")
	fmt.Fprintf(w, "\n",
		testCount, failed, skipped, duration.Seconds(), timestamp)

	for i := 0; i < testCount; i++ {
		fmt.Fprintf(w, "%s", summary[i])
	}

	fmt.Fprintln(w, "")

	if failed > 0 {
		return 1
	}

	return 0
}

func escapeString(str string) string {
	buffer := new(bytes.Buffer)
	xml.EscapeText(buffer, []byte(str))
	return buffer.String()
}
goss-0.4.9/outputs/nagios.go000066400000000000000000000041221467505051300160360ustar00rootroot00000000000000package outputs

import (
	"fmt"
	"io"
	"strconv"
	"time"

	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
)

type Nagios struct{}

func (r Nagios) ValidOptions() []*formatOption {
	return []*formatOption{
		{name: foPerfData},
		{name: foVerbose},
	}
}

func (r Nagios) Output(w io.Writer, results <-chan []resource.TestResult,
	outConfig util.OutputConfig) (exitCode int) {

	var testCount, failed, skipped int

	var perfdata, verbose bool
	perfdata = util.IsValueInList(foPerfData, outConfig.FormatOptions)
	verbose = util.IsValueInList(foVerbose, outConfig.FormatOptions)
	includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)

	var startTime time.Time
	var endTime time.Time
	var summary map[int]string = make(map[int]string)

	for resultGroup := range results {
		for _, testResult := range resultGroup {
			if startTime.IsZero() || testResult.StartTime.Before(startTime) {
				startTime = testResult.StartTime
			}
			if endTime.IsZero() || testResult.EndTime.After(endTime) {
				endTime = testResult.EndTime
			}
			switch testResult.Result {
			case resource.FAIL:
				if util.IsValueInList(foVerbose, outConfig.FormatOptions) {
					summary[failed] = "Fail " + strconv.Itoa(failed+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n"
				}
				failed++
			case resource.SKIP:
				skipped++
			}
			testCount++
		}
	}

	duration := endTime.Sub(startTime)
	if failed > 0 {
		fmt.Fprintf(w, "GOSS CRITICAL - Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs", testCount, failed, skipped, duration.Seconds())
		if perfdata {
			fmt.Fprintf(w, "|total=%d failed=%d skipped=%d duration=%.3fs", testCount, failed, skipped, duration.Seconds())
		}
		fmt.Fprint(w, "\n")
		if verbose {
			for i := 0; i < failed; i++ {
				fmt.Fprintf(w, "%s", summary[i])
			}
		}
		return 2
	}
	fmt.Fprintf(w, "GOSS OK - Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs", testCount, failed, skipped, duration.Seconds())
	if perfdata {
		fmt.Fprintf(w, "|total=%d failed=%d skipped=%d duration=%.3fs", testCount, failed, skipped, duration.Seconds())
	}
	fmt.Fprint(w, "\n")
	return 0
}
goss-0.4.9/outputs/outputs.go000066400000000000000000000164761467505051300163200ustar00rootroot00000000000000package outputs

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"reflect"
	"regexp"
	"sort"
	"strings"
	"sync"
	"time"
	"unicode"

	"github.com/fatih/color"
	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
	"github.com/pmezard/go-difflib/difflib"
)

type formatOption struct {
	name string
}

type Outputer interface {
	Output(io.Writer, <-chan []resource.TestResult, util.OutputConfig) int
	ValidOptions() []*formatOption
}

var (
	outputersMu sync.Mutex
	outputers   = map[string]Outputer{
		"documentation": &Documentation{},
		"json":          &Json{},
		"junit":         &JUnit{},
		"nagios":        &Nagios{},
		"prometheus":    &Prometheus{},
		"rspecish":      &Rspecish{},
		"structured":    &Structured{},
		"tap":           &Tap{},
		"silent":        &Silent{},
	}
	foPerfData   = "perfdata"
	foVerbose    = "verbose"
	foPretty     = "pretty"
	foExcludeRaw = "exclude_raw"
	foSort       = "sort"
)

var green = color.New(color.FgGreen).SprintfFunc()
var red = color.New(color.FgRed).SprintfFunc()
var yellow = color.New(color.FgYellow).SprintfFunc()
var multiple_space = regexp.MustCompile(`\s+`)

func humanizeResult(r resource.TestResult, compact bool, includeRaw bool) string {
	sep := "\n"
	if compact {
		sep = " "
	}

	switch r.Result {
	case resource.SUCCESS:
		return green("%s: %s: %s: %s: %s", r.ResourceType, r.ResourceId, r.Property, r.MatcherResult.Message, prettyPrint(r.MatcherResult.Expected, false))
	case resource.FAIL:
		matcherResult := prettyPrintTestResult(r, compact, includeRaw)
		return red("%s: %s: %s:%s%s", r.ResourceType, r.ResourceId, r.Property, sep, matcherResult)
	case resource.SKIP:
		return yellow("%s: %s: %s: skipped", r.ResourceType, r.ResourceId, r.Property)
	default:
		panic(fmt.Sprintf("Unexpected Result Code: %v\n", r.Result))
	}
}

func prettyPrintTestResult(t resource.TestResult, compact bool, includeRaw bool) string {
	sep := "\n"
	if compact {
		sep = " "
	}
	m := t.MatcherResult
	var ss []string
	//var s string
	if t.Err != nil {
		e := fmt.Sprint(t.Err)
		if compact {
			e = multiple_space.ReplaceAllString(e, " ")
		} else {
			e = indentLines(e)
		}
		ss = append(ss, "Error")
		ss = append(ss, e)
	} else {
		ss = append(ss, "Expected")
		ss = append(ss, prettyPrint(m.Actual, !compact))
		ss = append(ss, m.Message)
		ss = append(ss, prettyPrint(m.Expected, !compact))
		ss = maybeAddDiff(ss, m.Expected, m.Actual, compact)
	}

	if reflect.ValueOf(m.MissingElements).IsValid() && !reflect.ValueOf(m.MissingElements).IsNil() {
		ss = append(ss, "the missing elements were")
		ss = append(ss, prettyPrint(m.MissingElements, !compact))
	}
	if reflect.ValueOf(m.ExtraElements).IsValid() && !reflect.ValueOf(m.ExtraElements).IsNil() {
		ss = append(ss, "the extra elements were")
		ss = append(ss, prettyPrint(m.ExtraElements, !compact))
	}
	if len(m.TransformerChain) != 0 {
		ss = append(ss, "the transform chain was")
		ss = append(ss, prettyPrint(m.TransformerChain, !compact))
		if includeRaw {
			ss = append(ss, "the raw value was")
			ss = append(ss, prettyPrint(m.UntransformedValue, !compact))
		}
	}
	return strings.Join(ss, sep)
}

func maybeAddDiff(ss []string, expected, actual any, compact bool) []string {
	if compact {
		return ss
	}
	want, ok := expected.(string)
	if !ok {
		return ss
	}
	got, ok := actual.(string)
	if !ok {
		return ss
	}
	if want == got {
		return ss
	}
	ss = append(ss, "diff")
	diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
		A:        difflib.SplitLines(want),
		B:        difflib.SplitLines(got),
		FromFile: "test",
		FromDate: "",
		ToFile:   "actual",
		ToDate:   "",
		Context:  1,
	})
	ss = append(ss, indentLines(diff))
	return ss
}

func prettyPrint(i interface{}, indent bool) string {
	buffer := &bytes.Buffer{}
	encoder := json.NewEncoder(buffer)
	encoder.SetEscapeHTML(false)
	var b []byte
	err := encoder.Encode(i)
	if err == nil {
		b = buffer.Bytes()
	} else {
		// FIXME: Is this the right thing to do?
		b = []byte(err.Error())
	}
	b = bytes.TrimRightFunc(b, unicode.IsSpace)
	if indent {
		return indentLines(string(b))
	} else {
		return string(b)
	}
}

// indents a block of text with an indent string
func indentLines(text string) string {
	indent := "    "
	result := ""
	for _, j := range strings.Split(strings.TrimRight(text, "\n"), "\n") {
		result += indent + j + "\n"
	}
	return result[:len(result)-1]
}

func RegisterOutputer(name string, outputer Outputer) {
	outputersMu.Lock()
	defer outputersMu.Unlock()

	if outputer == nil {
		panic("goss: Register outputer is nil")
	}
	if _, dup := outputers[name]; dup {
		panic("goss: Register called twice for ouputer " + name)
	}
	outputers[name] = outputer
}

// Outputers returns a sorted list of the names of the registered outputers.
func Outputers() []string {
	outputersMu.Lock()
	defer outputersMu.Unlock()
	var list []string
	for name := range outputers {
		list = append(list, name)
	}
	sort.Strings(list)
	return list
}

// FormatOptions returns a sorted list of all the valid options that outputers accept
func FormatOptions() []string {
	outputersMu.Lock()
	defer outputersMu.Unlock()
	found := map[string]*formatOption{}
	for _, o := range outputers {
		for _, opt := range o.ValidOptions() {
			found[opt.name] = opt
		}
	}
	var list []string
	for name := range found {
		list = append(list, name)
	}
	sort.Strings(list)
	return list
}

// IsValidFormat determines if f is a valid format name based on Outputers()
func IsValidFormat(f string) bool {
	for _, o := range Outputers() {
		if o == f {
			return true
		}
	}

	return false
}

func GetOutputer(name string) (Outputer, error) {
	if _, ok := outputers[name]; !ok {
		return nil, fmt.Errorf("bad output format: " + name)
	}
	return outputers[name], nil
}

func header(t resource.TestResult) string {
	var out string
	if t.Title != "" {
		out += fmt.Sprintf("Title: %s\n", t.Title)
	}
	if t.Meta != nil {
		var keys []string
		for k := range t.Meta {
			keys = append(keys, k)
		}
		sort.Strings(keys)

		out += "Meta:\n"
		for _, k := range keys {
			out += fmt.Sprintf("    %v: %v\n", k, t.Meta[k])
		}
	}
	return out
}

func summary(startTime, endTime time.Time, count, failed, skipped int) string {
	var s string
	s += fmt.Sprintf("Total Duration: %.3fs\n", endTime.Sub(startTime).Seconds())
	f := green
	if failed > 0 {
		f = red
	}
	s += f("Count: %d, Failed: %d, Skipped: %d\n", count, failed, skipped)
	return s
}

func failedOrSkippedSummary(failedOrSkipped [][]resource.TestResult, includeRaw bool) string {
	var s string
	if len(failedOrSkipped) > 0 {
		s += "Failures/Skipped:\n\n"
		sort.Slice(failedOrSkipped, func(i, j int) bool {
			return failedOrSkipped[i][0].SortKey() < failedOrSkipped[j][0].SortKey()
		})
		for _, failedGroup := range failedOrSkipped {
			first := failedGroup[0]
			header := header(first)
			if header != "" {
				s += fmt.Sprint(header)
			}
			for _, testResult := range failedGroup {
				s += fmt.Sprintln(humanizeResult(testResult, false, includeRaw))
			}
			s += "\n"
		}
	}
	return s
}

func getResults(tr <-chan []resource.TestResult, doSort bool) <-chan []resource.TestResult {
	if !doSort {
		return tr
	}
	str := make([][]resource.TestResult, 0)
	for i := range tr {
		str = append(str, i)
	}

	sort.Slice(str, func(i, j int) bool {
		return str[i][0].SortKey() < str[j][0].SortKey()
	})

	c := make(chan []resource.TestResult)
	go func(c chan []resource.TestResult) {
		defer close(c)

		for _, i := range str {
			c <- i
		}
	}(c)

	return c
}
goss-0.4.9/outputs/outputs_test.go000066400000000000000000000026361467505051300173500ustar00rootroot00000000000000package outputs

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestIsValidFormat(t *testing.T) {
	if IsValidFormat("ne") {
		t.Fatal("'ne' should not be a valid output format")
	}

	if !IsValidFormat("json") {
		t.Fatal("'json' should be a valid output format")
	}
}

func TestOutputers(t *testing.T) {
	list := Outputers()
	assert.NotEmpty(t, list)
}

func TestGetOutputer(t *testing.T) {
	t.Run("valid", func(t *testing.T) {
		got, err := GetOutputer("rspecish")
		assert.NoError(t, err)
		assert.NotNil(t, got)
	})
	t.Run("not-valid", func(t *testing.T) {
		got, err := GetOutputer("gibberish")
		assert.Error(t, err)
		assert.Nil(t, got)
	})
}

func TestOutputFormatOptions(t *testing.T) {
	list := FormatOptions()
	assert.NotEmpty(t, list)

	assert.Contains(t, list, foPerfData)
	assert.Contains(t, list, foPretty)
	assert.Contains(t, list, foVerbose)
	assert.Len(t, list, 4)
}

func TestOptionsRegistration(t *testing.T) {
	registeredOutputs := Outputers()
	assert.Contains(t, registeredOutputs, "documentation")
	assert.Contains(t, registeredOutputs, "json")
	assert.Contains(t, registeredOutputs, "junit")
	assert.Contains(t, registeredOutputs, "nagios")
	assert.Contains(t, registeredOutputs, "prometheus")
	assert.Contains(t, registeredOutputs, "rspecish")
	assert.Contains(t, registeredOutputs, "silent")
	assert.Contains(t, registeredOutputs, "structured")
	assert.Contains(t, registeredOutputs, "tap")
}
goss-0.4.9/outputs/prometheus.go000066400000000000000000000074651467505051300167660ustar00rootroot00000000000000package outputs

import (
	"io"
	"strings"
	"time"

	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/common/expfmt"
)

const (
	labelType       = "type"
	labelOutcome    = "outcome"
	labelResourceId = "resource_id"
)

var (
	registry      *prometheus.Registry
	testOutcomes  *prometheus.CounterVec
	testDurations *prometheus.CounterVec
	runOutcomes   *prometheus.CounterVec
	runDuration   *prometheus.CounterVec
)

// Prometheus renders metrics in prometheus.io text-format https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
type Prometheus struct{}

// ValidOptions is a list of valid format options for prometheus
func (r Prometheus) ValidOptions() []*formatOption {
	return []*formatOption{
		{name: foVerbose},
	}
}

// Output converts the results into the prometheus text-format.
func (r Prometheus) Output(w io.Writer, results <-chan []resource.TestResult,
	outConfig util.OutputConfig) (exitCode int) {
	verbose := util.IsValueInList(foVerbose, outConfig.FormatOptions)

	if registry == nil {
		setupMetrics(verbose)
	}

	overallOutcome := resource.OutcomeUnknown
	var startTime time.Time
	for resultGroup := range results {
		for i, tr := range resultGroup {
			if startTime.IsZero() || tr.StartTime.Before(startTime) {
				startTime = tr.StartTime
			}
			resType := strings.ToLower(tr.ResourceType)
			outcome := tr.ToOutcome()
			if verbose {
				resId := tr.ResourceId
				testOutcomes.WithLabelValues(resType, outcome, resId).Inc()
				testDurations.WithLabelValues(resType, outcome, resId).Add(float64(tr.Duration.Milliseconds()))
			} else {
				testOutcomes.WithLabelValues(resType, outcome).Inc()
				testDurations.WithLabelValues(resType, outcome).Add(float64(tr.Duration.Milliseconds()))
			}
			if i == 0 || canChangeOverallOutcome(overallOutcome, outcome) {
				overallOutcome = outcome
			}
		}
	}

	runOutcomes.WithLabelValues(overallOutcome).Inc()
	runDuration.WithLabelValues(overallOutcome).Add(float64(time.Since(startTime).Milliseconds()))

	metricsFamilies, err := registry.Gather()
	if err != nil {
		return -1
	}

	encoder := expfmt.NewEncoder(w, expfmt.NewFormat(expfmt.TypeTextPlain))
	for _, mf := range metricsFamilies {
		err := encoder.Encode(mf)
		if err != nil {
			return -1
		}
	}

	return 0
}

func setupMetrics(verbose bool) {
	registry = prometheus.NewRegistry()
	factory := promauto.With(registry)

	var testLabels []string
	if verbose {
		testLabels = []string{labelType, labelOutcome, labelResourceId}
	} else {
		testLabels = []string{labelType, labelOutcome}
	}

	testOutcomes = factory.NewCounterVec(prometheus.CounterOpts{
		Namespace: "goss",
		Subsystem: "tests",
		Name:      "outcomes_total",
		Help:      "The number of test-outcomes from this run.",
	}, testLabels)
	testDurations = factory.NewCounterVec(prometheus.CounterOpts{
		Namespace: "goss",
		Subsystem: "tests",
		Name:      "outcomes_duration_milliseconds",
		Help:      "The duration of tests from this run. Note; tests run concurrently.",
	}, testLabels)
	runOutcomes = factory.NewCounterVec(prometheus.CounterOpts{
		Namespace: "goss",
		Subsystem: "tests",
		Name:      "run_outcomes_total",
		Help:      "The outcomes of this run as a whole.",
	}, []string{labelOutcome})
	runDuration = factory.NewCounterVec(prometheus.CounterOpts{
		Namespace: "goss",
		Subsystem: "tests",
		Name:      "run_duration_milliseconds",
		Help:      "The end-to-end duration of this run.",
	}, []string{labelOutcome})
}

func canChangeOverallOutcome(current, result string) bool {
	switch current {
	case resource.OutcomeSkip:
		return true
	case resource.OutcomeFail:
		return false
	case resource.OutcomePass:
		return result != resource.OutcomeSkip
	default:
		return result == resource.OutcomeFail
	}
}
goss-0.4.9/outputs/prometheus_test.go000066400000000000000000000446401467505051300200210ustar00rootroot00000000000000package outputs

import (
	"bytes"
	"fmt"
	"sync"
	"testing"
	"time"

	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
	"github.com/stretchr/testify/assert"
)

func TestPrometheusOutput(t *testing.T) {
	testCases := map[string]struct {
		results         []resource.TestResult
		formatOptions   []string
		expectedMetrics []string
	}{
		"all-success-single-type": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",type="command"} 20`,
				`goss_tests_outcomes_total{outcome="pass",type="command"} 2`,
				`goss_tests_run_duration_milliseconds{outcome="pass"}`,
				`goss_tests_run_outcomes_total{outcome="pass"} 1`,
			},
		},
		"all-skip-single-type": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SKIP,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SKIP,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="skip",type="command"} 20`,
				`goss_tests_outcomes_total{outcome="skip",type="command"} 2`,
				`goss_tests_run_duration_milliseconds{outcome="skip"}`,
				`goss_tests_run_outcomes_total{outcome="skip"} 1`,
			},
		},
		"all-fail-single-type": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.FAIL,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.FAIL,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="fail",type="command"} 20`,
				`goss_tests_outcomes_total{outcome="fail",type="command"} 2`,
				`goss_tests_run_duration_milliseconds{outcome="fail"}`,
				`goss_tests_run_outcomes_total{outcome="fail"} 1`,
			},
		},
		"all-unknown-single-type": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.UNKNOWN,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.UNKNOWN,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="unknown",type="command"} 20`,
				`goss_tests_outcomes_total{outcome="unknown",type="command"} 2`,
				`goss_tests_run_duration_milliseconds{outcome="unknown"}`,
				`goss_tests_run_outcomes_total{outcome="unknown"} 1`,
			},
		},
		"all-success-multiple-types": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
				{
					ResourceType: "File",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",type="file"} 10`,
				`goss_tests_outcomes_total{outcome="pass",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="pass",type="file"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="pass"}`,
				`goss_tests_run_outcomes_total{outcome="pass"} 1`,
			},
		},
		"various-results-single-type": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SKIP,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.FAIL,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.UNKNOWN,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="skip",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="fail",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="unknown",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="pass",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="skip",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="fail",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="unknown",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="fail"}`,
				`goss_tests_run_outcomes_total{outcome="fail"} 1`,
			},
		},
		"various-results-multiple-types": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
				{
					ResourceType: "File",
					Duration:     10 * time.Millisecond,
					Result:       resource.SKIP,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.FAIL,
				},
				{
					ResourceType: "File",
					Duration:     10 * time.Millisecond,
					Result:       resource.UNKNOWN,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="skip",type="file"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="fail",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="unknown",type="file"} 10`,
				`goss_tests_outcomes_total{outcome="pass",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="skip",type="file"} 1`,
				`goss_tests_outcomes_total{outcome="fail",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="unknown",type="file"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="fail"}`,
				`goss_tests_run_outcomes_total{outcome="fail"} 1`,
			},
		},
		"unknown-skip": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.UNKNOWN,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SKIP,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="skip",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="unknown",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="skip",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="unknown",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="unknown"}`,
				`goss_tests_run_outcomes_total{outcome="unknown"} 1`,
			},
		},
		"unknown-fail": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.UNKNOWN,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.FAIL,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="fail",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="unknown",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="fail",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="unknown",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="fail"}`,
				`goss_tests_run_outcomes_total{outcome="fail"} 1`,
			},
		},
		"unknown-success": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.UNKNOWN,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="unknown",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="pass",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="unknown",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="unknown"}`,
				`goss_tests_run_outcomes_total{outcome="unknown"} 1`,
			},
		},
		"skip-unknown": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SKIP,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.UNKNOWN,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="skip",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="unknown",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="skip",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="unknown",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="unknown"}`,
				`goss_tests_run_outcomes_total{outcome="unknown"} 1`,
			},
		},
		"skip-fail": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SKIP,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.FAIL,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="skip",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="fail",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="skip",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="fail",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="fail"}`,
				`goss_tests_run_outcomes_total{outcome="fail"} 1`,
			},
		},
		"skip-success": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SKIP,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="skip",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="pass",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="skip",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="pass"}`,
				`goss_tests_run_outcomes_total{outcome="pass"} 1`,
			},
		},
		"fail-unknown": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.FAIL,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.UNKNOWN,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="fail",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="unknown",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="fail",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="unknown",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="fail"}`,
				`goss_tests_run_outcomes_total{outcome="fail"} 1`,
			},
		},
		"fail-skip": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.FAIL,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SKIP,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="fail",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="skip",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="fail",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="skip",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="fail"}`,
				`goss_tests_run_outcomes_total{outcome="fail"} 1`,
			},
		},
		"fail-success": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.FAIL,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="fail",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="fail",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="pass",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="fail"}`,
				`goss_tests_run_outcomes_total{outcome="fail"} 1`,
			},
		},
		"success-unknown": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.UNKNOWN,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="unknown",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="pass",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="unknown",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="unknown"}`,
				`goss_tests_run_outcomes_total{outcome="unknown"} 1`,
			},
		},
		"success-skip": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SKIP,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="skip",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="pass",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="skip",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="pass"}`,
				`goss_tests_run_outcomes_total{outcome="pass"} 1`,
			},
		},
		"success-fail": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
				{
					ResourceType: "Command",
					Duration:     10 * time.Millisecond,
					Result:       resource.FAIL,
				},
			},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",type="command"} 10`,
				`goss_tests_outcomes_duration_milliseconds{outcome="fail",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="pass",type="command"} 1`,
				`goss_tests_outcomes_total{outcome="fail",type="command"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="fail"}`,
				`goss_tests_run_outcomes_total{outcome="fail"} 1`,
			},
		},
		"no-results": {
			results: []resource.TestResult{},
			expectedMetrics: []string{
				`goss_tests_run_duration_milliseconds{outcome="unknown"}`,
				`goss_tests_run_outcomes_total{outcome="unknown"} 1`,
			},
		},
		"verbose": {
			results: []resource.TestResult{
				{
					ResourceType: "Command",
					ResourceId:   "some command here",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
				{
					ResourceType: "Command",
					ResourceId:   "something else here",
					Duration:     10 * time.Millisecond,
					Result:       resource.SUCCESS,
				},
				{
					ResourceType: "File",
					ResourceId:   "/path/to/file",
					Duration:     10 * time.Millisecond,
					Result:       resource.FAIL,
				},
			},
			formatOptions: []string{foVerbose},
			expectedMetrics: []string{
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",resource_id="some command here",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="pass",resource_id="some command here",type="command"} 1`,
				`goss_tests_outcomes_duration_milliseconds{outcome="pass",resource_id="something else here",type="command"} 10`,
				`goss_tests_outcomes_total{outcome="pass",resource_id="something else here",type="command"} 1`,
				`goss_tests_outcomes_duration_milliseconds{outcome="fail",resource_id="/path/to/file",type="file"} 10`,
				`goss_tests_outcomes_total{outcome="fail",resource_id="/path/to/file",type="file"} 1`,
				`goss_tests_run_duration_milliseconds{outcome="fail"}`,
				`goss_tests_run_outcomes_total{outcome="fail"} 1`,
			},
		},
	}

	for name, testCase := range testCases {
		t.Run(name, func(t *testing.T) {
			buf := &bytes.Buffer{}
			outputer := &Prometheus{}
			config := util.OutputConfig{
				FormatOptions: testCase.formatOptions,
			}

			defer resetMetrics()

			exitCode := outputer.Output(buf, makeResults(testCase.results...), config)
			assert.Equal(t, 0, exitCode)

			output := buf.String()
			t.Logf(output)
			for _, metric := range testCase.expectedMetrics {
				assert.Contains(t, output, metric)
			}
		})
	}
}

func makeResults(results ...resource.TestResult) <-chan []resource.TestResult {
	out := make(chan []resource.TestResult)
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		out <- append([]resource.TestResult{}, results...)
	}()

	go func() {
		wg.Wait()
		close(out)
	}()
	return out
}

func resetMetrics() {
	registry = nil
}

func TestCanChangeOverallOutcome(t *testing.T) {
	testCases := map[string]map[string]bool{
		resource.OutcomePass: {
			resource.OutcomePass:    true,
			resource.OutcomeSkip:    false,
			resource.OutcomeFail:    true,
			resource.OutcomeUnknown: true,
		},
		resource.OutcomeSkip: {
			resource.OutcomePass:    true,
			resource.OutcomeSkip:    true,
			resource.OutcomeFail:    true,
			resource.OutcomeUnknown: true,
		},
		resource.OutcomeFail: {
			resource.OutcomePass:    false,
			resource.OutcomeSkip:    false,
			resource.OutcomeFail:    false,
			resource.OutcomeUnknown: false,
		},
		resource.OutcomeUnknown: {
			resource.OutcomePass:    false,
			resource.OutcomeSkip:    false,
			resource.OutcomeFail:    true,
			resource.OutcomeUnknown: false,
		},
	}
	for current, expectations := range testCases {
		for result, canChange := range expectations {
			t.Run(fmt.Sprintf("%s/%s", current, result), func(t *testing.T) {
				assert.Equalf(t, canChange, canChangeOverallOutcome(current, result), "canChangeOverallOutcome(%v, %v)", current, result)
			})
		}
	}
}
goss-0.4.9/outputs/rspecish.go000066400000000000000000000042471467505051300164060ustar00rootroot00000000000000package outputs

import (
	"fmt"
	"io"
	"log"
	"strings"
	"time"

	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
)

type Rspecish struct{}

func (r Rspecish) ValidOptions() []*formatOption {
	return []*formatOption{}
}

func (r Rspecish) Output(w io.Writer, results <-chan []resource.TestResult,
	outConfig util.OutputConfig) (exitCode int) {

	sort := util.IsValueInList(foSort, outConfig.FormatOptions)
	results = getResults(results, sort)

	var startTime time.Time
	var endTime time.Time
	testCount := 0
	var failedOrSkipped [][]resource.TestResult
	var skipped, failed int
	for resultGroup := range results {
		failedOrSkippedGroup := []resource.TestResult{}
		for _, testResult := range resultGroup {
			// Calculates the start and end times based on the start of the first test
			// and end of the last test, this allows the time/duration to be stable
			// FIXME: move this to shared code
			if startTime.IsZero() || testResult.StartTime.Before(startTime) {
				startTime = testResult.StartTime
			}
			if endTime.IsZero() || testResult.EndTime.After(endTime) {
				endTime = testResult.EndTime
			}
			switch testResult.Result {
			case resource.SUCCESS:
				logTrace("TRACE", "SUCCESS", testResult, false)
				fmt.Fprint(w, green("."))
			case resource.SKIP:
				logTrace("TRACE", "SKIP", testResult, false)
				fmt.Fprint(w, yellow("S"))
				failedOrSkippedGroup = append(failedOrSkippedGroup, testResult)
				skipped++
			case resource.FAIL:
				logTrace("TRACE", "FAIL", testResult, false)
				fmt.Fprint(w, red("F"))
				failedOrSkippedGroup = append(failedOrSkippedGroup, testResult)
				failed++
			}
			testCount++
		}
		if len(failedOrSkippedGroup) > 0 {
			failedOrSkipped = append(failedOrSkipped, failedOrSkippedGroup)
		}
	}

	fmt.Fprint(w, "\n\n")
	includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)

	fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped, includeRaw))

	outstr := summary(startTime, endTime, testCount, failed, skipped)
	fmt.Fprint(w, outstr)
	resstr := strings.ReplaceAll(outstr, "\n", " ")
	if failed > 0 {
		log.Printf("[DEBUG] FAIL SUMMARY: %s", resstr)
		return 1
	}
	log.Printf("[DEBUG] OK SUMMARY: %s", resstr)
	return 0
}
goss-0.4.9/outputs/silent.go000066400000000000000000000010411467505051300160510ustar00rootroot00000000000000package outputs

import (
	"io"

	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
)

type Silent struct{}

func (r Silent) ValidOptions() []*formatOption {
	return []*formatOption{}
}

func (r Silent) Output(w io.Writer, results <-chan []resource.TestResult,
	outConfig util.OutputConfig) (exitCode int) {

	var failed int
	for resultGroup := range results {
		for _, testResult := range resultGroup {
			switch testResult.Result {
			case resource.FAIL:
				failed++
			}
		}
	}

	if failed > 0 {
		return 1
	}
	return 0
}
goss-0.4.9/outputs/structured.go000066400000000000000000000055321467505051300167700ustar00rootroot00000000000000package outputs

import (
	"encoding/json"
	"fmt"
	"io"
	"time"

	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
)

// Structured is a output formatter that logs into a StructuredOutput structure
type Structured struct{}

func (r Structured) ValidOptions() []*formatOption {
	return []*formatOption{
		{name: foPretty},
		{name: foSort},
	}
}

// StructuredTestResult is an individual test result with additional human friendly summary
type StructuredTestResult struct {
	resource.TestResult
	SummaryLine        string `json:"summary-line"`
	SummaryLineCompact string `json:"summary-line-compact"`
}

// StructureTestSummary holds summary information about a test run
type StructureTestSummary struct {
	TestCount     int           `json:"test-count"`
	Failed        int           `json:"failed-count"`
	TotalDuration time.Duration `json:"total-duration"`
}

// StructuredOutput is the full output structure for the structured output format
type StructuredOutput struct {
	Results     []StructuredTestResult `json:"results"`
	Summary     StructureTestSummary   `json:"summary"`
	SummaryLine string                 `json:"summary-line"`
}

// String represents human friendly representation of the test summary
func (s *StructureTestSummary) String() string {
	return fmt.Sprintf("Count: %d, Failed: %d, Duration: %.3fs", s.TestCount, s.Failed, s.TotalDuration.Seconds())
}

// Output processes output from tests into StructuredOutput written to w as a string
func (r Structured) Output(w io.Writer, results <-chan []resource.TestResult, outConfig util.OutputConfig) (exitCode int) {
	includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)

	sort := util.IsValueInList(foSort, outConfig.FormatOptions)
	results = getResults(results, sort)

	result := &StructuredOutput{
		Results: []StructuredTestResult{},
		Summary: StructureTestSummary{},
	}

	var startTime time.Time
	var endTime time.Time
	for resultGroup := range results {
		for _, testResult := range resultGroup {
			if startTime.IsZero() || testResult.StartTime.Before(startTime) {
				startTime = testResult.StartTime
			}
			if endTime.IsZero() || testResult.EndTime.After(endTime) {
				endTime = testResult.EndTime
			}
			r := StructuredTestResult{
				TestResult:         testResult,
				SummaryLine:        humanizeResult(testResult, false, includeRaw),
				SummaryLineCompact: humanizeResult(testResult, true, includeRaw),
			}

			if testResult.Result == resource.FAIL {
				result.Summary.Failed++
			}

			result.Summary.TestCount++

			result.Results = append(result.Results, r)
		}
	}

	result.Summary.TotalDuration = endTime.Sub(startTime)
	result.SummaryLine = result.Summary.String()

	var j []byte

	if util.IsValueInList(foPretty, outConfig.FormatOptions) {
		j, _ = json.MarshalIndent(result, "", "  ")
	} else {
		j, _ = json.Marshal(result)
	}

	fmt.Fprintln(w, string(j))

	return 0
}
goss-0.4.9/outputs/tap.go000066400000000000000000000026521467505051300153500ustar00rootroot00000000000000package outputs

import (
	"fmt"
	"io"
	"strconv"

	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
)

type Tap struct{}

func (r Tap) ValidOptions() []*formatOption {
	return []*formatOption{
		{name: foSort},
	}
}

func (r Tap) Output(w io.Writer, results <-chan []resource.TestResult,
	outConfig util.OutputConfig) (exitCode int) {
	includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions)

	sort := util.IsValueInList(foSort, outConfig.FormatOptions)
	results = getResults(results, sort)

	testCount := 0
	failed := 0

	var summary map[int]string = make(map[int]string)

	for resultGroup := range results {
		for _, testResult := range resultGroup {
			switch testResult.Result {
			case resource.SUCCESS:
				summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n"
			case resource.FAIL:
				summary[testCount] = "not ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n"
				failed++
			case resource.SKIP:
				summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - # SKIP " + humanizeResult(testResult, true, includeRaw) + "\n"
			default:
				panic(fmt.Sprintf("Unexpected Result Code: %v\n", testResult.Result))
			}
			testCount++
		}
	}

	fmt.Fprintf(w, "1..%d\n", testCount)

	for i := 0; i < testCount; i++ {
		fmt.Fprintf(w, "%s", summary[i])
	}

	if failed > 0 {
		return 1
	}

	return 0
}
goss-0.4.9/outputs/traces.go000066400000000000000000000013641467505051300160440ustar00rootroot00000000000000package outputs

import (
	"log"

	"github.com/goss-org/goss/resource"
)

func logTrace(level string, msg string, testResult resource.TestResult, withIntResult bool) {
	if withIntResult {
		log.Printf("[%s] %s: %s => %s (%s %+v %+v) [%.02f] [%d]",
			level,
			msg,
			testResult.ResourceType,
			testResult.ResourceId,
			testResult.Property,
			testResult.MatcherResult.Expected,
			testResult.MatcherResult.Actual,
			testResult.Duration.Seconds(),
			testResult.Result,
		)
	} else {
		log.Printf("[%s] %s: %s => %s (%s %+v %+v) [%.02f]",
			level,
			msg,
			testResult.ResourceType,
			testResult.ResourceId,
			testResult.Property,
			testResult.MatcherResult.Expected,
			testResult.MatcherResult.Actual,
			testResult.Duration.Seconds(),
		)
	}
}
goss-0.4.9/release-build.sh000077500000000000000000000017411467505051300155640ustar00rootroot00000000000000#!/usr/bin/env bash
set -euo pipefail

platform_spec="${1:?"Must supply name of release binary to build e.g. goss-linux-amd64"}"
version_stamp="${TRAVIS_TAG:-"0.0.0"}"

# Split platform_spec into platform/arch segments
IFS='- ' read -r -a segments <<< "${platform_spec}"

os="${segments[0]}"
arch="${segments[1]}"
if [[ "${segments[0]}" == "alpha" ]]; then
  os="${segments[1]}"
  arch="${segments[2]}"
fi

output_dir="release/"
output_fname="goss-${platform_spec}"
if [[ "${os}" == "windows" ]]; then
  output_fname="${output_fname}.exe"
fi
output="${output_dir}/${output_fname}"

GOOS="${os}" GOARCH="${arch}" CGO_ENABLED=0 go build \
  -ldflags "-X github.com/goss-org/goss/util.Version=${version_stamp} -s -w" \
  -o "${output}" \
  github.com/goss-org/goss/cmd/goss

chmod +x "${output}"

function __sha256sum {
  if [[ "$OSTYPE" == "darwin"* ]]; then
    shasum -a 256 "$1"
  else
    sha256sum "$1"
  fi
}

(cd "$output_dir" && __sha256sum "${output_fname}" > "${output_fname}.sha256")
goss-0.4.9/resource/000077500000000000000000000000001467505051300143345ustar00rootroot00000000000000goss-0.4.9/resource/addr.go000066400000000000000000000042101467505051300155720ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"
	"time"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type Addr struct {
	Title        string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta         meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id           string  `json:"-" yaml:"-"`
	Address      string  `json:"address,omitempty" yaml:"address,omitempty"`
	LocalAddress string  `json:"local-address,omitempty" yaml:"local-address,omitempty"`
	Reachable    matcher `json:"reachable" yaml:"reachable"`
	Timeout      int     `json:"timeout" yaml:"timeout"`
	Skip         bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

type idKey struct{}

const (
	AddrResourceKey = "addr"
	AddResourceName = "Addr"
)

func init() {
	registerResource(AddrResourceKey, &Addr{})
}

func (a *Addr) ID() string {
	if a.Address != "" && a.Address != a.id {
		return fmt.Sprintf("%s: %s", a.id, a.Address)
	}
	return a.id
}
func (a *Addr) SetID(id string)  { a.id = id }
func (a *Addr) SetSkip()         { a.Skip = true }
func (a *Addr) TypeKey() string  { return AddrResourceKey }
func (a *Addr) TypeName() string { return AddResourceName }

// FIXME: Can this be refactored?
func (a *Addr) GetTitle() string { return a.Title }
func (a *Addr) GetMeta() meta    { return a.Meta }
func (a *Addr) GetAddress() string {
	if a.Address != "" {
		return a.Address
	}
	return a.id
}

func (a *Addr) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, a.ID())
	skip := a.Skip

	if a.Timeout == 0 {
		a.Timeout = 500
	}

	sysAddr := sys.NewAddr(ctx, a.GetAddress(), sys, util.Config{Timeout: time.Duration(a.Timeout) * time.Millisecond, LocalAddress: a.LocalAddress})

	var results []TestResult
	results = append(results, ValidateValue(a, "reachable", a.Reachable, sysAddr.Reachable, skip))
	return results
}

func NewAddr(sysAddr system.Addr, config util.Config) (*Addr, error) {
	address := sysAddr.Address()
	reachable, err := sysAddr.Reachable()
	a := &Addr{
		id:           address,
		Reachable:    reachable,
		Timeout:      config.TimeOutMilliSeconds(),
		LocalAddress: config.LocalAddress,
	}
	return a, err
}
goss-0.4.9/resource/command.go000066400000000000000000000061641467505051300163100ustar00rootroot00000000000000package resource

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"strings"
	"time"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type Command struct {
	Title      string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta       meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id         string  `json:"-" yaml:"-"`
	Exec       string  `json:"exec,omitempty" yaml:"exec,omitempty"`
	ExitStatus matcher `json:"exit-status" yaml:"exit-status"`
	Stdout     matcher `json:"stdout" yaml:"stdout"`
	Stderr     matcher `json:"stderr" yaml:"stderr"`
	Timeout    int     `json:"timeout" yaml:"timeout"`
	Skip       bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
	CommandResourceKey  = "command"
	CommandResourceName = "Command"
)

func init() {
	registerResource(CommandResourceKey, &Command{})
}

func (c *Command) ID() string       { return c.id }
func (c *Command) SetID(id string)  { c.id = id }
func (c *Command) SetSkip()         { c.Skip = true }
func (c *Command) TypeKey() string  { return CommandResourceKey }
func (c *Command) TypeName() string { return CommandResourceName }

func (c *Command) GetTitle() string { return c.Title }
func (c *Command) GetMeta() meta    { return c.Meta }
func (c *Command) GetExec() string {
	if c.Exec != "" {
		return c.Exec
	}
	return c.id
}

func (c *Command) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, c.ID())
	skip := c.Skip

	if c.Timeout == 0 {
		c.Timeout = 10000
	}

	var results []TestResult
	sysCommand := sys.NewCommand(ctx, c.GetExec(), sys, util.Config{Timeout: time.Duration(c.Timeout) * time.Millisecond})

	cExitStatus := deprecateAtoI(c.ExitStatus, fmt.Sprintf("%s: command.exit-status", c.ID()))
	results = append(results, ValidateValue(c, "exit-status", cExitStatus, sysCommand.ExitStatus, skip))
	if isSet(c.Stdout) {
		results = append(results, ValidateValue(c, "stdout", c.Stdout, sysCommand.Stdout, skip))
	}
	if isSet(c.Stderr) {
		results = append(results, ValidateValue(c, "stderr", c.Stderr, sysCommand.Stderr, skip))
	}
	return results
}

func NewCommand(sysCommand system.Command, config util.Config) (*Command, error) {
	command := sysCommand.Command()
	exitStatus, err := sysCommand.ExitStatus()
	c := &Command{
		id:         command,
		ExitStatus: exitStatus,
		Stdout:     "",
		Stderr:     "",
		Timeout:    config.TimeOutMilliSeconds(),
	}

	if !contains(config.IgnoreList, "stdout") {
		stdout, _ := sysCommand.Stdout()
		outSlice := readerToSlice(stdout)
		if len(outSlice) != 0 {
			c.Stdout = outSlice
		}
	}
	if !contains(config.IgnoreList, "stderr") {
		stderr, _ := sysCommand.Stderr()
		errSlice := readerToSlice(stderr)
		if len(errSlice) != 0 {
			c.Stderr = errSlice
		}
	}

	return c, err
}

func escapePattern(s string) string {
	if strings.HasPrefix(s, "!") || strings.HasPrefix(s, "/") {
		return "\\" + s
	}
	return s
}

func readerToSlice(reader io.Reader) []string {
	scanner := bufio.NewScanner(reader)
	slice := []string{}
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		line = escapePattern(line)
		if line != "" {
			slice = append(slice, line)
		}
	}

	return slice
}
goss-0.4.9/resource/dns.go000066400000000000000000000052231467505051300154510ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type DNS struct {
	Title       string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta        meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id          string  `json:"-" yaml:"-"`
	Resolve     string  `json:"resolve,omitempty" yaml:"resolve,omitempty"`
	Resolveable matcher `json:"resolveable,omitempty" yaml:"resolveable,omitempty"`
	Resolvable  matcher `json:"resolvable" yaml:"resolvable"`
	Addrs       matcher `json:"addrs,omitempty" yaml:"addrs,omitempty"`
	Timeout     int     `json:"timeout" yaml:"timeout"`
	Server      string  `json:"server,omitempty" yaml:"server,omitempty"`
	Skip        bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
	DNSResourceKey  = "dns"
	DNSResourceName = "DNS"
)

func init() {
	registerResource(DNSResourceKey, &DNS{})
}

func (d *DNS) ID() string {
	if d.Resolve != "" && d.Resolve != d.id {
		return fmt.Sprintf("%s: %s", d.id, d.Resolve)
	}
	return d.id
}
func (d *DNS) SetID(id string)  { d.id = id }
func (d *DNS) SetSkip()         { d.Skip = true }
func (d *DNS) TypeKey() string  { return DNSResourceKey }
func (d *DNS) TypeName() string { return DNSResourceName }
func (d *DNS) GetTitle() string { return d.Title }
func (d *DNS) GetMeta() meta    { return d.Meta }
func (d *DNS) GetResolve() string {
	if d.Resolve != "" {
		return d.Resolve
	}
	return d.id
}

func (d *DNS) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, d.ID())
	skip := d.Skip
	if d.Timeout == 0 {
		d.Timeout = 500
	}

	sysDNS := sys.NewDNS(ctx, d.GetResolve(), sys, util.Config{Timeout: time.Duration(d.Timeout) * time.Millisecond, Server: d.Server})

	var results []TestResult
	// Backwards compatibility hack for now
	if d.Resolvable == nil {
		d.Resolvable = d.Resolveable
	}
	results = append(results, ValidateValue(d, "resolvable", d.Resolvable, sysDNS.Resolvable, skip))
	if shouldSkip(results) {
		skip = true
	}
	if d.Addrs != nil {
		results = append(results, ValidateValue(d, "addrs", d.Addrs, sysDNS.Addrs, skip))
	}
	return results
}

func NewDNS(sysDNS system.DNS, config util.Config) (*DNS, error) {
	var host string
	if sysDNS.Qtype() != "" {
		host = strings.Join([]string{sysDNS.Qtype(), sysDNS.Host()}, ":")
	} else {
		host = sysDNS.Host()
	}

	resolvable, err := sysDNS.Resolvable()
	server := sysDNS.Server()

	d := &DNS{
		id:         host,
		Resolvable: resolvable,
		Timeout:    config.TimeOutMilliSeconds(),
		Server:     server,
	}
	if !contains(config.IgnoreList, "addrs") {
		addrs, _ := sysDNS.Addrs()
		d.Addrs = addrs
	}
	return d, err
}
goss-0.4.9/resource/file.go000066400000000000000000000113071467505051300156040ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"
	"os"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type File struct {
	Title    string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta     meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id       string  `json:"-" yaml:"-"`
	Path     string  `json:"path,omitempty" yaml:"path,omitempty"`
	Exists   matcher `json:"exists" yaml:"exists"`
	Mode     matcher `json:"mode,omitempty" yaml:"mode,omitempty"`
	Size     matcher `json:"size,omitempty" yaml:"size,omitempty"`
	Owner    matcher `json:"owner,omitempty" yaml:"owner,omitempty"`
	Uid      matcher `json:"uid,omitempty" yaml:"uid,omitempty"`
	Group    matcher `json:"group,omitempty" yaml:"group,omitempty"`
	Gid      matcher `json:"gid,omitempty" yaml:"gid,omitempty"`
	LinkedTo matcher `json:"linked-to,omitempty" yaml:"linked-to,omitempty"`
	Filetype matcher `json:"filetype,omitempty" yaml:"filetype,omitempty"`
	Contains matcher `json:"contains,omitempty" yaml:"contains,omitempty"`
	Contents matcher `json:"contents" yaml:"contents"`
	Md5      matcher `json:"md5,omitempty" yaml:"md5,omitempty"`
	Sha256   matcher `json:"sha256,omitempty" yaml:"sha256,omitempty"`
	Sha512   matcher `json:"sha512,omitempty" yaml:"sha512,omitempty"`
	Skip     bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
	FileResourceKey  = "file"
	FileResourceName = "File"
)

func init() {
	registerResource(FileResourceKey, &File{})
}

func (f *File) ID() string {
	if f.Path != "" && f.Path != f.id {
		return fmt.Sprintf("%s: %s", f.id, f.Path)
	}
	return f.id
}
func (f *File) SetID(id string)  { f.id = id }
func (f *File) SetSkip()         { f.Skip = true }
func (f *File) TypeKey() string  { return FileResourceKey }
func (f *File) TypeName() string { return FileResourceName }

func (f *File) GetTitle() string { return f.Title }
func (f *File) GetMeta() meta    { return f.Meta }
func (f *File) GetPath() string {
	if f.Path != "" {
		return f.Path
	}
	return f.id
}

func (f *File) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, f.ID())
	skip := f.Skip
	sysFile := sys.NewFile(ctx, f.GetPath(), sys, util.Config{})

	var results []TestResult
	results = append(results, ValidateValue(f, "exists", f.Exists, sysFile.Exists, skip))
	if shouldSkip(results) {
		skip = true
	}
	if f.Mode != nil {
		results = append(results, ValidateValue(f, "mode", f.Mode, sysFile.Mode, skip))
	}
	if f.Owner != nil {
		results = append(results, ValidateValue(f, "owner", f.Owner, sysFile.Owner, skip))
	}
	if f.Uid != nil {
		results = append(results, ValidateValue(f, "uid", f.Uid, sysFile.Uid, skip))
	}
	if f.Group != nil {
		results = append(results, ValidateValue(f, "group", f.Group, sysFile.Group, skip))
	}
	if f.Gid != nil {
		results = append(results, ValidateValue(f, "gid", f.Gid, sysFile.Gid, skip))
	}
	if f.LinkedTo != nil {
		results = append(results, ValidateValue(f, "linkedto", f.LinkedTo, sysFile.LinkedTo, skip))
	}
	if f.Filetype != nil {
		results = append(results, ValidateValue(f, "filetype", f.Filetype, sysFile.Filetype, skip))
	}
	if isSet(f.Contains) {
		fmt.Fprintf(os.Stderr, "DEPRECATION WARNING: file.contains has been renamed to file.contents\n")
		results = append(results, ValidateValue(f, "contains", f.Contains, sysFile.Contents, skip))
	}
	if isSet(f.Contents) {
		results = append(results, ValidateValue(f, "contents", f.Contents, sysFile.Contents, skip))
	}
	if f.Size != nil {
		results = append(results, ValidateValue(f, "size", f.Size, sysFile.Size, skip))
	}
	if f.Md5 != nil {
		results = append(results, ValidateValue(f, "md5", f.Md5, sysFile.Md5, skip))
	}
	if f.Sha256 != nil {
		results = append(results, ValidateValue(f, "sha256", f.Sha256, sysFile.Sha256, skip))
	}
	if f.Sha512 != nil {
		results = append(results, ValidateValue(f, "sha512", f.Sha512, sysFile.Sha512, skip))
	}
	return results
}

func NewFile(sysFile system.File, config util.Config) (*File, error) {
	path := sysFile.Path()
	exists, err := sysFile.Exists()
	if err != nil {
		return nil, err
	}
	f := &File{
		id:       path,
		Exists:   exists,
		Contents: []string{},
	}
	if !contains(config.IgnoreList, "mode") {
		if mode, err := sysFile.Mode(); err == nil {
			f.Mode = mode
		}
	}
	if !contains(config.IgnoreList, "owner") {
		if owner, err := sysFile.Owner(); err == nil {
			f.Owner = owner
		}
	}
	if !contains(config.IgnoreList, "group") {
		if group, err := sysFile.Group(); err == nil {
			f.Group = group
		}
	}
	if !contains(config.IgnoreList, "linked-to") {
		if linkedTo, err := sysFile.LinkedTo(); err == nil {
			f.LinkedTo = linkedTo
		}
	}
	if !contains(config.IgnoreList, "filetype") {
		if filetype, err := sysFile.Filetype(); err == nil {
			f.Filetype = filetype
		}
	}
	return f, nil
}
goss-0.4.9/resource/gomega.go000066400000000000000000000133321467505051300161240ustar00rootroot00000000000000package resource

import (
	"fmt"

	"github.com/goss-org/goss/matchers"
	"github.com/samber/lo"
)

func matcherToGomegaMatcher(matcher any) (matchers.GossMatcher, error) {
	// Default matchers
	switch x := matcher.(type) {
	case string:
		return matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal(x)), nil
	case float64, int:
		return matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("eq", x)), nil
	case bool:
		return matchers.Equal(x), nil
	case []any:
		subMatchers, err := sliceToGomega(x, "")
		if err != nil {
			return nil, err
		}
		var interfaceSlice []any
		for _, d := range subMatchers {
			interfaceSlice = append(interfaceSlice, d)
		}
		return matchers.ContainElements(interfaceSlice...), nil
	}
	if matcher == nil {
		return nil, fmt.Errorf("Syntax Error: Missing required attribute")
	}
	matcherMap, ok := matcher.(map[string]any)
	if !ok {
		return nil, invalidArgSyntaxError("matcher", "map", matcher)
		//panic(fmt.Sprintf("Syntax Error: Unexpected matcher type: %T\n\n", matcher))
	}
	keys := lo.Keys(matcherMap)
	if len(keys) > 1 {
		return nil, fmt.Errorf("Syntax Error: Invalid matcher configuration. At a given nesting level, only one matcher is allowed. Found multiple matchers: %q", keys)
	}
	matchType := keys[0]
	value := matcherMap[matchType]
	switch matchType {
	case "equal":
		return matchers.Equal(value), nil
	case "have-prefix":
		v, isStr := value.(string)
		if !isStr {
			return nil, invalidArgSyntaxError("have-prefix", "string", value)
		}
		return matchers.WithSafeTransform(matchers.ToString{}, matchers.HavePrefix(v)), nil
	case "have-suffix":
		v, isStr := value.(string)
		if !isStr {
			return nil, invalidArgSyntaxError("have-suffix", "string", value)
		}
		return matchers.WithSafeTransform(matchers.ToString{}, matchers.HaveSuffix(v)), nil
	case "match-regexp":
		v, isStr := value.(string)
		if !isStr {
			return nil, invalidArgSyntaxError("match-regexp", "string", value)
		}
		return matchers.WithSafeTransform(matchers.ToString{}, matchers.MatchRegexp(v)), nil
	case "contain-substring":
		v, isStr := value.(string)
		if !isStr {
			return nil, invalidArgSyntaxError("contain-substring", "string", value)

		}
		return matchers.WithSafeTransform(matchers.ToString{}, matchers.ContainSubstring(v)), nil
	case "have-len":
		var v int
		switch val := value.(type) {
		case float64:
			v = int(val)
		case int:
			v = val
		default:
			return nil, invalidArgSyntaxError("have-len", "numeric", value)
		}
		return matchers.HaveLen(v), nil
	case "have-patterns":
		_, isArr := value.([]any)
		if !isArr {
			return nil, invalidArgSyntaxError("have-patterns", "array", value)

		}
		return matchers.WithSafeTransform(matchers.ToString{}, matchers.HavePatterns(value)), nil
	case "have-key":
		subMatcher, err := matcherToGomegaMatcher(value)
		if err != nil {
			return nil, err
		}
		return matchers.HaveKey(subMatcher), nil
	case "contain-element":
		switch value.(type) {
		case map[string]any, string, float64, int:
		default:
			return nil, invalidArgSyntaxError("contain-element", "matcher, string or numeric", value)

		}
		subMatcher, err := matcherToGomegaMatcher(value)
		if err != nil {
			return nil, err
		}
		return matchers.WithSafeTransform(matchers.ToArray{}, matchers.ContainElement(subMatcher)), nil
	case "contain-elements":
		subMatchers, err := sliceToGomega(value, "contains-elements")
		if err != nil {
			return nil, err
		}
		var interfaceSlice []any
		for _, d := range subMatchers {
			interfaceSlice = append(interfaceSlice, d)
		}
		return matchers.WithSafeTransform(matchers.ToArray{}, matchers.ContainElements(interfaceSlice...)), nil
	case "not":
		subMatcher, err := matcherToGomegaMatcher(value)
		if err != nil {
			return nil, err
		}
		return matchers.Not(subMatcher), nil
	case "consist-of":
		subMatchers, err := sliceToGomega(value, "consist-of")
		if err != nil {
			return nil, err
		}
		var interfaceSlice []any
		for _, d := range subMatchers {
			interfaceSlice = append(interfaceSlice, d)
		}
		return matchers.ConsistOf(interfaceSlice...), nil
	case "and":
		subMatchers, err := sliceToGomega(value, "and")
		if err != nil {
			return nil, err
		}
		return matchers.And(subMatchers...), nil
	case "or":
		subMatchers, err := sliceToGomega(value, "or")
		if err != nil {
			return nil, err
		}
		return matchers.Or(subMatchers...), nil
	case "gt", "ge", "lt", "le":
		return matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically(matchType, value)), nil

	case "semver-constraint":
		v, isStr := value.(string)
		if !isStr {
			return nil, invalidArgSyntaxError("semver-constraint", "string", value)

		}
		return matchers.BeSemverConstraint(v), nil
	case "gjson":
		var subMatchers []matchers.GossMatcher
		valueI, ok := value.(map[string]any)
		if !ok {
			return nil, invalidArgSyntaxError("gjson", "map", value)
		}
		for key, val := range valueI {
			subMatcher, err := matcherToGomegaMatcher(val)
			if err != nil {
				return nil, err
			}
			subMatchers = append(subMatchers, matchers.WithSafeTransform(matchers.Gjson{Path: key}, subMatcher))

		}
		return matchers.And(subMatchers...), nil
	default:
		return nil, fmt.Errorf("Syntax Error: Unknown matcher: %s", matchType)

	}
}

func sliceToGomega(value any, name string) ([]matchers.GossMatcher, error) {
	valueI, ok := value.([]any)
	if !ok {
		return nil, invalidArgSyntaxError(name, "array", value)
	}
	var subMatchers []matchers.GossMatcher
	for _, v := range valueI {
		subMatcher, err := matcherToGomegaMatcher(v)
		if err != nil {
			return nil, err
		}
		subMatchers = append(subMatchers, subMatcher)
	}
	return subMatchers, nil
}

func invalidArgSyntaxError(name, expected string, value any) error {
	return fmt.Errorf("Syntax Error: Invalid '%s' argument. Expected %s value, but received: %T: %q", name, expected, value, value)
}
goss-0.4.9/resource/gomega_test.go000066400000000000000000000102271467505051300171630ustar00rootroot00000000000000package resource

import (
	"encoding/json"
	"testing"

	"github.com/goss-org/goss/matchers"
	"github.com/stretchr/testify/assert"
)

var gomegaTests = []struct {
	in              string
	want            any
	useNegateTester bool
}{
	// Default for simple types
	{
		in:   `"foo"`,
		want: matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo")),
	},
	{
		in:   `1`,
		want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("eq", float64(1))),
	},
	{
		in:   `true`,
		want: matchers.Equal(true),
	},
	// Default for Array
	{
		in: `["foo", "bar"]`,
		want: matchers.ContainElements(
			matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo")),
			matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("bar"))),
		useNegateTester: true,
	},

	// Numeric
	// Golang json escapes '>', '<' symbols, so we use 'gt', 'le' instead
	{
		in:   `{"gt": 1}`,
		want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("gt", float64(1))),
	},
	{
		in:   `{"ge": 1}`,
		want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("ge", float64(1))),
	},
	{
		in:   `{"lt": 1}`,
		want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("lt", float64(1))),
	},
	{
		in:   `{"le": 1}`,
		want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("le", float64(1))),
	},

	// String
	{
		in:   `{"have-prefix": "foo"}`,
		want: matchers.WithSafeTransform(matchers.ToString{}, matchers.HavePrefix("foo")),
	},
	{
		in:   `{"have-suffix": "foo"}`,
		want: matchers.WithSafeTransform(matchers.ToString{}, matchers.HaveSuffix("foo")),
	},
	// Regex support is based on golangs regex engine https://golang.org/pkg/regexp/syntax/
	{
		in:   `{"match-regexp": "foo"}`,
		want: matchers.WithSafeTransform(matchers.ToString{}, matchers.MatchRegexp("foo")),
	},

	// Collection
	{
		in:   `{"consist-of": ["foo"]}`,
		want: matchers.ConsistOf(matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo"))),
	},
	{
		in: `{"contain-element": "foo"}`,
		want: matchers.WithSafeTransform(matchers.ToArray{},
			matchers.ContainElement(
				matchers.WithSafeTransform(matchers.ToString{},
					matchers.Equal("foo")))),
	},
	{
		in:   `{"have-len": 3}`,
		want: matchers.HaveLen(3),
	},
	{
		in:   `{"have-key": "foo"}`,
		want: matchers.HaveKey(matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo"))),
	},

	// Negation
	{
		in:   `{"not": "foo"}`,
		want: matchers.Not(matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo"))),
	},
	// Complex logic
	{
		in: `{"and": ["foo", "foo"]}`,
		want: matchers.And(
			matchers.WithSafeTransform(matchers.ToString{},
				matchers.Equal("foo")),
			matchers.WithSafeTransform(matchers.ToString{},
				matchers.Equal("foo")),
		),
		useNegateTester: true,
	},
	{
		in: `{"and": [{"have-prefix": "foo"}, "foo"]}`,
		want: matchers.And(
			matchers.WithSafeTransform(matchers.ToString{},
				matchers.HavePrefix("foo")),
			matchers.WithSafeTransform(matchers.ToString{},
				matchers.Equal("foo")),
		),
		useNegateTester: true,
	},
	{
		in: `{"not": {"have-prefix": "foo"}}`,
		want: matchers.Not(
			matchers.WithSafeTransform(matchers.ToString{},
				matchers.HavePrefix("foo"))),
	},
	{
		in: `{"or": ["foo", "foo"]}`,
		want: matchers.Or(
			matchers.WithSafeTransform(matchers.ToString{},
				matchers.Equal("foo")),
			matchers.WithSafeTransform(matchers.ToString{},
				matchers.Equal("foo"))),
	},
	{
		in: `{"not": {"and": [{"have-prefix": "foo"}]}}`,
		want: matchers.Not(
			matchers.And(
				matchers.WithSafeTransform(matchers.ToString{},
					matchers.HavePrefix("foo")))),
	},

	// Semver Constraint
	{
		in:   `{"semver-constraint": "> 1.0.0"}`,
		want: matchers.BeSemverConstraint("> 1.0.0"),
	},
}

func TestMatcherToGomegaMatcher(t *testing.T) {
	for _, c := range gomegaTests {
		var dat any
		if err := json.Unmarshal([]byte(c.in), &dat); err != nil {
			t.Fatal(err)
		}
		got, err := matcherToGomegaMatcher(dat)
		if err != nil {
			t.Fatal(err)
		}
		gomegaTestEqual(t, got, c.want, c.useNegateTester, c.in)
	}
}

func gomegaTestEqual(t *testing.T, got, want any, useNegateTester bool, in string) {
	assert.Equal(t, got, want)
}
goss-0.4.9/resource/gossfile.go000066400000000000000000000025021467505051300164750ustar00rootroot00000000000000package resource

import (
	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type Gossfile struct {
	Title string `json:"title,omitempty" yaml:"title,omitempty"`
	Meta  meta   `json:"meta,omitempty" yaml:"meta,omitempty"`
	Path  string `json:"-" yaml:"-"`
	Skip  bool   `json:"skip,omitempty" yaml:"skip,omitempty"`
	File  string `json:"file,omitempty" yaml:"file,omitempty"`
}

const (
	GossFileResourceKey  = "gossfile"
	GossFileResourceName = "Gossfile"
)

func init() {
	registerResource(GossFileResourceKey, &Gossfile{})
}

func (g *Gossfile) ID() string       { return g.Path }
func (g *Gossfile) SetID(id string)  { g.Path = id }
func (g *Gossfile) SetSkip()         {}
func (g *Gossfile) TypeKey() string  { return GossFileResourceKey }
func (g *Gossfile) TypeName() string { return GossFileResourceName }

func (g *Gossfile) GetTitle() string { return g.Title }
func (g *Gossfile) GetMeta() meta    { return g.Meta }

func (g *Gossfile) GetSkip() bool { return g.Skip }

func (g *Gossfile) GetGossfile() string {
	if g.File != "" {
		return g.File
	}
	return g.Path
}

func (g *Gossfile) Validate(sys *system.System) []TestResult {
	return []TestResult{}
}

func NewGossfile(sysGossfile system.Gossfile, config util.Config) (*Gossfile, error) {
	path := sysGossfile.Path()
	return &Gossfile{
		Path: path,
	}, nil
}
goss-0.4.9/resource/group.go000066400000000000000000000041571467505051300160260ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type Group struct {
	Title     string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta      meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id        string  `json:"-" yaml:"-"`
	Groupname string  `json:"groupname,omitempty" yaml:"groupname,omitempty"`
	Exists    matcher `json:"exists" yaml:"exists"`
	GID       matcher `json:"gid,omitempty" yaml:"gid,omitempty"`
	Skip      bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
	GroupResourceKey  = "group"
	GroupResourceName = "Group"
)

func init() {
	registerResource(GroupResourceKey, &Group{})
}

func (g *Group) ID() string {
	if g.Groupname != "" && g.Groupname != g.id {
		return fmt.Sprintf("%s: %s", g.id, g.Groupname)
	}
	return g.id
}
func (g *Group) SetID(id string)  { g.id = id }
func (g *Group) SetSkip()         { g.Skip = true }
func (g *Group) TypeKey() string  { return GroupResourceKey }
func (g *Group) TypeName() string { return GroupResourceName }
func (g *Group) GetTitle() string { return g.Title }
func (g *Group) GetMeta() meta    { return g.Meta }
func (g *Group) GetGroupname() string {
	if g.Groupname != "" {
		return g.Groupname
	}
	return g.id
}

func (g *Group) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, g.ID())
	skip := g.Skip
	sysgroup := sys.NewGroup(ctx, g.GetGroupname(), sys, util.Config{})

	var results []TestResult
	results = append(results, ValidateValue(g, "exists", g.Exists, sysgroup.Exists, skip))
	if shouldSkip(results) {
		skip = true
	}
	if g.GID != nil {
		gGID := deprecateAtoI(g.GID, fmt.Sprintf("%s: group.gid", g.ID()))
		results = append(results, ValidateValue(g, "gid", gGID, sysgroup.GID, skip))
	}
	return results
}

func NewGroup(sysGroup system.Group, config util.Config) (*Group, error) {
	groupname := sysGroup.Groupname()
	exists, _ := sysGroup.Exists()
	g := &Group{
		id:     groupname,
		Exists: exists,
	}
	if !contains(config.IgnoreList, "stderr") {
		if gid, err := sysGroup.GID(); err == nil {
			g.GID = gid
		}
	}
	return g, nil
}
goss-0.4.9/resource/http.go000066400000000000000000000077401467505051300156520ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"
	"time"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type HTTP struct {
	Title             string   `json:"title,omitempty" yaml:"title,omitempty"`
	Meta              meta     `json:"meta,omitempty" yaml:"meta,omitempty"`
	id                string   `json:"-" yaml:"-"`
	URL               string   `json:"url,omitempty" yaml:"url,omitempty"`
	Method            string   `json:"method,omitempty" yaml:"method,omitempty"`
	Status            matcher  `json:"status" yaml:"status"`
	AllowInsecure     bool     `json:"allow-insecure" yaml:"allow-insecure"`
	NoFollowRedirects bool     `json:"no-follow-redirects" yaml:"no-follow-redirects"`
	Timeout           int      `json:"timeout,omitempty" yaml:"timeout,omitempty"`
	RequestHeader     []string `json:"request-headers,omitempty" yaml:"request-headers,omitempty"`
	RequestBody       string   `json:"request-body,omitempty" yaml:"request-body,omitempty"`
	Headers           matcher  `json:"headers,omitempty" yaml:"headers,omitempty"`
	Body              matcher  `json:"body,omitempty" yaml:"body,omitempty"`
	Username          string   `json:"username,omitempty" yaml:"username,omitempty"`
	Password          string   `json:"password,omitempty" yaml:"password,omitempty"`
	CAFile            string   `json:"ca-file,omitempty" yaml:"ca-file,omitempty"`
	CertFile          string   `json:"cert-file,omitempty" yaml:"cert-file,omitempty"`
	KeyFile           string   `json:"key-file,omitempty" yaml:"key-file,omitempty"`
	Skip              bool     `json:"skip,omitempty" yaml:"skip,omitempty"`
	Proxy             string   `json:"proxy,omitempty" yaml:"proxy,omitempty"`
}

const (
	HTTPResourceKey  = "http"
	HTTPResourceName = "HTTP"
)

func init() {
	registerResource(HTTPResourceKey, &HTTP{})
}

func (h *HTTP) ID() string {
	if h.URL != "" && h.URL != h.id {
		return fmt.Sprintf("%s: %s", h.id, h.URL)
	}
	return h.id
}
func (h *HTTP) SetID(id string)  { h.id = id }
func (u *HTTP) SetSkip()         { u.Skip = true }
func (u *HTTP) TypeKey() string  { return HTTPResourceKey }
func (u *HTTP) TypeName() string { return HTTPResourceName }

// FIXME: Can this be refactored?
func (r *HTTP) GetTitle() string { return r.Title }
func (r *HTTP) GetMeta() meta    { return r.Meta }
func (r *HTTP) getURL() string {
	if r.URL != "" {
		return r.URL
	}
	return r.id
}

func (u *HTTP) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, u.ID())
	skip := u.Skip
	if u.Timeout == 0 {
		u.Timeout = 5000
	}
	sysHTTP := sys.NewHTTP(ctx, u.getURL(), sys, util.Config{
		AllowInsecure:     u.AllowInsecure,
		CAFile:            u.CAFile,
		CertFile:          u.CertFile,
		KeyFile:           u.KeyFile,
		NoFollowRedirects: u.NoFollowRedirects,
		Timeout:           time.Duration(u.Timeout) * time.Millisecond, Username: u.Username, Password: u.Password, Proxy: u.Proxy,
		RequestHeader: u.RequestHeader, RequestBody: u.RequestBody, Method: u.Method})
	sysHTTP.SetAllowInsecure(u.AllowInsecure)
	sysHTTP.SetNoFollowRedirects(u.NoFollowRedirects)

	var results []TestResult
	results = append(results, ValidateValue(u, "status", u.Status, sysHTTP.Status, skip))
	if shouldSkip(results) {
		skip = true
	}
	if isSet(u.Headers) {
		results = append(results, ValidateValue(u, "Headers", u.Headers, sysHTTP.Headers, skip))
	}
	if isSet(u.Body) {
		results = append(results, ValidateValue(u, "Body", u.Body, sysHTTP.Body, skip))
	}

	return results
}

func NewHTTP(sysHTTP system.HTTP, config util.Config) (*HTTP, error) {
	http := sysHTTP.HTTP()
	status, err := sysHTTP.Status()
	u := &HTTP{
		id:                http,
		Status:            status,
		RequestHeader:     []string{},
		Headers:           nil,
		Body:              []string{},
		AllowInsecure:     config.AllowInsecure,
		NoFollowRedirects: config.NoFollowRedirects,
		Timeout:           config.TimeOutMilliSeconds(),
		Username:          config.Username,
		Password:          config.Password,
		Proxy:             config.Proxy,
	}
	return u, err
}
goss-0.4.9/resource/interface.go000066400000000000000000000046401467505051300166270ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type Interface struct {
	Title  string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta   meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id     string  `json:"-" yaml:"-"`
	Name   string  `json:"name,omitempty" yaml:"name,omitempty"`
	Exists matcher `json:"exists" yaml:"exists"`
	Addrs  matcher `json:"addrs,omitempty" yaml:"addrs,omitempty"`
	MTU    matcher `json:"mtu,omitempty" yaml:"mtu,omitempty"`
	Skip   bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
	InterfaceResourceKey  = "interface"
	InterfaceResourceName = "Interface"
)

func init() {
	registerResource(InterfaceResourceKey, &Interface{})
}

func (i *Interface) ID() string {
	if i.Name != "" && i.Name != i.id {
		return fmt.Sprintf("%s: %s", i.id, i.Name)
	}
	return i.id
}
func (i *Interface) SetID(id string)  { i.id = id }
func (i *Interface) SetSkip()         { i.Skip = true }
func (i *Interface) TypeKey() string  { return InterfaceResourceKey }
func (i *Interface) TypeName() string { return InterfaceResourceName }

// FIXME: Can this be refactored?
func (i *Interface) GetTitle() string { return i.Title }
func (i *Interface) GetMeta() meta    { return i.Meta }
func (i *Interface) GetName() string {
	if i.Name != "" {
		return i.Name
	}
	return i.id
}

func (i *Interface) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, i.ID())
	skip := i.Skip
	sysInterface := sys.NewInterface(ctx, i.GetName(), sys, util.Config{})

	var results []TestResult
	results = append(results, ValidateValue(i, "exists", i.Exists, sysInterface.Exists, skip))
	if shouldSkip(results) {
		skip = true
	}
	if i.Addrs != nil {
		results = append(results, ValidateValue(i, "addrs", i.Addrs, sysInterface.Addrs, skip))
	}
	if i.MTU != nil {
		results = append(results, ValidateValue(i, "mtu", i.MTU, sysInterface.MTU, skip))
	}
	return results
}

func NewInterface(sysInterface system.Interface, config util.Config) (*Interface, error) {
	name := sysInterface.Name()
	exists, _ := sysInterface.Exists()
	i := &Interface{
		id:     name,
		Exists: exists,
	}
	if !contains(config.IgnoreList, "addrs") {
		if addrs, err := sysInterface.Addrs(); err == nil {
			i.Addrs = addrs
		}
	}
	if !contains(config.IgnoreList, "mtu") {
		if mtu, err := sysInterface.MTU(); err == nil {
			i.MTU = mtu
		}
	}
	return i, nil
}
goss-0.4.9/resource/kernel_param.go000066400000000000000000000035721467505051300173320ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type KernelParam struct {
	Title string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta  meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id    string  `json:"-" yaml:"-"`
	Name  string  `json:"name,omitempty" yaml:"name,omitempty"`
	Key   string  `json:"-" yaml:"-"`
	Value matcher `json:"value" yaml:"value"`
	Skip  bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
	KernelParamResourceKey  = "kernel-param"
	KernelParamResourceName = "KernelParam"
)

func init() {
	registerResource(KernelParamResourceKey, &KernelParam{})
}

func (k *KernelParam) ID() string {
	if k.Name != "" && k.Name != k.id {
		return fmt.Sprintf("%s: %s", k.id, k.Name)
	}
	return k.id
}
func (a *KernelParam) SetID(id string) { a.id = id }

func (a *KernelParam) SetSkip()         { a.Skip = true }
func (a *KernelParam) TypeKey() string  { return KernelParamResourceKey }
func (a *KernelParam) TypeName() string { return KernelParamResourceName }

// FIXME: Can this be refactored?
func (k *KernelParam) GetTitle() string { return k.Title }
func (k *KernelParam) GetMeta() meta    { return k.Meta }
func (k *KernelParam) GetName() string {
	if k.Name != "" {
		return k.Name
	}
	return k.id
}

func (k *KernelParam) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, k.ID())
	skip := k.Skip
	sysKernelParam := sys.NewKernelParam(ctx, k.GetName(), sys, util.Config{})

	var results []TestResult
	results = append(results, ValidateValue(k, "value", k.Value, sysKernelParam.Value, skip))
	return results
}

func NewKernelParam(sysKernelParam system.KernelParam, config util.Config) (*KernelParam, error) {
	key := sysKernelParam.Key()
	value, err := sysKernelParam.Value()
	a := &KernelParam{
		id:    key,
		Value: value,
	}
	return a, err
}
goss-0.4.9/resource/matching.go000066400000000000000000000057671467505051300164740ustar00rootroot00000000000000package resource

import (
	"encoding/json"
	"fmt"
	"io"
	"reflect"
	"strings"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type Matching struct {
	Title    string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta     meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	Content  any     `json:"content,omitempty" yaml:"content,omitempty"`
	AsReader bool    `json:"as-reader,omitempty" yaml:"as-reader,omitempty"`
	id       string  `json:"-" yaml:"-"`
	Matches  matcher `json:"matches" yaml:"matches"`
	Skip     bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
	MatchingResourceKey  = "mount"
	MatchingResourceName = "Mount"
)

type MatchingMap map[string]*Matching

func (a *Matching) ID() string       { return a.id }
func (a *Matching) SetID(id string)  { a.id = id }
func (a *Matching) SetSkip()         {}
func (a *Matching) TypeKey() string  { return MatchingResourceKey }
func (a *Matching) TypeName() string { return MatchingResourceName }

// FIXME: Can this be refactored?
func (r *Matching) GetTitle() string { return r.Title }
func (r *Matching) GetMeta() meta    { return r.Meta }

func (a *Matching) Validate(sys *system.System) []TestResult {
	skip := false
	if a.Skip {
		skip = true
	}

	var stub interface{}
	if a.AsReader {
		s := fmt.Sprintf("%v", a.Content)
		// ValidateValue expects a function
		stub = func() (io.Reader, error) {
			return strings.NewReader(s), nil
		}
	} else {
		// ValidateValue expects a function
		stub = func() (any, error) {
			return a.Content, nil
		}
	}

	var results []TestResult
	results = append(results, ValidateValue(a, "matches", a.Matches, stub, skip))
	return results
}

func (ret *MatchingMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i any) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := Matching{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Matching
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *MatchingMap) UnmarshalYAML(unmarshal func(v any) error) error {
	// Validate configuration
	zero := Matching{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Matching
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}
goss-0.4.9/resource/mount.go000066400000000000000000000066741467505051300160420ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"
	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
	"time"
)

type Mount struct {
	Title      string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta       meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id         string  `json:"-" yaml:"-"`
	MountPoint string  `json:"mountpoint,omitempty" yaml:"mountpoint,omitempty"`
	Exists     matcher `json:"exists" yaml:"exists"`
	Opts       matcher `json:"opts,omitempty" yaml:"opts,omitempty"`
	VfsOpts    matcher `json:"vfs-opts,omitempty" yaml:"vfs-opts,omitempty"`
	Source     matcher `json:"source,omitempty" yaml:"source,omitempty"`
	Filesystem matcher `json:"filesystem,omitempty" yaml:"filesystem,omitempty"`
	Timeout    int     `json:"timeout" yaml:"timeout"`
	Skip       bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
	Usage      matcher `json:"usage,omitempty" yaml:"usage,omitempty"`
}

const (
	MountResourceKey  = "mount"
	MountResourceName = "Mount"
)

func init() {
	registerResource(MountResourceKey, &Mount{})
}

func (m *Mount) ID() string {
	if m.MountPoint != "" && m.MountPoint != m.id {
		return fmt.Sprintf("%s: %s", m.id, m.MountPoint)
	}
	return m.id
}
func (m *Mount) SetID(id string)  { m.id = id }
func (m *Mount) SetSkip()         { m.Skip = true }
func (m *Mount) TypeKey() string  { return MountResourceKey }
func (m *Mount) TypeName() string { return MountResourceName }

// FIXME: Can this be refactored?
func (m *Mount) GetTitle() string { return m.Title }
func (m *Mount) GetMeta() meta    { return m.Meta }
func (m *Mount) GetMountPoint() string {
	if m.MountPoint != "" {
		return m.MountPoint
	}
	return m.id
}

func (m *Mount) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, m.ID())
	skip := m.Skip

	if m.Timeout == 0 {
		m.Timeout = 1000
	}

	sysMount := sys.NewMount(ctx, m.GetMountPoint(), sys, util.Config{Timeout: time.Duration(m.Timeout) * time.Millisecond})

	var results []TestResult
	results = append(results, ValidateValue(m, "exists", m.Exists, sysMount.Exists, skip))
	if shouldSkip(results) {
		skip = true
	}
	if m.Opts != nil {
		results = append(results, ValidateValue(m, "opts", m.Opts, sysMount.Opts, skip))
	}
	if m.VfsOpts != nil {
		results = append(results, ValidateValue(m, "vfs-opts", m.VfsOpts, sysMount.VfsOpts, skip))
	}
	if m.Source != nil {
		results = append(results, ValidateValue(m, "source", m.Source, sysMount.Source, skip))
	}
	if m.Filesystem != nil {
		results = append(results, ValidateValue(m, "filesystem", m.Filesystem, sysMount.Filesystem, skip))
	}
	if m.Usage != nil {
		results = append(results, ValidateValue(m, "usage", m.Usage, sysMount.Usage, skip))
	}
	return results
}

func NewMount(sysMount system.Mount, config util.Config) (*Mount, error) {
	mountPoint := sysMount.MountPoint()
	exists, _ := sysMount.Exists()
	m := &Mount{
		id:      mountPoint,
		Exists:  exists,
		Timeout: config.TimeOutMilliSeconds(),
	}
	if !contains(config.IgnoreList, "opts") {
		if opts, err := sysMount.Opts(); err == nil {
			m.Opts = opts
		}
	}
	if !contains(config.IgnoreList, "vfs-opts") {
		if vfsOpts, err := sysMount.VfsOpts(); err == nil {
			m.VfsOpts = vfsOpts
		}
	}
	if !contains(config.IgnoreList, "source") {
		if source, err := sysMount.Source(); err == nil {
			m.Source = source
		}
	}
	if !contains(config.IgnoreList, "filesystem") {
		if filesystem, err := sysMount.Filesystem(); err == nil {
			m.Filesystem = filesystem
		}
	}
	return m, nil
}
goss-0.4.9/resource/package.go000066400000000000000000000041601467505051300162570ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type Package struct {
	Title     string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta      meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id        string  `json:"-" yaml:"-"`
	Name      string  `json:"name,omitempty" yaml:"name,omitempty"`
	Installed matcher `json:"installed" yaml:"installed"`
	Versions  matcher `json:"versions,omitempty" yaml:"versions,omitempty"`
	Skip      bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
	PackageResourceKey  = "package"
	PackageResourceName = "Package"
)

func init() {
	registerResource(PackageResourceKey, &Package{})
}

func (p *Package) ID() string {
	if p.Name != "" && p.Name != p.id {
		return fmt.Sprintf("%s: %s", p.id, p.Name)
	}
	return p.id
}
func (p *Package) SetID(id string)  { p.id = id }
func (p *Package) SetSkip()         { p.Skip = true }
func (p *Package) TypeKey() string  { return PackageResourceKey }
func (p *Package) TypeName() string { return PackageResourceName }
func (p *Package) GetTitle() string { return p.Title }
func (p *Package) GetMeta() meta    { return p.Meta }
func (p *Package) GetName() string {
	if p.Name != "" {
		return p.Name
	}
	return p.id
}

func (p *Package) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, p.ID())
	skip := p.Skip
	sysPkg := sys.NewPackage(ctx, p.GetName(), sys, util.Config{})

	var results []TestResult
	results = append(results, ValidateValue(p, "installed", p.Installed, sysPkg.Installed, skip))
	if shouldSkip(results) {
		skip = true
	}
	if p.Versions != nil {
		results = append(results, ValidateValue(p, "version", p.Versions, sysPkg.Versions, skip))
	}
	return results
}

func NewPackage(sysPackage system.Package, config util.Config) (*Package, error) {
	name := sysPackage.Name()
	installed, _ := sysPackage.Installed()
	p := &Package{
		id:        name,
		Installed: installed,
	}
	if !contains(config.IgnoreList, "versions") {
		if versions, err := sysPackage.Versions(); err == nil {
			p.Versions = versions
		}
	}
	return p, nil
}
goss-0.4.9/resource/port.go000066400000000000000000000037411467505051300156540ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type Port struct {
	Title     string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta      meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id        string  `json:"-" yaml:"-"`
	Port      string  `json:"port,omitempty" yaml:"port,omitempty"`
	Listening matcher `json:"listening" yaml:"listening"`
	IP        matcher `json:"ip,omitempty" yaml:"ip,omitempty"`
	Skip      bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
	PortResourceKey  = "port"
	PortResourceName = "Port"
)

func init() {
	registerResource(PortResourceKey, &Port{})
}

func (p *Port) ID() string {
	if p.Port != "" && p.Port != p.id {
		return fmt.Sprintf("%s: %s", p.id, p.Port)
	}
	return p.id
}
func (p *Port) SetID(id string)  { p.id = id }
func (p *Port) SetSkip()         { p.Skip = true }
func (p *Port) TypeKey() string  { return PortResourceKey }
func (p *Port) TypeName() string { return PortResourceName }
func (p *Port) GetTitle() string { return p.Title }
func (p *Port) GetMeta() meta    { return p.Meta }
func (p *Port) GetPort() string {
	if p.Port != "" {
		return p.Port
	}
	return p.id
}

func (p *Port) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, p.ID())
	skip := p.Skip
	sysPort := sys.NewPort(ctx, p.GetPort(), sys, util.Config{})

	var results []TestResult
	results = append(results, ValidateValue(p, "listening", p.Listening, sysPort.Listening, skip))
	if shouldSkip(results) {
		skip = true
	}
	if p.IP != nil {
		results = append(results, ValidateValue(p, "ip", p.IP, sysPort.IP, skip))
	}
	return results
}

func NewPort(sysPort system.Port, config util.Config) (*Port, error) {
	port := sysPort.Port()
	listening, _ := sysPort.Listening()
	p := &Port{
		id:        port,
		Listening: listening,
	}
	if !contains(config.IgnoreList, "ip") {
		if ip, err := sysPort.IP(); err == nil {
			p.IP = ip
		}
	}
	return p, nil
}
goss-0.4.9/resource/process.go000066400000000000000000000034261467505051300163460ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type Process struct {
	Title   string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta    meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id      string  `json:"-" yaml:"-"`
	Comm    string  `json:"comm,omitempty" yaml:"comm,omitempty"`
	Running matcher `json:"running" yaml:"running"`
	Skip    bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
	ProcessResourceKey  = "process"
	ProcessResourceName = "Process"
)

func init() {
	registerResource(ProcessResourceKey, &Process{})
}

func (p *Process) ID() string {
	if p.Comm != "" && p.Comm != p.id {
		return fmt.Sprintf("%s: %s", p.id, p.Comm)
	}
	return p.id
}
func (p *Process) SetID(id string)  { p.id = id }
func (p *Process) SetSkip()         { p.Skip = true }
func (p *Process) TypeKey() string  { return ProcessResourceKey }
func (p *Process) TypeName() string { return ProcessResourceName }
func (p *Process) GetTitle() string { return p.Title }
func (p *Process) GetMeta() meta    { return p.Meta }
func (p *Process) GetComm() string {
	if p.Comm != "" {
		return p.Comm
	}
	return p.id
}

func (p *Process) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, p.ID())
	skip := p.Skip
	sysProcess := sys.NewProcess(ctx, p.GetComm(), sys, util.Config{})

	var results []TestResult
	results = append(results, ValidateValue(p, "running", p.Running, sysProcess.Running, skip))
	return results
}

func NewProcess(sysProcess system.Process, config util.Config) (*Process, error) {
	executable := sysProcess.Executable()
	running, err := sysProcess.Running()
	if err != nil {
		return nil, err
	}
	return &Process{
		id:      executable,
		Running: running,
	}, nil
}
goss-0.4.9/resource/resource.go000066400000000000000000000025741467505051300165220ustar00rootroot00000000000000package resource

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"sync"

	"github.com/goss-org/goss/system"
)

type Resource interface {
	Validate(sys *system.System) []TestResult
	SetID(string)
	SetSkip()
	TypeKey() string
	TypeName() string
}

var (
	resourcesMu sync.Mutex
	resources   = map[string]Resource{}
)

func registerResource(key string, resource Resource) {
	resourcesMu.Lock()
	resources[key] = resource
	resourcesMu.Unlock()
}

func Resources() map[string]Resource {
	return resources
}

type ResourceRead interface {
	ID() string
	GetTitle() string
	GetMeta() meta
}

type matcher any
type meta map[string]any

func contains(a []string, s string) bool {
	for _, e := range a {
		if m, _ := filepath.Match(e, s); m {
			return true
		}
	}
	return false
}

func deprecateAtoI(depr any, desc string) any {
	s, ok := depr.(string)
	if !ok {
		return depr
	}
	fmt.Fprintf(os.Stderr, "DEPRECATION WARNING: %s should be an integer not a string\n", desc)
	i, err := strconv.Atoi(s)
	if err != nil {
		panic(err)
	}
	return float64(i)
}

func shouldSkip(results []TestResult) bool {
	if len(results) < 1 {
		return false
	}
	if results[0].Err != nil || results[0].Result != SUCCESS || results[0].MatcherResult.Actual == false {
		return true
	}
	return false
}

func isSet(i interface{}) bool {
	switch v := i.(type) {
	case []interface{}:
		return len(v) > 0
	default:
		return i != nil
	}
}
goss-0.4.9/resource/resource_list.go000066400000000000000000001065121467505051300175520ustar00rootroot00000000000000// This file was automatically generated by genny.
// Any changes will be lost if this file is regenerated.
// see https://github.com/cheekybits/genny

package resource

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"
	"strings"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type AddrMap map[string]*Addr

func (r AddrMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Addr, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewAddr(ctx, sr, sys, config)
	res, err := NewAddr(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r AddrMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Addr, system.Addr, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewAddr(ctx, sr, sys, util.Config{})
	res, err := NewAddr(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *AddrMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := Addr{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Addr
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *AddrMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := Addr{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Addr
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type CommandMap map[string]*Command

func (r CommandMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Command, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewCommand(ctx, sr, sys, config)
	res, err := NewCommand(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r CommandMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Command, system.Command, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewCommand(ctx, sr, sys, util.Config{})
	res, err := NewCommand(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *CommandMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := Command{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Command
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *CommandMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := Command{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Command
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type DNSMap map[string]*DNS

func (r DNSMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*DNS, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewDNS(ctx, sr, sys, config)
	res, err := NewDNS(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r DNSMap) AppendSysResourceIfExists(sr string, sys *system.System) (*DNS, system.DNS, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewDNS(ctx, sr, sys, util.Config{})
	res, err := NewDNS(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *DNSMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := DNS{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*DNS
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *DNSMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := DNS{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*DNS
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type FileMap map[string]*File

func (r FileMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*File, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewFile(ctx, sr, sys, config)
	res, err := NewFile(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r FileMap) AppendSysResourceIfExists(sr string, sys *system.System) (*File, system.File, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewFile(ctx, sr, sys, util.Config{})
	res, err := NewFile(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *FileMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := File{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*File
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *FileMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := File{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*File
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type GossfileMap map[string]*Gossfile

func (r GossfileMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Gossfile, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewGossfile(ctx, sr, sys, config)
	res, err := NewGossfile(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r GossfileMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Gossfile, system.Gossfile, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewGossfile(ctx, sr, sys, util.Config{})
	res, err := NewGossfile(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *GossfileMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := Gossfile{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Gossfile
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *GossfileMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := Gossfile{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Gossfile
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type GroupMap map[string]*Group

func (r GroupMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Group, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewGroup(ctx, sr, sys, config)
	res, err := NewGroup(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r GroupMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Group, system.Group, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewGroup(ctx, sr, sys, util.Config{})
	res, err := NewGroup(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *GroupMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := Group{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Group
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *GroupMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := Group{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Group
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type PackageMap map[string]*Package

func (r PackageMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Package, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewPackage(ctx, sr, sys, config)
	res, err := NewPackage(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r PackageMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Package, system.Package, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewPackage(ctx, sr, sys, util.Config{})
	res, err := NewPackage(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *PackageMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := Package{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Package
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *PackageMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := Package{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Package
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type PortMap map[string]*Port

func (r PortMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Port, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewPort(ctx, sr, sys, config)
	res, err := NewPort(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r PortMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Port, system.Port, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewPort(ctx, sr, sys, util.Config{})
	res, err := NewPort(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *PortMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := Port{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Port
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *PortMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := Port{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Port
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type ProcessMap map[string]*Process

func (r ProcessMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Process, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewProcess(ctx, sr, sys, config)
	res, err := NewProcess(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r ProcessMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Process, system.Process, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewProcess(ctx, sr, sys, util.Config{})
	res, err := NewProcess(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *ProcessMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := Process{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Process
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *ProcessMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := Process{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Process
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type ServiceMap map[string]*Service

func (r ServiceMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Service, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewService(ctx, sr, sys, config)
	res, err := NewService(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r ServiceMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Service, system.Service, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewService(ctx, sr, sys, util.Config{})
	res, err := NewService(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *ServiceMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := Service{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Service
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *ServiceMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := Service{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Service
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type UserMap map[string]*User

func (r UserMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*User, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewUser(ctx, sr, sys, config)
	res, err := NewUser(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r UserMap) AppendSysResourceIfExists(sr string, sys *system.System) (*User, system.User, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewUser(ctx, sr, sys, util.Config{})
	res, err := NewUser(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *UserMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := User{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*User
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *UserMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := User{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*User
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type KernelParamMap map[string]*KernelParam

func (r KernelParamMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*KernelParam, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewKernelParam(ctx, sr, sys, config)
	res, err := NewKernelParam(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r KernelParamMap) AppendSysResourceIfExists(sr string, sys *system.System) (*KernelParam, system.KernelParam, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewKernelParam(ctx, sr, sys, util.Config{})
	res, err := NewKernelParam(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *KernelParamMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := KernelParam{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*KernelParam
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *KernelParamMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := KernelParam{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*KernelParam
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type MountMap map[string]*Mount

func (r MountMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Mount, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewMount(ctx, sr, sys, config)
	res, err := NewMount(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r MountMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Mount, system.Mount, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewMount(ctx, sr, sys, util.Config{})
	res, err := NewMount(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *MountMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := Mount{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Mount
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *MountMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := Mount{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Mount
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type InterfaceMap map[string]*Interface

func (r InterfaceMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Interface, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewInterface(ctx, sr, sys, config)
	res, err := NewInterface(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r InterfaceMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Interface, system.Interface, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewInterface(ctx, sr, sys, util.Config{})
	res, err := NewInterface(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *InterfaceMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := Interface{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Interface
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *InterfaceMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := Interface{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*Interface
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

type HTTPMap map[string]*HTTP

func (r HTTPMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*HTTP, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewHTTP(ctx, sr, sys, config)
	res, err := NewHTTP(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r HTTPMap) AppendSysResourceIfExists(sr string, sys *system.System) (*HTTP, system.HTTP, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewHTTP(ctx, sr, sys, util.Config{})
	res, err := NewHTTP(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *HTTPMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := HTTP{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*HTTP
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *HTTPMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := HTTP{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*HTTP
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}
goss-0.4.9/resource/resource_list_genny.go000066400000000000000000000061611467505051300207510ustar00rootroot00000000000000//go:build genny
// +build genny

package resource

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"
	"strings"

	"github.com/cheekybits/genny/generic"
	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

//go:generate genny -in=$GOFILE -out=resource_list.go gen "ResourceType=Addr,Command,DNS,File,Gossfile,Group,Package,Port,Process,Service,User,KernelParam,Mount,Interface,HTTP"
//go:generate sed -i -e "/^\\/\\/ +build genny/d" resource_list.go
//go:generate sed -i -e "/^\\/\\/go:.*/d" resource_list.go
//go:generate sed -i -e "s/aelsabbahy/goss-org/" resource_list.go
//go:generate goimports -w resource_list.go resource_list.go

type ResourceType generic.Type

type ResourceTypeMap map[string]*ResourceType

func (r ResourceTypeMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*ResourceType, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewResourceType(ctx, sr, sys, config)
	res, err := NewResourceType(sysres, config)
	if err != nil {
		return nil, err
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, nil
}

func (r ResourceTypeMap) AppendSysResourceIfExists(sr string, sys *system.System) (*ResourceType, system.ResourceType, bool, error) {
	ctx := context.WithValue(context.Background(), idKey{}, sr)
	sysres := sys.NewResourceType(ctx, sr, sys, util.Config{})
	res, err := NewResourceType(sysres, util.Config{})
	if err != nil {
		return nil, nil, false, err
	}
	if e, _ := sysres.Exists(); !e {
		return res, sysres, false, nil
	}
	if old_res, ok := r[res.ID()]; ok {
		res.Title = old_res.Title
		res.Meta = old_res.Meta
	}
	r[res.ID()] = res
	return res, sysres, true, nil
}

func (ret *ResourceTypeMap) UnmarshalJSON(data []byte) error {
	// Curried json.Unmarshal
	unmarshal := func(i interface{}) error {
		if err := json.Unmarshal(data, i); err != nil {
			return err
		}
		return nil
	}

	// Validate configuration
	zero := ResourceType{}
	whitelist, err := util.WhitelistAttrs(zero, util.JSON)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*ResourceType
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}

func (ret *ResourceTypeMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
	// Validate configuration
	zero := ResourceType{}
	whitelist, err := util.WhitelistAttrs(zero, util.YAML)
	if err != nil {
		return err
	}
	if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil {
		return err
	}

	var tmp map[string]*ResourceType
	if err := unmarshal(&tmp); err != nil {
		return err
	}

	typ := reflect.TypeOf(zero)
	typs := strings.Split(typ.String(), ".")[1]
	for id, res := range tmp {
		if res == nil {
			return fmt.Errorf("Could not parse resource %s:%s", typs, id)
		}
		res.SetID(id)
	}

	*ret = tmp
	return nil
}
goss-0.4.9/resource/service.go000066400000000000000000000044121467505051300163240ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type Service struct {
	Title     string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta      meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id        string  `json:"-" yaml:"-"`
	Name      string  `json:"name,omitempty" yaml:"name,omitempty"`
	Enabled   matcher `json:"enabled" yaml:"enabled"`
	Running   matcher `json:"running" yaml:"running"`
	Skip      bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
	RunLevels matcher `json:"runlevels,omitempty" yaml:"runlevels,omitempty"`
}

const (
	ServiceResourceKey  = "service"
	ServiceResourceName = "Service"
)

func init() {
	registerResource(ServiceResourceKey, &Service{})
}

func (s *Service) ID() string {
	if s.Name != "" && s.Name != s.id {
		return fmt.Sprintf("%s: %s", s.id, s.Name)
	}
	return s.id
}
func (s *Service) SetID(id string)  { s.id = id }
func (s *Service) SetSkip()         { s.Skip = true }
func (s *Service) TypeKey() string  { return ServiceResourceKey }
func (s *Service) TypeName() string { return ServiceResourceName }
func (s *Service) GetTitle() string { return s.Title }
func (s *Service) GetMeta() meta    { return s.Meta }
func (s *Service) GetName() string {
	if s.Name != "" {
		return s.Name
	}
	return s.id
}

func (s *Service) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, s.ID())
	skip := s.Skip
	sysservice := sys.NewService(ctx, s.GetName(), sys, util.Config{})

	var results []TestResult
	if s.Enabled != nil {
		results = append(results, ValidateValue(s, "enabled", s.Enabled, sysservice.Enabled, skip))
	}
	if s.Running != nil {
		results = append(results, ValidateValue(s, "running", s.Running, sysservice.Running, skip))
	}
	if s.RunLevels != nil {
		results = append(results, ValidateValue(s, "runlevels", s.RunLevels, sysservice.RunLevels, skip))
	}
	return results
}

func NewService(sysService system.Service, config util.Config) (*Service, error) {
	service := sysService.Service()
	enabled, err := sysService.Enabled()
	if err != nil {
		return nil, err
	}
	running, err := sysService.Running()
	if err != nil {
		return nil, err
	}
	return &Service{
		id:      service,
		Enabled: enabled,
		Running: running,
	}, nil
}
goss-0.4.9/resource/user.go000066400000000000000000000064071467505051300156500ustar00rootroot00000000000000package resource

import (
	"context"
	"fmt"

	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

type User struct {
	Title    string  `json:"title,omitempty" yaml:"title,omitempty"`
	Meta     meta    `json:"meta,omitempty" yaml:"meta,omitempty"`
	id       string  `json:"-" yaml:"-"`
	Username string  `json:"username,omitempty" yaml:"username,omitempty"`
	Exists   matcher `json:"exists" yaml:"exists"`
	UID      matcher `json:"uid,omitempty" yaml:"uid,omitempty"`
	GID      matcher `json:"gid,omitempty" yaml:"gid,omitempty"`
	Groups   matcher `json:"groups,omitempty" yaml:"groups,omitempty"`
	Home     matcher `json:"home,omitempty" yaml:"home,omitempty"`
	Shell    matcher `json:"shell,omitempty" yaml:"shell,omitempty"`
	Skip     bool    `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
	UserResourceKey  = "user"
	UserResourceName = "User"
)

func init() {
	registerResource(UserResourceKey, &User{})
}

func (u *User) ID() string {
	if u.Username != "" && u.Username != u.id {
		return fmt.Sprintf("%s: %s", u.id, u.Username)
	}
	return u.id
}
func (u *User) SetID(id string)  { u.id = id }
func (u *User) SetSkip()         { u.Skip = true }
func (u *User) TypeKey() string  { return UserResourceKey }
func (u *User) TypeName() string { return UserResourceName }
func (u *User) GetTitle() string { return u.Title }
func (u *User) GetMeta() meta    { return u.Meta }
func (u *User) GetUsername() string {
	if u.Username != "" {
		return u.Username
	}
	return u.id
}

func (u *User) Validate(sys *system.System) []TestResult {
	ctx := context.WithValue(context.Background(), idKey{}, u.ID())
	skip := u.Skip
	sysuser := sys.NewUser(ctx, u.GetUsername(), sys, util.Config{})

	var results []TestResult
	results = append(results, ValidateValue(u, "exists", u.Exists, sysuser.Exists, skip))
	if shouldSkip(results) {
		skip = true
	}
	if u.UID != nil {
		uUID := deprecateAtoI(u.UID, fmt.Sprintf("%s: user.uid", u.Username))
		results = append(results, ValidateValue(u, "uid", uUID, sysuser.UID, skip))
	}
	if u.GID != nil {
		uGID := deprecateAtoI(u.GID, fmt.Sprintf("%s: user.gid", u.Username))
		results = append(results, ValidateValue(u, "gid", uGID, sysuser.GID, skip))
	}
	if u.Home != nil {
		results = append(results, ValidateValue(u, "home", u.Home, sysuser.Home, skip))
	}
	if u.Groups != nil {
		results = append(results, ValidateValue(u, "groups", u.Groups, sysuser.Groups, skip))
	}
	if u.Shell != nil {
		results = append(results, ValidateValue(u, "shell", u.Shell, sysuser.Shell, skip))
	}
	return results
}

func NewUser(sysUser system.User, config util.Config) (*User, error) {
	username := sysUser.Username()
	exists, _ := sysUser.Exists()
	u := &User{
		id:     username,
		Exists: exists,
	}
	if !contains(config.IgnoreList, "uid") {
		if uid, err := sysUser.UID(); err == nil {
			u.UID = uid
		}
	}
	if !contains(config.IgnoreList, "gid") {
		if gid, err := sysUser.GID(); err == nil {
			u.GID = gid
		}
	}
	if !contains(config.IgnoreList, "groups") {
		if groups, err := sysUser.Groups(); err == nil {
			u.Groups = groups
		}
	}
	if !contains(config.IgnoreList, "home") {
		if home, err := sysUser.Home(); err == nil {
			u.Home = home
		}
	}
	if !contains(config.IgnoreList, "shell") {
		if shell, err := sysUser.Shell(); err == nil {
			u.Shell = shell
		}
	}
	return u, nil
}
goss-0.4.9/resource/validate.go000066400000000000000000000116441467505051300164620ustar00rootroot00000000000000package resource

import (
	"fmt"
	"io"
	"reflect"
	"strings"
	"time"

	"github.com/goss-org/goss/matchers"
)

const (
	Value = iota
	Values
	Contains
)

const (
	SUCCESS = iota
	FAIL
	SKIP
	UNKNOWN
)

const (
	OutcomePass    = "pass"
	OutcomeFail    = "fail"
	OutcomeSkip    = "skip"
	OutcomeUnknown = "unknown"
)

var humanOutcomes map[int]string = map[int]string{
	UNKNOWN: OutcomeUnknown,
	SUCCESS: OutcomePass,
	FAIL:    OutcomeFail,
	SKIP:    OutcomeSkip,
}

func HumanOutcomes() map[int]string {
	return humanOutcomes
}

type ValidateError string

func (g ValidateError) Error() string { return string(g) }
func toValidateError(err error) *ValidateError {
	if err == nil {
		return nil
	}
	ve := ValidateError(err.Error())
	return &ve
}

type TestResult struct {
	Successful bool `json:"successful" yaml:"successful"`
	Skipped    bool `json:"skipped" yaml:"skipped"`
	// Resource data
	ResourceId   string `json:"resource-id" yaml:"resource-id"`
	ResourceType string `json:"resource-type" yaml:"resource-type"`
	Property     string `json:"property" yaml:"property"`

	// User added info
	Title string `json:"title" yaml:"title"`
	Meta  meta   `json:"meta" yaml:"meta"`

	// Result
	Result        int                    `json:"result" yaml:"result"`
	Err           *ValidateError         `json:"err" yaml:"err"`
	MatcherResult matchers.MatcherResult `json:"matcher-result" yaml:"matcher-result"`
	StartTime     time.Time              `json:"start-time" yaml:"start-time"`
	EndTime       time.Time              `json:"end-time" yaml:"end-time"`
	Duration      time.Duration          `json:"duration" yaml:"duration"`
}

// ToOutcome converts the enum to a human-friendly string.
func (tr TestResult) ToOutcome() string {
	switch tr.Result {
	case SUCCESS:
		return OutcomePass
	case FAIL:
		return OutcomeFail
	case SKIP:
		return OutcomeSkip
	default:
		return OutcomeUnknown
	}
}

func (t TestResult) SortKey() string {
	return fmt.Sprintf("%s:%s", t.ResourceType, t.ResourceId)
}

func skipResult(typeS string, id string, title string, meta meta, property string, startTime time.Time) TestResult {
	endTime := time.Now()
	return TestResult{
		Result:       SKIP,
		Skipped:      true,
		ResourceType: typeS,
		ResourceId:   id,
		Title:        title,
		Meta:         meta,
		Property:     property,
		StartTime:    startTime,
		EndTime:      endTime,
		Duration:     endTime.Sub(startTime),
	}
}

func ValidateValue(res ResourceRead, property string, expectedValue any, actual any, skip bool) TestResult {
	if f, ok := actual.(func() (io.Reader, error)); ok {
		if _, ok := expectedValue.([]any); !ok {
			actual = func() (string, error) {
				v, err := f()
				if err != nil {
					return "", err
				}
				i, err := matchers.ReaderToString{}.Transform(v)
				if err != nil {
					return "", err
				}
				return i.(string), nil
			}
		}
	}
	return ValidateGomegaValue(res, property, expectedValue, actual, skip)
}

func ValidateGomegaValue(res ResourceRead, property string, expectedValue any, actual any, skip bool) TestResult {
	id := res.ID()
	title := res.GetTitle()
	meta := res.GetMeta()
	typ := reflect.TypeOf(res)
	typeS := strings.Split(typ.String(), ".")[1]
	startTime := time.Now()
	if skip {
		return skipResult(
			typeS,
			id,
			title,
			meta,
			property,
			startTime,
		)
	}

	var foundValue any
	var gomegaMatcher matchers.GossMatcher
	var err error
	switch f := actual.(type) {
	case func() (bool, error):
		foundValue, err = f()
	case func() (string, error):
		foundValue, err = f()
	case func() (int, error):
		foundValue, err = f()
	case func() ([]string, error):
		foundValue, err = f()
	case func() (any, error):
		foundValue, err = f()
	case func() (io.Reader, error):
		foundValue, err = f()
		gomegaMatcher = matchers.HavePatterns(expectedValue)
	default:
		err = fmt.Errorf("Unknown method signature: %t", f)
	}

	var success bool
	if gomegaMatcher == nil && err == nil {
		gomegaMatcher, err = matcherToGomegaMatcher(expectedValue)
	}
	if err != nil {
		endTime := time.Now()
		return TestResult{
			Result:       FAIL,
			ResourceType: typeS,
			ResourceId:   id,
			Title:        title,
			Meta:         meta,
			Property:     property,
			Err:          toValidateError(err),
			StartTime:    startTime,
			EndTime:      endTime,
			Duration:     endTime.Sub(startTime),
		}
	}

	success, err = gomegaMatcher.Match(foundValue)

	var matcherResult matchers.MatcherResult
	result := SUCCESS
	if success {
		matcherResult = matchers.MatcherResult{
			Actual:   foundValue,
			Message:  "matches expectation",
			Expected: expectedValue,
		}
	} else {
		matcherResult = gomegaMatcher.FailureResult(foundValue)
		result = FAIL
	}

	endTime := time.Now()
	return TestResult{
		Result:        result,
		ResourceType:  typeS,
		ResourceId:    id,
		Title:         title,
		Meta:          meta,
		Property:      property,
		MatcherResult: matcherResult,
		Err:           toValidateError(err),
		StartTime:     startTime,
		EndTime:       endTime,
		Duration:      endTime.Sub(startTime),
	}
}
goss-0.4.9/resource/validate_test.go000066400000000000000000000101071467505051300175120ustar00rootroot00000000000000package resource

import (
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"testing"
)

type FakeResource struct {
	id string
}

func (f *FakeResource) ID() string {
	return f.id
}
func (f *FakeResource) GetTitle() string { return "title" }

func (f *FakeResource) GetMeta() meta { return meta{"foo": "bar"} }

var stringTests = []struct {
	in, in2 any
	want    int
}{
	{"", "", SUCCESS},
	{"foo", "foo", SUCCESS},
	{"foo", "bar", FAIL},
	{"foo", "", FAIL},
	{true, true, SUCCESS},
}

func TestValidateValue(t *testing.T) {
	for _, c := range stringTests {
		inFunc := func() (any, error) {
			return c.in2, nil
		}
		got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false)
		if got.Result != c.want {
			t.Errorf("%+v: got %v, want %v", c, got.Result, c.want)
		}
	}
}

func TestValidateValueErr(t *testing.T) {
	for _, c := range stringTests {
		inFunc := func() (any, error) {
			return c.in2, fmt.Errorf("some err")
		}
		got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false)
		if got.Result != FAIL {
			t.Errorf("%+v: got %v, want %v", c, got.Result, FAIL)
		}
	}
}

func TestValidateValueSkip(t *testing.T) {
	for _, c := range stringTests {
		inFunc := func() (any, error) {
			return c.in2, nil
		}
		got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, true)
		if got.Result != SKIP {
			t.Errorf("%+v: got %v, want %v", c, got.Result, SKIP)
		}
	}
}

func BenchmarkValidateValue(b *testing.B) {
	inFunc := func() (any, error) {
		return "foo", nil
	}
	for n := 0; n < b.N; n++ {
		ValidateValue(&FakeResource{""}, "", "foo", inFunc, false)
	}
}

var containsTests = []struct {
	in   []interface{}
	in2  string
	want int
}{
	{[]interface{}{""}, "", SUCCESS},
	{[]interface{}{"foo"}, "foo\nbar", SUCCESS},
	{[]interface{}{"!foo"}, "foo\nbar", FAIL},
	{[]interface{}{"!moo"}, "foo\nbar", SUCCESS},
	{[]interface{}{"/fo.*/"}, "foo\nbar", SUCCESS},
	{[]interface{}{"!/fo.*/"}, "foo\nbar", FAIL},
	{[]interface{}{"!/mo.*/"}, "foo\nbar", SUCCESS},
	{[]interface{}{"foo"}, "", FAIL},
	{[]interface{}{`/\s/tmp\b/`}, "test /tmp bar", SUCCESS},
}

func TestValidateContains(t *testing.T) {
	for _, c := range containsTests {
		inFunc := func() (io.Reader, error) {
			reader := strings.NewReader(c.in2)
			return reader, nil
		}
		got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false)
		if got.Result != c.want {
			t.Errorf("%+v: got %v, want %v", c, got.Result, c.want)
		}
	}
}

func TestValidateContainsErr(t *testing.T) {
	for _, c := range containsTests {
		inFunc := func() (io.Reader, error) {
			reader := strings.NewReader(c.in2)
			return reader, fmt.Errorf("some err")
		}
		got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false)
		if got.Result != FAIL {
			t.Errorf("%+v: got %v, want %v", c, got.Result, FAIL)
		}
	}
}

func TestValidateContainsBadRegexErr(t *testing.T) {
	inFunc := func() (io.Reader, error) {
		reader := strings.NewReader("dummy")
		return reader, nil
	}
	got := ValidateValue(&FakeResource{""}, "", []interface{}{"/*\\.* @@.*/"}, inFunc, false)
	if got.Err == nil {
		t.Errorf("Expected bad regex to raise error, got nil")
	}
}

func TestValidateContainsSkip(t *testing.T) {
	for _, c := range containsTests {
		inFunc := func() (io.Reader, error) {
			reader := strings.NewReader(c.in2)
			return reader, nil
		}
		got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, true)
		if got.Result != SKIP {
			t.Errorf("%+v: got %v, want %v", c, got.Result, SKIP)
		}
	}
}

func TestResultMarshaling(t *testing.T) {
	inFunc := func() (io.Reader, error) {
		return nil, fmt.Errorf("dummy error")
	}
	res := ValidateValue(&FakeResource{}, "", []string{"x"}, inFunc, false)
	if res.Err == nil {
		t.Fatalf("Expected to receive an error")
	}
	if res.Err.Error() != "dummy error" {
		t.Fatalf("expected to receive 'dummy error', got: %v", res.Err.Error())
	}

	rj, _ := json.Marshal(res)
	res = TestResult{}
	err := json.Unmarshal(rj, &res)
	if err != nil {
		t.Fatalf("could not unmarshal result: %v", err)
	}

	if res.Err == nil {
		t.Fatalf("Expected to receive an error")
	}
	if res.Err.Error() != "dummy error" {
		t.Fatalf("expected to receive 'dummy error', got: %v", res.Err.Error())
	}
}
goss-0.4.9/serve.go000066400000000000000000000117111467505051300141610ustar00rootroot00000000000000package goss

import (
	"bytes"
	"fmt"
	"log"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/fatih/color"
	"github.com/goss-org/goss/outputs"
	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
	"github.com/patrickmn/go-cache"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

func Serve(c *util.Config) error {
	err := setLogLevel(c)
	if err != nil {
		return err
	}
	endpoint := c.Endpoint
	health, err := newHealthHandler(c)
	if err != nil {
		return err
	}
	http.Handle(endpoint, health)
	http.Handle("/metrics", promhttp.Handler())
	log.Printf("[INFO] Starting to listen on: %s", c.ListenAddress)
	return http.ListenAndServe(c.ListenAddress, nil)
}

func newHealthHandler(c *util.Config) (*healthHandler, error) {
	color.NoColor = true
	cache := cache.New(c.Cache, 30*time.Second)

	cfg, err := getGossConfig(c.Vars, c.VarsInline, c.Spec)
	if err != nil {
		return nil, err
	}

	output, err := getOutputer(c.NoColor, c.OutputFormat)
	if err != nil {
		return nil, err
	}

	health := &healthHandler{
		c:             c,
		gossConfig:    *cfg,
		sys:           system.New(c.PackageManager),
		outputer:      output,
		cache:         cache,
		gossMu:        &sync.Mutex{},
		maxConcurrent: c.MaxConcurrent,
	}
	return health, nil
}

type res struct {
	body       bytes.Buffer
	statusCode int
}
type healthHandler struct {
	c             *util.Config
	gossConfig    GossConfig
	sys           *system.System
	outputer      outputs.Outputer
	cache         *cache.Cache
	gossMu        *sync.Mutex
	maxConcurrent int
}

func (h healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	outputFormat, outputer, err := h.negotiateResponseContentType(r)
	if err != nil {
		log.Printf("[DEBUG] Warn: Using process-level output-format. %s", err)
		outputFormat = h.c.OutputFormat
		outputer = h.outputer
	}
	negotiatedContentType := h.responseContentType(outputFormat)

	log.Printf("[TRACE] %v: requesting health probe", r.RemoteAddr)
	resp := h.processAndEnsureCached(negotiatedContentType, outputer)
	w.Header().Set(http.CanonicalHeaderKey("Content-Type"), negotiatedContentType) //nolint:gosimple
	w.WriteHeader(resp.statusCode)
	logBody := ""
	if resp.statusCode != http.StatusOK {
		logBody = " - " + resp.body.String()
	}
	resp.body.WriteTo(w)
	log.Printf("[DEBUG] %v: status %d%s", r.RemoteAddr, resp.statusCode, logBody)
}

func (h healthHandler) processAndEnsureCached(negotiatedContentType string, outputer outputs.Outputer) res {
	var tra [][]resource.TestResult
	cacheKey := "res"
	tmp, found := h.cache.Get(cacheKey)
	if found {
		log.Printf("[TRACE] Returning cached[%s].", cacheKey)
		tra = tmp.([][]resource.TestResult)
	} else {
		log.Printf("Stale cache[%s], running tests", cacheKey)
		h.sys = system.New(h.c.PackageManager)
		tra = h.validate()
		h.cache.SetDefault(cacheKey, tra)
	}
	trc := testResultArrayToChan(tra)
	return h.output(trc, outputer)
}

func (h healthHandler) output(trc <-chan []resource.TestResult, outputer outputs.Outputer) res {
	var b bytes.Buffer
	outputConfig := util.OutputConfig{
		FormatOptions: h.c.FormatOptions,
	}
	exitCode := outputer.Output(&b, trc, outputConfig)
	resp := res{
		body: b,
	}
	if exitCode == 0 {
		resp.statusCode = http.StatusOK
	} else {
		resp.statusCode = http.StatusServiceUnavailable
	}
	return resp
}
func (h healthHandler) validate() [][]resource.TestResult {
	h.sys = system.New(h.c.PackageManager)
	res := make([][]resource.TestResult, 0)
	tr := validate(h.sys, h.gossConfig, h.c.DisabledResourceTypes, h.maxConcurrent)
	for i := range tr {
		res = append(res, i)
	}
	return res
}

func testResultArrayToChan(tra [][]resource.TestResult) <-chan []resource.TestResult {
	c := make(chan []resource.TestResult)
	go func(c chan []resource.TestResult) {
		defer close(c)

		for _, i := range tra {
			c <- i
		}
	}(c)

	return c
}

const (
	// https://en.wikipedia.org/wiki/Media_type
	mediaTypePrefix = "application/vnd.goss-"
)

func (h healthHandler) negotiateResponseContentType(r *http.Request) (string, outputs.Outputer, error) {
	acceptHeader := r.Header[http.CanonicalHeaderKey("Accept")]
	var outputer outputs.Outputer
	outputName := ""
	for _, acceptCandidate := range acceptHeader {
		acceptCandidate = strings.TrimSpace(acceptCandidate)
		if strings.HasPrefix(acceptCandidate, mediaTypePrefix) {
			outputName = strings.TrimPrefix(acceptCandidate, mediaTypePrefix)
		} else if strings.EqualFold("application/json", acceptCandidate) || strings.EqualFold("text/json", acceptCandidate) {
			outputName = "json"
		} else {
			outputName = ""
		}
		var err error
		outputer, err = outputs.GetOutputer(outputName)
		if err != nil {
			continue
		}
	}
	if outputer == nil {
		return "", nil, fmt.Errorf("Accept header on request missing or invalid. Accept header: %v", acceptHeader)
	}

	return outputName, outputer, nil
}

func (h healthHandler) responseContentType(outputName string) string {
	if outputName == "json" {
		return "application/json"
	}
	return fmt.Sprintf("%s%s", mediaTypePrefix, outputName)
}
goss-0.4.9/serve_test.go000066400000000000000000000212571467505051300152260ustar00rootroot00000000000000package goss

import (
	"bytes"
	"log"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"testing"
	"time"

	"github.com/goss-org/goss/util"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestServeWithNoContentNegotiation(t *testing.T) {
	t.Parallel()
	tests := map[string]struct {
		outputFormat        string
		specFile            string
		expectedHTTPStatus  int
		expectedContentType string
	}{
		"passing-json": {
			outputFormat:        "json",
			specFile:            filepath.Join("testdata", "passing.goss.yaml"),
			expectedHTTPStatus:  http.StatusOK,
			expectedContentType: "application/json",
		},
		"failing-json": {
			outputFormat:        "json",
			specFile:            filepath.Join("testdata", "failing.goss.yaml"),
			expectedHTTPStatus:  http.StatusServiceUnavailable,
			expectedContentType: "application/json",
		},
		"failing-default-output": {
			outputFormat:        "rspecish",
			specFile:            filepath.Join("testdata", "failing.goss.yaml"),
			expectedHTTPStatus:  http.StatusServiceUnavailable,
			expectedContentType: "",
		},
	}
	for testName := range tests {
		tc := tests[testName]
		t.Run(testName, func(t *testing.T) {
			var logOutput bytes.Buffer
			log.SetOutput(&logOutput)

			config, err := util.NewConfig(
				util.WithSpecFile(tc.specFile),
				util.WithOutputFormat(tc.outputFormat),
			)
			require.NoError(t, err)

			hh, err := newHealthHandler(config)
			require.NoError(t, err)

			req := makeRequest(t, config, nil)
			rr := httptest.NewRecorder()

			handler := http.HandlerFunc(hh.ServeHTTP)

			handler.ServeHTTP(rr, req)

			t.Logf("testName %q log output:\n%s", testName, logOutput.String())
			assert.Equal(t, tc.expectedHTTPStatus, rr.Code)
			if tc.expectedContentType != "" {
				assert.Equal(t, tc.expectedContentType, rr.Result().Header.Get("Content-Type"))
			}
		})
	}
}

func TestServeNegotiatingContent(t *testing.T) {
	t.Parallel()
	tests := map[string]struct {
		acceptHeader        []string
		outputFormat        string
		specFile            string
		expectedHTTPStatus  int
		expectedContentType string
	}{
		"accept {blank} returns process-level format-option": {
			acceptHeader: []string{
				"",
			},
			outputFormat:        "structured",
			specFile:            filepath.Join("testdata", "passing.goss.yaml"),
			expectedHTTPStatus:  http.StatusOK,
			expectedContentType: "application/vnd.goss-structured",
		},
		"accept application/json": {
			acceptHeader: []string{
				"application/json",
			},
			outputFormat:        "structured",
			specFile:            filepath.Join("testdata", "passing.goss.yaml"),
			expectedHTTPStatus:  http.StatusOK,
			expectedContentType: "application/json",
		},
		"accept text/json translates to application/json": {
			acceptHeader: []string{
				"text/json",
			},
			outputFormat:        "structured",
			specFile:            filepath.Join("testdata", "passing.goss.yaml"),
			expectedHTTPStatus:  http.StatusOK,
			expectedContentType: "application/json",
		},
		"when accept is application/vnd.goss-json, return more widely known application/json": {
			acceptHeader: []string{
				"application/vnd.goss-json",
			},
			outputFormat:        "structured",
			specFile:            filepath.Join("testdata", "passing.goss.yaml"),
			expectedHTTPStatus:  http.StatusOK,
			expectedContentType: "application/json",
		},
		"accept header contains vendor-specific output format different from process-level": {
			acceptHeader: []string{
				"application/vnd.goss-rspecish",
			},
			outputFormat:        "structured",
			specFile:            filepath.Join("testdata", "passing.goss.yaml"),
			expectedHTTPStatus:  http.StatusOK,
			expectedContentType: "application/vnd.goss-rspecish",
		},
		"accept header contains nonsense": {
			acceptHeader: []string{
				"application/vnd.goss-nonexistent",
			},
			outputFormat:        "structured",
			specFile:            filepath.Join("testdata", "passing.goss.yaml"),
			expectedHTTPStatus:  http.StatusOK,
			expectedContentType: "application/vnd.goss-structured",
		},
		"accept header contains nonsense then valid": {
			acceptHeader: []string{
				"application/vnd.goss-nonexistent",
				"application/json",
			},
			outputFormat:        "structured",
			specFile:            filepath.Join("testdata", "passing.goss.yaml"),
			expectedHTTPStatus:  http.StatusOK,
			expectedContentType: "application/json",
		},
	}
	for testName := range tests {
		tc := tests[testName]
		t.Run(testName, func(t *testing.T) {
			var logOutput bytes.Buffer
			log.SetOutput(&logOutput)

			config, err := util.NewConfig(
				util.WithSpecFile(tc.specFile),
				util.WithOutputFormat(tc.outputFormat),
			)
			require.NoError(t, err)

			hh, err := newHealthHandler(config)
			require.NoError(t, err)

			req := makeRequest(t, config, map[string][]string{
				"accept": tc.acceptHeader,
			})
			rr := httptest.NewRecorder()

			handler := http.HandlerFunc(hh.ServeHTTP)

			handler.ServeHTTP(rr, req)

			t.Logf("testName %q log output:\n%s", testName, logOutput.String())
			assert.Equal(t, tc.expectedHTTPStatus, rr.Code)
			if tc.expectedContentType != "" {
				assert.Equal(t, tc.expectedContentType, rr.Result().Header.Get("Content-Type"))
			}
		})
	}
}

func TestServeCacheWithNoContentNegotiation(t *testing.T) {
	var logOutput bytes.Buffer
	log.SetOutput(&logOutput)
	const cache = time.Duration(time.Millisecond * 100)
	config, err := util.NewConfig(
		util.WithSpecFile(filepath.Join("testdata", "passing.goss.yaml")),
		util.WithCache(cache),
	)
	require.NoError(t, err)

	hh, err := newHealthHandler(config)
	require.NoError(t, err)

	req := makeRequest(t, config, nil)
	rr := httptest.NewRecorder()

	handler := http.HandlerFunc(hh.ServeHTTP)

	t.Run("fresh cache", func(t *testing.T) {
		handler.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusOK, rr.Result().StatusCode)
		assert.Contains(t, logOutput.String(), "Stale cache")
		t.Log(logOutput.String())
		logOutput.Reset()
	})

	t.Run("immediately re-request, cache should be warm", func(t *testing.T) {
		handler.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusOK, rr.Result().StatusCode)
		assert.NotContains(t, logOutput.String(), "Stale cache")
		t.Log(logOutput.String())
		logOutput.Reset()
	})

	t.Run("allow cache to expire, cache should be cold", func(t *testing.T) {
		time.Sleep(cache + 5*time.Millisecond)
		handler.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusOK, rr.Result().StatusCode)
		assert.Contains(t, logOutput.String(), "Stale cache")
		t.Log(logOutput.String())
		logOutput.Reset()
	})
}

func TestServeCacheNegotiatingContent(t *testing.T) {
	var logOutput bytes.Buffer
	log.SetOutput(&logOutput)
	const cache = time.Duration(time.Millisecond * 100)
	config, err := util.NewConfig(
		util.WithSpecFile(filepath.Join("testdata", "passing.goss.yaml")),
		util.WithCache(cache),
		util.WithOutputFormat("structured"),
	)
	require.NoError(t, err)

	hh, err := newHealthHandler(config)
	require.NoError(t, err)

	rr := httptest.NewRecorder()

	handler := http.HandlerFunc(hh.ServeHTTP)

	t.Run("fresh cache", func(t *testing.T) {
		req := makeRequest(t, config, map[string][]string{
			"accept": {"application/json"},
		})
		handler.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusOK, rr.Result().StatusCode)
		assert.Contains(t, logOutput.String(), "Stale cache")
		t.Log(logOutput.String())
		logOutput.Reset()
	})

	t.Run("immediately re-request, cache should be warm", func(t *testing.T) {
		req := makeRequest(t, config, map[string][]string{
			"accept": {"application/json"},
		})
		handler.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusOK, rr.Result().StatusCode)
		assert.NotContains(t, logOutput.String(), "Stale cache")
		t.Log(logOutput.String())
		logOutput.Reset()
	})

	t.Run("immediately re-request but different accept header, cache should be warm", func(t *testing.T) {
		req := makeRequest(t, config, map[string][]string{
			"accept": {"application/vnd.goss-rspecish"},
		})
		handler.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusOK, rr.Result().StatusCode)
		assert.NotContains(t, logOutput.String(), "Stale cache")
		t.Log(logOutput.String())
		logOutput.Reset()
	})

	t.Run("allow cache to expire, cache should be cold", func(t *testing.T) {
		time.Sleep(cache + 5*time.Millisecond)
		req := makeRequest(t, config, map[string][]string{
			"accept": {"application/json"},
		})
		handler.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusOK, rr.Result().StatusCode)
		assert.Contains(t, logOutput.String(), "Stale cache")
		t.Log(logOutput.String())
		logOutput.Reset()
	})
}

func makeRequest(t *testing.T, config *util.Config, headers map[string][]string) *http.Request {
	req, err := http.NewRequest("GET", config.Endpoint, nil)
	require.NoError(t, err)
	for header, vals := range headers {
		for _, v := range vals {
			req.Header.Add(header, v)
		}
	}
	return req
}
goss-0.4.9/store.go000066400000000000000000000156621467505051300142020ustar00rootroot00000000000000package goss

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"sort"
	"strings"

	"gopkg.in/yaml.v3"

	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/util"
)

const (
	UNSET = iota
	JSON
	YAML
)

var outStoreFormat = UNSET
var currentTemplateFilter TemplateFilter
var debug = false

func getStoreFormatFromFileName(f string) (int, error) {
	ext := filepath.Ext(f)
	switch ext {
	case ".json":
		return JSON, nil
	case ".yaml", ".yml":
		return YAML, nil
	default:
		return 0, fmt.Errorf("unknown file extension: %v", ext)
	}
}

func getStoreFormatFromData(data []byte) (int, error) {
	var v any
	if err := unmarshalJSON(data, &v); err == nil {
		return JSON, nil
	}
	if err := unmarshalYAML(data, &v); err == nil {
		return YAML, nil
	}

	return 0, fmt.Errorf("unable to determine format from content")
}

// ReadJSON Reads json file returning GossConfig
func ReadJSON(filePath string) (GossConfig, error) {
	file, err := os.ReadFile(filePath)
	if err != nil {
		return GossConfig{}, fmt.Errorf("file error: %v", err)
	}

	return ReadJSONData(file, false)
}

type TmplVars struct {
	Vars map[string]any
}

func (t *TmplVars) Env() map[string]string {
	env := make(map[string]string)
	for _, i := range os.Environ() {
		sep := strings.Index(i, "=")
		env[i[0:sep]] = i[sep+1:]
	}
	return env
}

func loadVars(varsFile string, varsInline string) (map[string]any, error) {
	vars, err := varsFromFile(varsFile)
	if err != nil {
		return nil, fmt.Errorf("loading vars file '%s'\n%w", varsFile, err)
	}

	varsExtra, err := varsFromString(varsInline)
	if err != nil {
		return nil, fmt.Errorf("loading inline vars\n%w", err)
	}

	for k, v := range varsExtra {
		vars[k] = v
	}

	return vars, nil
}

func varsFromFile(varsFile string) (map[string]any, error) {
	vars := make(map[string]any)
	if varsFile == "" {
		return vars, nil
	}
	data, err := os.ReadFile(varsFile)
	if err != nil {
		return vars, err
	}
	format, err := getStoreFormatFromData(data)
	if err != nil {
		return nil, err
	}
	if err := unmarshal(data, &vars, format); err != nil {
		return vars, err
	}
	return vars, nil
}

func varsFromString(varsString string) (map[string]any, error) {
	vars := make(map[string]any)
	if varsString == "" {
		return vars, nil
	}
	data := []byte(varsString)
	format, err := getStoreFormatFromData(data)
	if err != nil {
		return nil, err
	}

	if err := unmarshal(data, &vars, format); err != nil {
		return vars, err
	}
	return vars, nil
}

// ReadJSONData Reads json byte array returning GossConfig
func ReadJSONData(data []byte, detectFormat bool) (GossConfig, error) {
	var err error
	if currentTemplateFilter != nil {
		data, err = currentTemplateFilter(data)
		if err != nil {
			return GossConfig{}, err
		}
		if debug {
			fmt.Println("DEBUG: file after text/template render")
			fmt.Println(string(data))
		}
	}

	format := outStoreFormat
	if detectFormat {
		format, err = getStoreFormatFromData(data)
		if err != nil {
			return GossConfig{}, err
		}
	}

	gossConfig := NewGossConfig()
	// Horrible, but will do for now
	if err := unmarshal(data, gossConfig, format); err != nil {
		return *gossConfig, err
	}

	return *gossConfig, nil
}

// RenderJSON reads json file recursively returning string
func RenderJSON(c *util.Config) (string, error) {
	var err error
	debug = c.Debug
	currentTemplateFilter, err = NewTemplateFilter(c.Vars, c.VarsInline)
	if err != nil {
		return "", err
	}

	outStoreFormat, err = getStoreFormatFromFileName(c.Spec)
	if err != nil {
		return "", err
	}

	j, err := ReadJSON(c.Spec)
	if err != nil {
		return "", err
	}

	gossConfig, err := mergeJSONData(j, 0, filepath.Dir(c.Spec))
	if err != nil {
		return "", err
	}

	b, err := marshal(gossConfig)
	if err != nil {
		return "", fmt.Errorf("rendering failed: %v", err)
	}

	return string(b), nil
}

func mergeJSONData(gossConfig GossConfig, depth int, path string) (GossConfig, error) {
	depth++
	if depth >= 50 {
		return GossConfig{}, fmt.Errorf("max depth of 50 reached, possibly due to dependency loop in goss file")
	}
	// Our return gossConfig
	ret := *NewGossConfig()
	ret = mergeGoss(ret, gossConfig)

	// Sort the gossfiles to ensure consistent ordering
	var keys []string
	for k := range gossConfig.Gossfiles {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	// Merge gossfiles in sorted order
	for _, k := range keys {
		g := gossConfig.Gossfiles[k]
		var fpath string
		if strings.HasPrefix(g.GetGossfile(), "/") {
			fpath = g.GetGossfile()
		} else {
			fpath = filepath.Join(path, g.GetGossfile())
		}
		if g.GetSkip() {
			// Do not process gossfiles with the skip attribute
			continue
		}
		matches, err := filepath.Glob(fpath)
		if err != nil {
			return ret, fmt.Errorf("error in expanding glob pattern: %q", err)
		}
		if matches == nil {
			return ret, fmt.Errorf("no matched files were found: %q", fpath)
		}
		for _, match := range matches {
			fdir := filepath.Dir(match)
			j, err := ReadJSON(match)
			if err != nil {
				return GossConfig{}, fmt.Errorf("could not read json data in %s: %s", match, err)
			}
			j, err = mergeJSONData(j, depth, fdir)
			if err != nil {
				return ret, fmt.Errorf("could not write json data: %s", err)
			}
			ret = mergeGoss(ret, j)
		}
	}
	return ret, nil
}

func WriteJSON(filePath string, gossConfig GossConfig) error {
	jsonData, err := marshal(gossConfig)
	if err != nil {
		return fmt.Errorf("failed to write %s: %s", filePath, err)
	}

	// check if the auto added json data is empty before writing to file.
	emptyConfig := *NewGossConfig()
	emptyData, err := marshal(emptyConfig)
	if err != nil {
		return fmt.Errorf("failed to write %s: %s", filePath, err)
	}

	if string(emptyData) == string(jsonData) {
		log.Printf("Can't write empty configuration file. Please check resource name(s).")
		return nil
	}

	if err := os.WriteFile(filePath, jsonData, 0644); err != nil {
		return fmt.Errorf("failed to write %s: %s", filePath, err)
	}

	return nil
}

func resourcePrint(fileName string, res resource.ResourceRead, announce bool) {
	resMap := map[string]resource.ResourceRead{res.ID(): res}

	oj, _ := marshal(resMap)
	typ := reflect.TypeOf(res)
	typs := strings.Split(typ.String(), ".")[1]

	if announce {
		fmt.Printf("Adding %s to '%s':\n\n%s\n\n", typs, fileName, string(oj))
	}
}

func marshal(gossConfig any) ([]byte, error) {
	switch outStoreFormat {
	case JSON:
		return marshalJSON(gossConfig)
	case YAML:
		return marshalYAML(gossConfig)
	default:
		return nil, fmt.Errorf("StoreFormat unset")
	}
}

func unmarshal(data []byte, v any, storeFormat int) error {
	switch storeFormat {
	case JSON:
		return unmarshalJSON(data, v)
	case YAML:
		return unmarshalYAML(data, v)
	default:
		return fmt.Errorf("StoreFormat unset")
	}
}

func marshalJSON(gossConfig any) ([]byte, error) {
	return json.MarshalIndent(gossConfig, "", "    ")
}

func unmarshalJSON(data []byte, v any) error {
	return json.Unmarshal(data, v)
}

func marshalYAML(gossConfig any) ([]byte, error) {
	return yaml.Marshal(gossConfig)
}

func unmarshalYAML(data []byte, v any) error {
	return yaml.Unmarshal(data, v)
}
goss-0.4.9/store_test.go000066400000000000000000000075671467505051300152460ustar00rootroot00000000000000package goss

import (
	"log"
	"os"
	"testing"

	"github.com/stretchr/testify/assert"
)

func Test_varsFromString(t *testing.T) {
	tests := []struct {
		name    string
		arg     string
		want    map[string]any
		wantErr bool
	}{
		{
			name:    "empty_string",
			arg:     ``,
			want:    map[string]any{},
			wantErr: false,
		},
		{
			name:    "empty_JSON",
			arg:     `{}`,
			want:    map[string]any{},
			wantErr: false,
		},
		{
			name: "JSON_simple",
			arg:  `{"a": "a", "b": 1}`,
			want: map[string]any{
				"a": "a",
				"b": float64(1),
			},
			wantErr: false,
		},
		{
			name: "YAML_simple",
			arg:  `{a: a, b: 1}`,
			want: map[string]any{
				"a": "a",
				"b": 1,
			},
			wantErr: false,
		},
		{
			name: "JSON_float",
			arg:  `{"f": 1.23}`,
			want: map[string]any{
				"f": 1.23,
			},
			wantErr: false,
		},
		{
			name: "YAML_float",
			arg:  `{f: 1.23}`,
			want: map[string]any{
				"f": 1.23,
			},
			wantErr: false,
		},
		{
			name: "JSON_list",
			arg:  `{"l": ["l1", "l2", 3]}`,
			want: map[string]any{
				"l": []any{
					"l1",
					"l2",
					float64(3),
				},
			},
			wantErr: false,
		},
		{
			name: "YAML_list",
			arg:  `{l: [l1, l2, 3]}`,
			want: map[string]any{
				"l": []any{
					"l1",
					"l2",
					3,
				},
			},
			wantErr: false,
		},
		{
			name: "JSON_object",
			arg:  `{"o": {"oa": "a", "oo": { "oo1": 1 } } }`,
			want: map[string]any{
				"o": map[string]any{
					"oa": "a",
					"oo": map[string]any{
						"oo1": float64(1),
					},
				},
			},
			wantErr: false,
		},
		{
			name: "YAML_object",
			arg:  `{o: {oa: a, oo: { oo1: 1 } } }`,
			want: map[string]any{
				"o": map[string]any{
					"oa": "a",
					"oo": map[string]any{
						"oo1": 1,
					},
				},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := varsFromString(tt.arg)

			assert.Equal(t, tt.want, got, "map contents")
			assert.Equal(t, tt.wantErr, err != nil, "has error")
		})
	}
}

func Test_loadVars(t *testing.T) {
	fileEmpty, fileEmptyClose := fileMaker(``)
	defer fileEmptyClose()

	fileNil, fileNilClose := fileMaker(``)
	defer fileNilClose()

	fileSimple, fileSimpleClose := fileMaker(`{a: a}`)
	defer fileSimpleClose()

	type args struct {
		varsFile   string
		varsInline string
	}
	tests := []struct {
		name    string
		args    args
		want    map[string]any
		wantErr bool
	}{
		{
			name: "both_empty",
			args: args{
				varsFile:   fileEmpty,
				varsInline: `{}`,
			},
			want:    map[string]any{},
			wantErr: false,
		},
		{
			name: "both_nil",
			args: args{
				varsFile:   fileNil,
				varsInline: `{}`,
			},
			want:    map[string]any{},
			wantErr: false,
		},
		{
			name: "file_empty",
			args: args{
				varsFile:   fileEmpty,
				varsInline: `{b: b}`,
			},
			want: map[string]any{
				"b": "b",
			},
			wantErr: false,
		},
		{
			name: "inline_empty",
			args: args{
				varsFile:   fileSimple,
				varsInline: `{}`,
			},
			want: map[string]any{
				"a": "a",
			},
			wantErr: false,
		},
		{
			name: "no_overwrite",
			args: args{
				varsFile:   fileSimple,
				varsInline: `{b: b}`,
			},
			want: map[string]any{
				"a": "a",
				"b": "b",
			},
			wantErr: false,
		},
		{
			name: "overwrite",
			args: args{
				varsFile:   fileSimple,
				varsInline: `{a: c, b: b}`,
			},
			want: map[string]any{
				"a": "c",
				"b": "b",
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := loadVars(tt.args.varsFile, tt.args.varsInline)

			assert.Equal(t, tt.want, got, "map contents")
			assert.Equal(t, tt.wantErr, err != nil, "has error")
		})
	}
}

func fileMaker(content string) (string, func()) {
	bytes := []byte(content)

	f, err := os.CreateTemp("", "*")
	if err != nil {
		log.Fatal(err)
	}

	_, err = f.Write(bytes)
	if err != nil {
		log.Fatal(err)
	}

	return f.Name(), func() {
		if err := f.Close(); err != nil {
			log.Fatal(err)
		}
	}
}
goss-0.4.9/system/000077500000000000000000000000001467505051300140315ustar00rootroot00000000000000goss-0.4.9/system/addr.go000066400000000000000000000027441467505051300153010ustar00rootroot00000000000000package system

import (
	"context"
	"net"
	"strings"
	"time"

	"github.com/goss-org/goss/util"
)

type Addr interface {
	Address() string
	Exists() (bool, error)
	Reachable() (bool, error)
}

type DefAddr struct {
	address      string
	LocalAddress string
	Timeout      int
}

func NewDefAddr(_ context.Context, address string, system *System, config util.Config) Addr {
	addr := normalizeAddress(address)
	return &DefAddr{
		address:      addr,
		LocalAddress: config.LocalAddress,
		Timeout:      config.TimeOutMilliSeconds(),
	}
}

func (a *DefAddr) ID() string {
	return a.address
}
func (a *DefAddr) Address() string {
	return a.address
}
func (a *DefAddr) Exists() (bool, error) { return a.Reachable() }

func (a *DefAddr) Reachable() (bool, error) {
	network, address := splitAddress(a.address)

	var localAddr net.Addr
	if network == "udp" {
		localAddr = &net.UDPAddr{IP: net.ParseIP(a.LocalAddress)}
	} else {
		localAddr = &net.TCPAddr{IP: net.ParseIP(a.LocalAddress)}
	}
	d := net.Dialer{LocalAddr: localAddr, Timeout: time.Duration(a.Timeout) * time.Millisecond}
	conn, err := d.Dial(network, address)
	if err != nil {
		return false, nil
	}
	conn.Close()
	return true, nil
}

func splitAddress(fulladdress string) (network, address string) {
	split := strings.SplitN(fulladdress, "://", 2)
	if len(split) == 2 {
		return split[0], split[1]
	}
	return "tcp", fulladdress
}

func normalizeAddress(fulladdress string) string {
	net, addr := splitAddress(fulladdress)
	return net + "://" + addr
}
goss-0.4.9/system/command.go000066400000000000000000000041041467505051300157750ustar00rootroot00000000000000package system

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os/exec"
	"time"

	"github.com/goss-org/goss/util"
)

type Command interface {
	Command() string
	Exists() (bool, error)
	ExitStatus() (int, error)
	Stdout() (io.Reader, error)
	Stderr() (io.Reader, error)
}

type DefCommand struct {
	Ctx        context.Context
	command    string
	exitStatus int
	stdout     io.Reader
	stderr     io.Reader
	loaded     bool
	Timeout    int
	err        error
}

func NewDefCommand(ctx context.Context, command string, system *System, config util.Config) Command {
	return &DefCommand{
		Ctx:     ctx,
		command: command,
		Timeout: config.TimeOutMilliSeconds(),
	}
}

func (c *DefCommand) setup() error {
	if c.loaded {
		return c.err
	}
	c.loaded = true

	cmd := commandWrapper(c.command)
	err := runCommand(cmd, c.Timeout)

	// We don't care about ExitError since it's covered by status
	if _, ok := err.(*exec.ExitError); !ok {
		c.err = err
	}
	c.exitStatus = cmd.Status
	stdoutB := cmd.Stdout.Bytes()
	stderrB := cmd.Stderr.Bytes()
	id := c.Ctx.Value("id")
	logBytes(stdoutB, fmt.Sprintf("[Command][%s][stdout] ", id))
	logBytes(stderrB, fmt.Sprintf("[Command][%s][stderr] ", id))
	c.stdout = bytes.NewReader(stdoutB)
	c.stderr = bytes.NewReader(stderrB)

	return c.err
}

func (c *DefCommand) Command() string {
	return c.command
}

func (c *DefCommand) ExitStatus() (int, error) {
	err := c.setup()

	return c.exitStatus, err
}

func (c *DefCommand) Stdout() (io.Reader, error) {
	err := c.setup()

	return c.stdout, err
}

func (c *DefCommand) Stderr() (io.Reader, error) {
	err := c.setup()

	return c.stderr, err
}

// Stub out
func (c *DefCommand) Exists() (bool, error) {
	return false, nil
}

func runCommand(cmd *util.Command, timeout int) error {
	c1 := make(chan bool, 1)
	e1 := make(chan error, 1)
	timeoutD := time.Duration(timeout) * time.Millisecond
	go func() {
		err := cmd.Run()
		if err != nil {
			e1 <- err
		}
		c1 <- true
	}()
	select {
	case <-c1:
		return nil
	case err := <-e1:
		return err
	case <-time.After(timeoutD):
		return fmt.Errorf("Command execution timed out (%s)", timeoutD)
	}
}
goss-0.4.9/system/command_posix.go000066400000000000000000000004011467505051300172130ustar00rootroot00000000000000//go:build linux || darwin || !windows
// +build linux darwin !windows

package system

import "github.com/goss-org/goss/util"

const linuxShell string = "sh"

func commandWrapper(cmd string) *util.Command {
	return util.NewCommand(linuxShell, "-c", cmd)
}
goss-0.4.9/system/command_posix_test.go000066400000000000000000000005751467505051300202660ustar00rootroot00000000000000//go:build linux || darwin || !windows
// +build linux darwin !windows

package system

import (
	"os/exec"
	"testing"
)

func TestCommandWrapper(t *testing.T) {
	t.Parallel()

	c := commandWrapper("echo hello world")
	cmdPath, _ := exec.LookPath(linuxShell)
	if c.Cmd.Path != cmdPath {
		t.Errorf("Command not wrapped properly for OS. got %s, want: %s", c.Cmd.Path, cmdPath)
	}
}
goss-0.4.9/system/command_windows.go000066400000000000000000000003611467505051300175500ustar00rootroot00000000000000//go:build windows
// +build windows

package system

import "github.com/goss-org/goss/util"

const windowsShell string = "cmd"

func commandWrapper(cmd string) *util.Command {
	return util.NewCommandForWindowsCmd(windowsShell, "/c", cmd)
}
goss-0.4.9/system/command_windows_test.go000066400000000000000000000012251467505051300206070ustar00rootroot00000000000000//go:build windows
// +build windows

package system

import (
	"os/exec"
	"testing"
)

func TestCommandWrapper(t *testing.T) {
	t.Parallel()

	c := commandWrapper("echo hello world")
	cmdPath, _ := exec.LookPath(windowsShell)
	if c.Cmd.Path != cmdPath {
		t.Errorf("Command not wrapped properly for Windows os. got %s, want: %s", c.Cmd.Path, cmdPath)
	}

	if c.Cmd.SysProcAttr.CmdLine != "/c echo hello world" {
		t.Errorf("Command not wrapped properly for Windows cmd.exe. got %s, want: %s", c.Cmd.SysProcAttr.CmdLine, "/c echo hello world")
	}

	if len(c.Cmd.Args) != 1 {
		t.Errorf("Args length should be blank. got: %d, want: %d", len(c.Cmd.Args), 1)
	}
}
goss-0.4.9/system/dns.go000066400000000000000000000165221467505051300151520ustar00rootroot00000000000000package system

import (
	"context"
	"fmt"
	"net"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/goss-org/goss/util"
	"github.com/miekg/dns"
)

type DNS interface {
	Host() string
	Addrs() ([]string, error)
	Resolvable() (bool, error)
	Exists() (bool, error)
	Server() string
	Qtype() string
}

type DefDNS struct {
	host       string
	resolvable bool
	addrs      []string
	Timeout    int
	loaded     bool
	err        error
	server     string
	qtype      string
}

func NewDefDNS(_ context.Context, host string, system *System, config util.Config) DNS {
	var h string
	var t string

	splitHost := strings.SplitN(host, ":", 2)
	if len(splitHost) == 2 && regexp.MustCompile(`^[A-Z]+$`).MatchString(splitHost[0]) {
		h = splitHost[1]
		t = splitHost[0]
	} else {
		h = host
	}

	return &DefDNS{
		host:    h,
		Timeout: config.TimeOutMilliSeconds(),
		server:  config.Server,
		qtype:   t,
	}
}

func (d *DefDNS) Host() string {
	return d.host
}

func (d *DefDNS) Server() string {
	return d.server
}

func (d *DefDNS) Qtype() string {
	return d.qtype
}

func (d *DefDNS) setup() error {
	if d.loaded {
		return d.err
	}
	d.loaded = true

	for i := 0; i < 3; i++ {
		addrs, err := DNSlookup(d.host, d.server, d.qtype, d.Timeout)
		if err != nil || len(addrs) == 0 {
			d.resolvable = false
			d.addrs = []string{}
			// DNSError is resolvable == false, ignore error
			if _, ok := err.(*net.DNSError); ok {
				return nil
			}
			d.err = err
			continue
		}
		sort.Strings(addrs)
		d.resolvable = true
		d.addrs = addrs
		d.err = nil
		return nil
	}
	return d.err
}

func (d *DefDNS) Addrs() ([]string, error) {
	err := d.setup()

	return d.addrs, err
}

func (d *DefDNS) Resolvable() (bool, error) {
	err := d.setup()

	return d.resolvable, err
}

// Stub out
func (d *DefDNS) Exists() (bool, error) {
	return false, nil
}

func DNSlookup(host string, server string, qtype string, timeout int) ([]string, error) {
	c1 := make(chan []string, 1)
	e1 := make(chan error, 1)
	timeoutD := time.Duration(timeout) * time.Millisecond

	var addrs []string
	var err error
	go func() {
		if server != "" {
			c := new(dns.Client)
			c.Timeout = timeoutD
			m := new(dns.Msg)

			switch qtype {
			case "A":
				addrs, err = LookupA(host, server, c, m)
			case "AAAA":
				addrs, err = LookupAAAA(host, server, c, m)
			case "PTR":
				addrs, err = LookupPTR(host, server, c, m)
			case "CNAME":
				addrs, err = LookupCNAME(host, server, c, m)
			case "MX":
				addrs, err = LookupMX(host, server, c, m)
			case "NS":
				addrs, err = LookupNS(host, server, c, m)
			case "SRV":
				addrs, err = LookupSRV(host, server, c, m)
			case "TXT":
				addrs, err = LookupTXT(host, server, c, m)
			case "CAA":
				addrs, err = LookupCAA(host, server, c, m)
			default:
				addrs, err = LookupHost(host, server, c, m)
			}
		} else {
			addrs, err = net.LookupHost(host)
		}
		if err != nil {
			e1 <- err
		}
		c1 <- addrs
	}()
	select {
	case res := <-c1:
		return res, nil
	case err := <-e1:
		return nil, err
	case <-time.After(timeoutD):
		return nil, fmt.Errorf("DNS lookup timed out (%s)", timeoutD)
	}
}

// A and AAAA record lookup - similar to net.LookupHost
func LookupHost(host string, server string, c *dns.Client, m *dns.Msg) (addrs []string, err error) {
	a, _ := LookupA(host, server, c, m)
	aaaa, _ := LookupAAAA(host, server, c, m)
	addrs = append(a, aaaa...)

	return
}

// A record lookup
func LookupA(host string, server string, c *dns.Client, m *dns.Msg) (addrs []string, err error) {
	m.SetQuestion(dns.Fqdn(host), dns.TypeA)
	r, _, err := c.Exchange(m, parseServerString(server))
	if err != nil {
		return nil, err
	}

	for _, ans := range r.Answer {
		if t, ok := ans.(*dns.A); ok {
			addrs = append(addrs, t.A.String())
		}
	}

	return
}

// parseServerString - Check if the DNS Server in server config has a port, if not ensure 53 is prefixed.
func parseServerString(server string) string {
	srvhost, srvport, err := net.SplitHostPort(server)
	if err != nil {
		srvport = "53"
		srvhost = server
	}
	return net.JoinHostPort(srvhost, srvport)
}

// AAAA (IPv6) record lookup
func LookupAAAA(host string, server string, c *dns.Client, m *dns.Msg) (addrs []string, err error) {
	m.SetQuestion(dns.Fqdn(host), dns.TypeAAAA)
	r, _, err := c.Exchange(m, parseServerString(server))
	if err != nil {
		return nil, err
	}

	for _, ans := range r.Answer {
		if t, ok := ans.(*dns.AAAA); ok {
			addrs = append(addrs, t.AAAA.String())
		}
	}

	return
}

// CNAME record lookup
func LookupCNAME(host string, server string, c *dns.Client, m *dns.Msg) (addrs []string, err error) {
	m.SetQuestion(dns.Fqdn(host), dns.TypeCNAME)
	r, _, err := c.Exchange(m, parseServerString(server))
	if err != nil {
		return nil, err
	}

	for _, ans := range r.Answer {
		if t, ok := ans.(*dns.CNAME); ok {
			addrs = append(addrs, t.Target)
		}
	}

	return
}

// MX record lookup
func LookupMX(host string, server string, c *dns.Client, m *dns.Msg) (addrs []string, err error) {
	m.SetQuestion(dns.Fqdn(host), dns.TypeMX)
	r, _, err := c.Exchange(m, parseServerString(server))
	if err != nil {
		return nil, err
	}

	for _, ans := range r.Answer {
		if t, ok := ans.(*dns.MX); ok {
			mxstring := strconv.Itoa(int(t.Preference)) + " " + t.Mx
			addrs = append(addrs, mxstring)
		}
	}

	return
}

// NS record lookup
func LookupNS(host string, server string, c *dns.Client, m *dns.Msg) (addrs []string, err error) {
	m.SetQuestion(dns.Fqdn(host), dns.TypeNS)
	r, _, err := c.Exchange(m, parseServerString(server))
	if err != nil {
		return nil, err
	}

	for _, ans := range r.Answer {
		if t, ok := ans.(*dns.NS); ok {
			addrs = append(addrs, t.Ns)
		}
	}

	return
}

// SRV record lookup
func LookupSRV(host string, server string, c *dns.Client, m *dns.Msg) (addrs []string, err error) {
	m.SetQuestion(dns.Fqdn(host), dns.TypeSRV)
	r, _, err := c.Exchange(m, parseServerString(server))
	if err != nil {
		return nil, err
	}

	for _, ans := range r.Answer {
		if t, ok := ans.(*dns.SRV); ok {
			prio := strconv.Itoa(int(t.Priority))
			weight := strconv.Itoa(int(t.Weight))
			port := strconv.Itoa(int(t.Port))
			srvrec := strings.Join([]string{prio, weight, port, t.Target}, " ")
			addrs = append(addrs, srvrec)
		}
	}

	return
}

// TXT record lookup
func LookupTXT(host string, server string, c *dns.Client, m *dns.Msg) (addrs []string, err error) {
	m.SetQuestion(dns.Fqdn(host), dns.TypeTXT)
	r, _, err := c.Exchange(m, parseServerString(server))
	if err != nil {
		return nil, err
	}

	for _, ans := range r.Answer {
		if t, ok := ans.(*dns.TXT); ok {
			addrs = append(addrs, t.Txt...)
		}
	}

	return
}

// PTR record lookup
func LookupPTR(addr string, server string, c *dns.Client, m *dns.Msg) (name []string, err error) {

	reverse, err := dns.ReverseAddr(addr)
	if err != nil {
		return nil, err
	}

	m.SetQuestion(reverse, dns.TypePTR)

	r, _, err := c.Exchange(m, parseServerString(server))
	if err != nil {
		return nil, err
	}

	for _, ans := range r.Answer {
		name = append(name, ans.(*dns.PTR).Ptr)
	}

	return
}

// CAA record lookup
func LookupCAA(host string, server string, c *dns.Client, m *dns.Msg) (addrs []string, err error) {
	m.SetQuestion(dns.Fqdn(host), dns.TypeCAA)
	r, _, err := c.Exchange(m, parseServerString(server))
	if err != nil {
		return nil, err
	}

	for _, ans := range r.Answer {
		if t, ok := ans.(*dns.CAA); ok {
			flag := strconv.Itoa(int(t.Flag))
			caarec := strings.Join([]string{flag, t.Tag, t.Value}, " ")
			addrs = append(addrs, caarec)
		}
	}

	return
}
goss-0.4.9/system/dns_test.go000066400000000000000000000007371467505051300162120ustar00rootroot00000000000000package system

import (
	"testing"
)

func TestParseServerString(t *testing.T) {

	tables := []struct {
		x string
		n string
	}{
		{"127.0.0.1", "127.0.0.1:53"},
		{"127.0.0.1:53", "127.0.0.1:53"},
		{"127.0.0.1:8600", "127.0.0.1:8600"},
		{"1.1.1.1:53", "1.1.1.1:53"},
	}

	for _, table := range tables {
		output := parseServerString(table.x)
		if output != table.n {
			t.Errorf("parseServerString (%s) was incorrect, got: %s, want: %s.", table.x, output, table.n)
		}
	}
}
goss-0.4.9/system/file.go000066400000000000000000000113441467505051300153020ustar00rootroot00000000000000package system

import (
	"context"
	"crypto/md5"
	"crypto/sha256"
	"crypto/sha512"
	"fmt"
	"hash"
	"io"
	"os"
	"os/user"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/goss-org/goss/util"
)

type File interface {
	Path() string
	Exists() (bool, error)
	Contents() (io.Reader, error)
	Mode() (string, error)
	Size() (int, error)
	Filetype() (string, error)
	Owner() (string, error)
	Uid() (int, error)
	Group() (string, error)
	Gid() (int, error)
	LinkedTo() (string, error)
	Md5() (string, error)
	Sha256() (string, error)
	Sha512() (string, error)
}

type hashFuncType string

const (
	md5Hash    hashFuncType = "md5"
	sha256Hash hashFuncType = "sha256"
	sha512Hash hashFuncType = "sha512"
)

type DefFile struct {
	path     string
	realPath string
	loaded   bool
	err      error
}

func NewDefFile(_ context.Context, path string, system *System, config util.Config) File {
	var err error
	if !strings.HasPrefix(path, "~") {
		path, err = filepath.Abs(path)
	}
	return &DefFile{path: path, err: err}
}

func (f *DefFile) setup() error {
	if f.loaded || f.err != nil {
		return f.err
	}
	f.loaded = true
	if f.realPath, f.err = realPath(f.path); f.err != nil {
		return f.err
	}

	return f.err
}

func (f *DefFile) Path() string {
	return f.path
}

func (f *DefFile) Exists() (bool, error) {
	if err := f.setup(); err != nil {
		return false, err
	}

	_, err := os.Lstat(f.realPath)
	if os.IsNotExist(err) {
		return false, nil
	}
	return true, err
}

func (f *DefFile) Contents() (io.Reader, error) {
	if err := f.setup(); err != nil {
		return nil, err
	}

	fh, err := os.Open(f.realPath)
	if err != nil {
		return nil, err
	}
	return fh, nil
}

func (f *DefFile) Size() (int, error) {
	if err := f.setup(); err != nil {
		return 0, err
	}

	fi, err := os.Lstat(f.realPath)
	if err != nil {
		return 0, err
	}

	size := fi.Size()
	return int(size), nil
}

func (f *DefFile) Filetype() (string, error) {
	if err := f.setup(); err != nil {
		return "", err
	}

	fi, err := os.Lstat(f.realPath)
	if err != nil {
		return "", err
	}

	switch {
	case fi.Mode()&os.ModeSymlink == os.ModeSymlink:
		return "symlink", nil
	case fi.Mode()&os.ModeDevice == os.ModeDevice:
		if fi.Mode()&os.ModeCharDevice == os.ModeCharDevice {
			return "character-device", nil
		}
		return "block-device", nil
	case fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe:
		return "pipe", nil
	case fi.Mode()&os.ModeSocket == os.ModeSocket:
		return "socket", nil
	case fi.IsDir():
		return "directory", nil
	case fi.Mode().IsRegular():
		return "file", nil
	}
	// FIXME: file as a catchall?
	return "file", nil
}

func (f *DefFile) LinkedTo() (string, error) {
	if err := f.setup(); err != nil {
		return "", err
	}

	dst, err := os.Readlink(f.realPath)
	if err != nil {
		return "", err
	}
	return dst, nil
}

func realPath(path string) (string, error) {
	if !strings.HasPrefix(path, "~") {
		return path, nil
	}
	pathS := strings.Split(path, "/")
	f := pathS[0]

	var usr *user.User
	var err error
	if f == "~" {
		usr, err = user.Current()
	} else {
		usr, err = user.Lookup(f[1:])
	}
	if err != nil {
		return "", err
	}
	pathS[0] = usr.HomeDir

	realPath := strings.Join(pathS, "/")
	realPath, err = filepath.Abs(realPath)

	return realPath, err
}

func (f *DefFile) hash(hashFunc hashFuncType) (string, error) {

	if err := f.setup(); err != nil {
		return "", err
	}

	fh, err := os.Open(f.realPath)
	if err != nil {
		return "", err
	}
	defer fh.Close()

	var hash hash.Hash

	switch hashFunc {
	case md5Hash:
		hash = md5.New()
	case sha256Hash:
		hash = sha256.New()
	case sha512Hash:
		hash = sha512.New()
	default:
		return "", fmt.Errorf("Unsupported hash function %s", hashFunc)
	}

	if _, err := io.Copy(hash, fh); err != nil {
		return "", err
	}

	return fmt.Sprintf("%x", hash.Sum(nil)), nil
}

func (f *DefFile) Md5() (string, error) {
	return f.hash(md5Hash)
}

func (f *DefFile) Sha256() (string, error) {
	return f.hash(sha256Hash)
}

func (f *DefFile) Sha512() (string, error) {
	return f.hash(sha512Hash)
}

func getUserForUid(uid int) (string, error) {
	if user, err := user.LookupId(strconv.Itoa(uid)); err == nil {
		return user.Username, nil
	}

	cmd := util.NewCommand("getent", "passwd", strconv.Itoa(uid))
	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("Error: no matching entries in passwd file. getent passwd: %v", err)
	}
	userS := strings.Split(cmd.Stdout.String(), ":")[0]

	return userS, nil
}

func getGroupForGid(gid int) (string, error) {
	if group, err := user.LookupGroupId(strconv.Itoa(gid)); err == nil {
		return group.Name, nil
	}

	cmd := util.NewCommand("getent", "group", strconv.Itoa(gid))
	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("Error: no matching entries in passwd file. getent group: %v", err)
	}
	groupS := strings.Split(cmd.Stdout.String(), ":")[0]

	return groupS, nil
}
goss-0.4.9/system/file_posix.go000066400000000000000000000033661467505051300165310ustar00rootroot00000000000000//go:build linux || darwin || !windows
// +build linux darwin !windows

package system

import (
	"fmt"
	"os"
	"strconv"
	"syscall"
)

func (f *DefFile) Mode() (string, error) {
	mode, err := f.getFileInfo(func(fi os.FileInfo) string {
		stat := fi.Sys().(*syscall.Stat_t)
		return fmt.Sprintf("%04o", (stat.Mode & 07777))
	})
	if err != nil {
		return "", err
	}

	return mode, nil
}

func (f *DefFile) Owner() (string, error) {
	uidS, err := f.getFileInfo(func(fi os.FileInfo) string {
		return fmt.Sprint(fi.Sys().(*syscall.Stat_t).Uid)
	})
	if err != nil {
		return "", err
	}

	uid, err := strconv.Atoi(uidS)
	if err != nil {
		return "", err
	}
	return getUserForUid(uid)
}

func (f *DefFile) Uid() (int, error) {
	uidS, err := f.getFileInfo(func(fi os.FileInfo) string {
		return fmt.Sprint(fi.Sys().(*syscall.Stat_t).Uid)
	})
	if err != nil {
		return -1, err
	}

	uid, err := strconv.Atoi(uidS)
	if err != nil {
		return -1, err
	}
	return uid, nil
}

func (f *DefFile) Group() (string, error) {
	gidS, err := f.getFileInfo(func(fi os.FileInfo) string {
		return fmt.Sprint(fi.Sys().(*syscall.Stat_t).Gid)
	})
	if err != nil {
		return "", err
	}

	gid, err := strconv.Atoi(gidS)
	if err != nil {
		return "", err
	}
	return getGroupForGid(gid)
}

func (f *DefFile) Gid() (int, error) {
	gidS, err := f.getFileInfo(func(fi os.FileInfo) string {
		return fmt.Sprint(fi.Sys().(*syscall.Stat_t).Gid)
	})
	if err != nil {
		return -1, err
	}

	gid, err := strconv.Atoi(gidS)
	if err != nil {
		return -1, err
	}
	return gid, nil
}

func (f *DefFile) getFileInfo(selectorFunc func(os.FileInfo) string) (string, error) {
	if err := f.setup(); err != nil {
		return "", err
	}

	fi, err := os.Lstat(f.realPath)
	if err != nil {
		return "", err
	}
	return selectorFunc(fi), nil
}
goss-0.4.9/system/file_windows.go000066400000000000000000000007741467505051300170610ustar00rootroot00000000000000//go:build windows
// +build windows

package system

func (f *DefFile) Mode() (string, error) {
	return "-1", nil // not applicable on Windows
}

func (f *DefFile) Owner() (string, error) {
	return "-1", nil // not applicable on Windows
}

func (f *DefFile) Uid() (int, error) {
	return -1, nil // not applicable on Windows
}

func (f *DefFile) Group() (string, error) {
	return "-1", nil // not applicable on Windows
}

func (f *DefFile) Gid() (int, error) {
	return -1, nil // not applicable on Windows
}
goss-0.4.9/system/gossfile.go000066400000000000000000000007061467505051300161760ustar00rootroot00000000000000package system

import (
	"context"

	"github.com/goss-org/goss/util"
)

type Gossfile interface {
	Path() string
	Exists() (bool, error)
}

type DefGossfile struct {
	path string
}

func (g *DefGossfile) Path() string {
	return g.path
}

// Stub out
func (g *DefGossfile) Exists() (bool, error) {
	return false, nil
}

func NewDefGossfile(_ context.Context, path string, system *System, config util.Config) Gossfile {
	return &DefGossfile{path: path}
}
goss-0.4.9/system/group.go000066400000000000000000000014321467505051300155140ustar00rootroot00000000000000package system

import (
	"context"
	"os/user"
	"strconv"

	"github.com/goss-org/goss/util"
)

type Group interface {
	Groupname() string
	Exists() (bool, error)
	GID() (int, error)
}

type DefGroup struct {
	groupname string
}

func NewDefGroup(_ context.Context, groupname string, system *System, config util.Config) Group {
	return &DefGroup{groupname: groupname}
}

func (u *DefGroup) Groupname() string {
	return u.groupname
}

func (u *DefGroup) Exists() (bool, error) {
	_, err := user.LookupGroup(u.groupname)
	if err != nil {
		return false, nil
	}
	return true, nil
}

func (u *DefGroup) GID() (int, error) {
	group, err := user.LookupGroup(u.groupname)
	if err != nil {
		return 0, err
	}

	gid, err := strconv.Atoi(group.Gid)
	if err != nil {
		return 0, err
	}

	return gid, nil
}
goss-0.4.9/system/http.go000066400000000000000000000113621467505051300153420ustar00rootroot00000000000000package system

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"sort"
	"strings"
	"time"

	"github.com/goss-org/goss/util"
)

const USER_AGENT_HEADER_PREFIX = "user-agent:"
const DEFAULT_USER_AGENT_PREFIX = "goss/"

type HTTP interface {
	HTTP() string
	Status() (int, error)
	Headers() (io.Reader, error)
	Body() (io.Reader, error)
	Exists() (bool, error)
	SetAllowInsecure(bool)
	SetNoFollowRedirects(bool)
}

type DefHTTP struct {
	http              string
	allowInsecure     bool
	noFollowRedirects bool
	resp              *http.Response
	RequestHeader     http.Header
	RequestBody       string
	Timeout           int
	loaded            bool
	err               error
	Username          string
	Password          string
	CAFile            string
	CertFile          string
	KeyFile           string
	Method            string
	Proxy             string
}

func NewDefHTTP(_ context.Context, httpStr string, system *System, config util.Config) HTTP {
	headers := http.Header{}

	if !hasUserAgentHeader(config.RequestHeader) {
		config.RequestHeader = append(config.RequestHeader, fmt.Sprintf("%s %s%s", USER_AGENT_HEADER_PREFIX, DEFAULT_USER_AGENT_PREFIX, util.Version))
	}

	for _, r := range config.RequestHeader {
		str := strings.SplitN(r, ": ", 2)
		headers.Add(str[0], str[1])
	}
	return &DefHTTP{
		http:              httpStr,
		allowInsecure:     config.AllowInsecure,
		Method:            config.Method,
		noFollowRedirects: config.NoFollowRedirects,
		RequestHeader:     headers,
		RequestBody:       config.RequestBody,
		Timeout:           config.TimeOutMilliSeconds(),
		Username:          config.Username,
		Password:          config.Password,
		CAFile:            config.CAFile,
		CertFile:          config.CertFile,
		KeyFile:           config.KeyFile,
		Proxy:             config.Proxy,
	}
}

func HeaderToArray(header http.Header) (res []string) {
	for name, values := range header {
		for _, value := range values {
			res = append(res, fmt.Sprintf("%s: %s", name, value))
		}
	}
	sort.Strings(res)
	return
}

func (u *DefHTTP) setup() error {
	if u.loaded {
		return u.err
	}
	u.loaded = true
	if err := u.setupReal(); err != nil {
		u.err = err
	}
	return u.err

}
func (u *DefHTTP) setupReal() error {
	proxyURL := http.ProxyFromEnvironment
	if u.Proxy != "" {
		parseProxy, err := url.Parse(u.Proxy)

		if err != nil {
			return err
		}

		proxyURL = http.ProxyURL(parseProxy)
	}

	tlsConfig := &tls.Config{
		InsecureSkipVerify: u.allowInsecure,
		Renegotiation:      tls.RenegotiateFreelyAsClient,
	}
	if u.CAFile != "" {
		// FIXME: iotutil
		caCert, err := os.ReadFile(u.CAFile)
		if err != nil {
			return err
		}
		roots := x509.NewCertPool()
		ok := roots.AppendCertsFromPEM(caCert)
		if !ok {
			return fmt.Errorf("Failed parse root certificate: %s", u.CAFile)
		}
		tlsConfig.RootCAs = roots
	}

	if u.CertFile != "" && u.KeyFile != "" {
		cert, err := tls.LoadX509KeyPair(u.CertFile, u.KeyFile)
		if err != nil {
			return err
		}

		tlsConfig.Certificates = []tls.Certificate{cert}
	}

	tr := &http.Transport{
		TLSClientConfig:   tlsConfig,
		DisableKeepAlives: true,
		Proxy:             proxyURL,
	}
	client := &http.Client{
		Transport: tr,
		Timeout:   time.Duration(u.Timeout) * time.Millisecond,
	}

	if u.noFollowRedirects {
		client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		}
	}

	req, err := http.NewRequest(u.Method, u.http, strings.NewReader(u.RequestBody))
	if err != nil {
		return err
	}
	req.Header = u.RequestHeader.Clone()

	if host := req.Header.Get("Host"); host != "" {
		req.Host = host
	}

	if u.Username != "" || u.Password != "" {
		req.SetBasicAuth(u.Username, u.Password)
	}
	u.resp, u.err = client.Do(req)

	return u.err
}

func (u *DefHTTP) Exists() (bool, error) {
	if _, err := u.Status(); err != nil {
		return false, err
	}
	return true, nil
}

func (u *DefHTTP) SetNoFollowRedirects(t bool) {
	u.noFollowRedirects = t
}

func (u *DefHTTP) SetAllowInsecure(t bool) {
	u.allowInsecure = t
}

func (u *DefHTTP) ID() string {
	return u.http
}

func (u *DefHTTP) HTTP() string {
	return u.http
}

func (u *DefHTTP) Status() (int, error) {
	if err := u.setup(); err != nil {
		return 0, err
	}

	return u.resp.StatusCode, nil
}

func (u *DefHTTP) Headers() (io.Reader, error) {
	if err := u.setup(); err != nil {
		return nil, err
	}

	var headerString = strings.Join(HeaderToArray(u.resp.Header), "\n")
	return strings.NewReader(headerString), nil
}

func (u *DefHTTP) Body() (io.Reader, error) {
	if err := u.setup(); err != nil {
		return nil, err
	}

	return u.resp.Body, nil
}

func hasUserAgentHeader(headers []string) bool {
	for _, header := range headers {
		if strings.HasPrefix(strings.ToLower(header), USER_AGENT_HEADER_PREFIX) {
			return true
		}
	}
	return false
}
goss-0.4.9/system/interface.go000066400000000000000000000025101467505051300163160ustar00rootroot00000000000000package system

import (
	"context"
	"net"

	"github.com/goss-org/goss/util"
)

type Interface interface {
	Name() string
	Exists() (bool, error)
	Addrs() ([]string, error)
	MTU() (int, error)
}

type DefInterface struct {
	name   string
	loaded bool
	exists bool
	iface  *net.Interface
	err    error
}

func NewDefInterface(_ context.Context, name string, systei *System, config util.Config) Interface {
	return &DefInterface{
		name: name,
	}
}

func (i *DefInterface) setup() error {
	if i.loaded {
		return i.err
	}
	i.loaded = true

	iface, err := net.InterfaceByName(i.name)
	if err != nil {
		i.exists = false
		i.err = err
		return i.err
	}
	i.iface = iface
	i.exists = true
	return nil
}

func (i *DefInterface) ID() string {
	return i.name
}

func (i *DefInterface) Name() string {
	return i.name
}

func (i *DefInterface) Exists() (bool, error) {
	if err := i.setup(); err != nil {
		return false, nil
	}

	return i.exists, nil
}

func (i *DefInterface) Addrs() ([]string, error) {
	if err := i.setup(); err != nil {
		return nil, err
	}

	addrs, err := i.iface.Addrs()
	if err != nil {
		return nil, err
	}

	var ret []string
	for _, addr := range addrs {
		ret = append(ret, addr.String())
	}
	return ret, nil
}

func (i *DefInterface) MTU() (int, error) {
	if err := i.setup(); err != nil {
		return 0, err
	}

	return i.iface.MTU, nil
}
goss-0.4.9/system/kernel_param.go000066400000000000000000000013131467505051300170160ustar00rootroot00000000000000package system

import (
	"context"

	"github.com/achanda/go-sysctl"
	"github.com/goss-org/goss/util"
)

type KernelParam interface {
	Key() string
	Exists() (bool, error)
	Value() (string, error)
}

type DefKernelParam struct {
	key string
}

func NewDefKernelParam(_ context.Context, key string, system *System, config util.Config) KernelParam {
	return &DefKernelParam{
		key: key,
	}
}

func (k *DefKernelParam) ID() string {
	return k.key
}

func (k *DefKernelParam) Key() string {
	return k.key
}

func (k *DefKernelParam) Exists() (bool, error) {
	if _, err := k.Value(); err != nil {
		return false, nil
	}
	return true, nil
}

func (k *DefKernelParam) Value() (string, error) {
	return sysctl.Get(k.key)
}
goss-0.4.9/system/log.go000066400000000000000000000003421467505051300151400ustar00rootroot00000000000000package system

import (
	"bytes"
	"log"
)

func logBytes(b []byte, prefix string) {
	if len(b) == 0 {
		return
	}
	lines := bytes.Split(b, []byte("\n"))
	for _, l := range lines {
		log.Printf("[DEBUG]%s %s", prefix, l)
	}
}
goss-0.4.9/system/mount.go000066400000000000000000000055551467505051300155340ustar00rootroot00000000000000package system

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/goss-org/goss/util"
	"github.com/moby/sys/mountinfo"
	"github.com/samber/lo"
)

type Mount interface {
	MountPoint() string
	Exists() (bool, error)
	Opts() ([]string, error)
	VfsOpts() ([]string, error)
	Source() (string, error)
	Filesystem() (string, error)
	Usage() (int, error)
}

type DefMount struct {
	mountPoint string
	loaded     bool
	exists     bool
	mountInfo  *mountinfo.Info
	usage      int
	Timeout    int
	err        error
}

func NewDefMount(_ context.Context, mountPoint string, system *System, config util.Config) Mount {
	return &DefMount{
		mountPoint: mountPoint,
		Timeout:    config.TimeOutMilliSeconds(),
	}
}

func (m *DefMount) setup() error {
	if m.loaded {
		return m.err
	}
	m.loaded = true

	mountInfo, err := getMount(m.mountPoint, m.Timeout)
	if err != nil {
		m.exists = false
		m.err = err
		return m.err
	}
	m.mountInfo = mountInfo
	m.exists = true

	usage, err := getUsage(m.mountPoint)
	if err != nil {
		m.err = err
		return m.err
	}
	m.usage = usage

	return nil
}

func (m *DefMount) ID() string {
	return m.mountPoint
}

func (m *DefMount) MountPoint() string {
	return m.mountPoint
}

func (m *DefMount) Exists() (bool, error) {
	if err := m.setup(); err != nil {
		return false, err
	}
	return m.exists, nil
}

func (m *DefMount) Opts() ([]string, error) {
	if err := m.setup(); err != nil {
		return nil, err
	}
	allOpts := splitMountInfo(m.mountInfo.Options)

	return lo.Uniq(allOpts), nil
}

func (m *DefMount) VfsOpts() ([]string, error) {
	if err := m.setup(); err != nil {
		return nil, err
	}
	opts := splitMountInfo(m.mountInfo.VFSOptions)
	return opts, nil
}

func (m *DefMount) Source() (string, error) {
	if err := m.setup(); err != nil {
		return "", err
	}

	return m.mountInfo.Source, nil
}

func (m *DefMount) Filesystem() (string, error) {
	if err := m.setup(); err != nil {
		return "", err
	}

	return m.mountInfo.FSType, nil
}

func (m *DefMount) Usage() (int, error) {
	if err := m.setup(); err != nil {
		return -1, err
	}

	return m.usage, nil
}

func getMount(mountpoint string, timeout int) (*mountinfo.Info, error) {
	c1 := make(chan *mountinfo.Info, 1)
	e1 := make(chan error, 1)
	timeoutD := time.Duration(timeout) * time.Millisecond

	go func() {
		entries, err := mountinfo.GetMounts(mountinfo.SingleEntryFilter(mountpoint))
		if err != nil {
			e1 <- err
			return
		}
		if len(entries) == 0 {
			e1 <- fmt.Errorf("Mountpoint not found")
			return
		}
		c1 <- entries[0]
	}()

	select {
	case result := <-c1:
		return result, nil
	case err := <-e1:
		return nil, err
	case <-time.After(timeoutD):
		return nil, fmt.Errorf("getMount operation timed out after %s milliseconds", timeoutD)
	}

}

func splitMountInfo(s string) []string {
	quoted := false
	return strings.FieldsFunc(s, func(r rune) bool {
		if r == '"' {
			quoted = !quoted
		}
		return !quoted && r == ','
	})
}
goss-0.4.9/system/mount_posix.go000066400000000000000000000006611467505051300167470ustar00rootroot00000000000000//go:build linux || darwin || !windows
// +build linux darwin !windows

package system

import (
	"math"
	"syscall"
)

func getUsage(mountpoint string) (int, error) {
	statfsOut := &syscall.Statfs_t{}
	err := syscall.Statfs(mountpoint, statfsOut)
	if err != nil {
		return -1, err
	}

	percentageFree := float64(statfsOut.Bfree) / float64(statfsOut.Blocks)
	usage := math.Round((1 - percentageFree) * 100)

	return int(usage), nil
}
goss-0.4.9/system/mount_test.go000066400000000000000000000006041467505051300165610ustar00rootroot00000000000000package system

import (
	"testing"

	"gotest.tools/v3/assert"
)

func TestSplitMountInfo(t *testing.T) {
	in := "rw,context=\"system_u:object_r:container_file_t:s0:c174,c741\",size=65536k,mode=755"
	want := []string{
		"rw",
		"context=\"system_u:object_r:container_file_t:s0:c174,c741\"",
		"size=65536k",
		"mode=755",
	}

	got := splitMountInfo(in)

	assert.DeepEqual(t, got, want)
}
goss-0.4.9/system/mount_windows.go000066400000000000000000000002421467505051300172720ustar00rootroot00000000000000//go:build windows
// +build windows

package system

import "errors"

func getUsage(mountpoint string) (int, error) {
	return 0, errors.New("Not implemented")
}
goss-0.4.9/system/package.go000066400000000000000000000014431467505051300157550ustar00rootroot00000000000000package system

import (
	"context"
	"errors"

	"github.com/goss-org/goss/util"
)

type Package interface {
	Name() string
	Exists() (bool, error)
	Installed() (bool, error)
	Versions() ([]string, error)
}

var ErrNullPackage = errors.New("Could not detect Package type on this system, please use --package flag to explicity set it")

type NullPackage struct {
	name string
}

func NewNullPackage(_ context.Context, name string, system *System, config util.Config) Package {
	return &NullPackage{name: name}
}

func (p *NullPackage) Name() string { return p.name }

func (p *NullPackage) Exists() (bool, error) { return p.Installed() }

func (p *NullPackage) Installed() (bool, error) {
	return false, ErrNullPackage
}

func (p *NullPackage) Versions() ([]string, error) {
	return nil, ErrNullPackage
}
goss-0.4.9/system/package_alpine.go000066400000000000000000000023361467505051300173070ustar00rootroot00000000000000package system

import (
	"context"
	"errors"
	"strings"

	"github.com/goss-org/goss/util"
)

type AlpinePackage struct {
	name      string
	versions  []string
	loaded    bool
	installed bool
}

func NewAlpinePackage(_ context.Context, name string, system *System, config util.Config) Package {
	return &AlpinePackage{name: name}
}

func (p *AlpinePackage) setup() {
	if p.loaded {
		return
	}
	p.loaded = true
	cmd := util.NewCommand("apk", "version", p.name)
	if err := cmd.Run(); err != nil {
		return
	}
	for _, l := range strings.Split(strings.TrimSpace(cmd.Stdout.String()), "\n") {
		if strings.HasPrefix(l, "Installed:") || strings.HasPrefix(l, "WARNING") {
			continue
		}
		ver := strings.TrimPrefix(strings.Fields(l)[0], p.name+"-")
		p.versions = append(p.versions, ver)
	}

	if len(p.versions) > 0 {
		p.installed = true
	}
}

func (p *AlpinePackage) Name() string {
	return p.name
}

func (p *AlpinePackage) Exists() (bool, error) { return p.Installed() }

func (p *AlpinePackage) Installed() (bool, error) {
	p.setup()

	return p.installed, nil
}

func (p *AlpinePackage) Versions() ([]string, error) {
	p.setup()
	if len(p.versions) == 0 {
		return p.versions, errors.New("Package version not found")
	}
	return p.versions, nil
}
goss-0.4.9/system/package_deb.go000066400000000000000000000023371467505051300165720ustar00rootroot00000000000000package system

import (
	"context"
	"errors"
	"strings"

	"github.com/goss-org/goss/util"
)

type DebPackage struct {
	name      string
	versions  []string
	loaded    bool
	installed bool
}

func NewDebPackage(_ context.Context, name string, system *System, config util.Config) Package {
	return &DebPackage{name: name}
}

func (p *DebPackage) setup() {
	if p.loaded {
		return
	}
	p.loaded = true
	cmd := util.NewCommand("dpkg-query", "-f", "${Status} ${Version}\n", "-W", p.name)
	if err := cmd.Run(); err != nil {
		return
	}
	for _, l := range strings.Split(strings.TrimSpace(cmd.Stdout.String()), "\n") {
		if !(strings.HasPrefix(l, "install ok installed") || strings.HasPrefix(l, "hold ok installed")) {
			continue
		}
		ver := strings.Fields(l)[3]
		p.versions = append(p.versions, ver)
	}

	if len(p.versions) > 0 {
		p.installed = true
	}
}

func (p *DebPackage) Name() string {
	return p.name
}

func (p *DebPackage) Exists() (bool, error) { return p.Installed() }

func (p *DebPackage) Installed() (bool, error) {
	p.setup()

	return p.installed, nil
}

func (p *DebPackage) Versions() ([]string, error) {
	p.setup()
	if len(p.versions) == 0 {
		return p.versions, errors.New("Package version not found")
	}
	return p.versions, nil
}
goss-0.4.9/system/package_pacman.go000066400000000000000000000022371467505051300172760ustar00rootroot00000000000000package system

import (
	"context"
	"errors"
	"strings"

	"github.com/goss-org/goss/util"
)

type PacmanPackage struct {
	name      string
	versions  []string
	loaded    bool
	installed bool
}

func NewPacmanPackage(_ context.Context, name string, system *System, config util.Config) Package {
	return &PacmanPackage{name: name}
}

func (p *PacmanPackage) setup() {
	if p.loaded {
		return
	}
	p.loaded = true
	// TODO: extract versions
	cmd := util.NewCommand("pacman", "-Q", "--color", "never", "--noconfirm", p.name)
	if err := cmd.Run(); err != nil {
		return
	}
	p.installed = true
	// the output format is "pkgname version\n", so if we split the string on
	// whitespace, the version is the second item.
	p.versions = []string{strings.Fields(cmd.Stdout.String())[1]}
}

func (p *PacmanPackage) Name() string {
	return p.name
}

func (p *PacmanPackage) Exists() (bool, error) { return p.Installed() }

func (p *PacmanPackage) Installed() (bool, error) {
	p.setup()

	return p.installed, nil
}

func (p *PacmanPackage) Versions() ([]string, error) {
	p.setup()
	if len(p.versions) == 0 {
		return p.versions, errors.New("Package version not found")
	}
	return p.versions, nil
}
goss-0.4.9/system/package_rpm.go000066400000000000000000000020751467505051300166350ustar00rootroot00000000000000package system

import (
	"context"
	"errors"
	"strings"

	"github.com/goss-org/goss/util"
)

type RpmPackage struct {
	name      string
	versions  []string
	loaded    bool
	installed bool
}

func NewRpmPackage(_ context.Context, name string, system *System, config util.Config) Package {
	return &RpmPackage{name: name}
}

func (p *RpmPackage) setup() {
	if p.loaded {
		return
	}
	p.loaded = true
	cmd := util.NewCommand("rpm", "-q", "--nosignature", "--nohdrchk", "--nodigest", "--qf", "%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\n", p.name)
	if err := cmd.Run(); err != nil {
		return
	}
	p.installed = true
	p.versions = strings.Split(strings.TrimSpace(cmd.Stdout.String()), "\n")
}

func (p *RpmPackage) Name() string {
	return p.name
}

func (p *RpmPackage) Exists() (bool, error) { return p.Installed() }

func (p *RpmPackage) Installed() (bool, error) {
	p.setup()

	return p.installed, nil
}

func (p *RpmPackage) Versions() ([]string, error) {
	p.setup()
	if len(p.versions) == 0 {
		return p.versions, errors.New("Package version not found")
	}
	return p.versions, nil
}
goss-0.4.9/system/package_test.go000066400000000000000000000004321467505051300170110ustar00rootroot00000000000000package system

import (
	"testing"
)

func TestIsSupportedPackageManager(t *testing.T) {
	if IsSupportedPackageManager("na") {
		t.Fatal("na should not be a valid package manager")
	}

	if !IsSupportedPackageManager("rpm") {
		t.Fatal("rpm should be a valid package manager")
	}
}
goss-0.4.9/system/port.go000066400000000000000000000050241467505051300153450ustar00rootroot00000000000000package system

import (
	"context"
	"strconv"
	"strings"

	"github.com/goss-org/GOnetstat"
	"github.com/goss-org/goss/util"
)

type Port interface {
	Port() string
	Exists() (bool, error)
	Listening() (bool, error)
	IP() ([]string, error)
}

type DefPort struct {
	port     string
	sysPorts map[string][]GOnetstat.Process
}

func NewDefPort(_ context.Context, port string, system *System, config util.Config) Port {
	p := normalizePort(port)
	return &DefPort{
		port:     p,
		sysPorts: system.Ports(),
	}
}

func splitPort(fullport string) (network, port string) {
	split := strings.SplitN(fullport, ":", 2)
	if len(split) == 2 {
		return split[0], split[1]
	}
	return "tcp", fullport

}

func normalizePort(fullport string) string {
	net, addr := splitPort(fullport)
	return net + ":" + addr
}

func (p *DefPort) Port() string {
	return p.port
}

func (p *DefPort) Exists() (bool, error) { return p.Listening() }

func (p *DefPort) Listening() (bool, error) {
	if _, ok := p.sysPorts[p.port]; ok {
		return true, nil
	}
	return false, nil
}

func (p *DefPort) IP() ([]string, error) {
	var ips []string
	for _, entry := range p.sysPorts[p.port] {
		ips = append(ips, entry.Ip)
	}
	return ips, nil
}

// FIXME: Is there a better way to do this rather than ignoring errors?
func GetPorts(lookupPids bool) map[string][]GOnetstat.Process {
	ports := make(map[string][]GOnetstat.Process)
	netstat, _ := GOnetstat.Tcp(lookupPids)
	var net string
	// netPorts := make(map[string]GOnetstat.Process)
	// ports["tcp"] = netPorts
	net = "tcp"
	for _, entry := range netstat {
		if entry.State == "LISTEN" {
			port := strconv.FormatInt(entry.Port, 10)
			ports[net+":"+port] = append(ports[net+":"+port], entry)
		}
	}
	netstat, _ = GOnetstat.Tcp6(lookupPids)
	// netPorts = make(map[string]GOnetstat.Process)
	// ports["tcp6"] = netPorts
	net = "tcp6"
	for _, entry := range netstat {
		if entry.State == "LISTEN" {
			port := strconv.FormatInt(entry.Port, 10)
			ports[net+":"+port] = append(ports[net+":"+port], entry)
		}
	}
	netstat, _ = GOnetstat.Udp(lookupPids)
	// netPorts = make(map[string]GOnetstat.Process)
	// ports["udp"] = netPorts
	net = "udp"
	for _, entry := range netstat {
		port := strconv.FormatInt(entry.Port, 10)
		ports[net+":"+port] = append(ports[net+":"+port], entry)
	}
	netstat, _ = GOnetstat.Udp6(lookupPids)
	// netPorts = make(map[string]GOnetstat.Process)
	// ports["udp6"] = netPorts
	net = "udp6"
	for _, entry := range netstat {
		port := strconv.FormatInt(entry.Port, 10)
		ports[net+":"+port] = append(ports[net+":"+port], entry)
	}
	return ports
}
goss-0.4.9/system/process.go000066400000000000000000000025041467505051300160370ustar00rootroot00000000000000package system

import (
	"context"

	"github.com/goss-org/go-ps"
	"github.com/goss-org/goss/util"
)

type Process interface {
	Executable() string
	Exists() (bool, error)
	Running() (bool, error)
	Pids() ([]int, error)
}

type DefProcess struct {
	executable string
	procMap    map[string][]ps.Process
	err        error
}

func NewDefProcess(_ context.Context, executable string, system *System, config util.Config) Process {
	pmap, err := system.ProcMap()
	return &DefProcess{
		executable: executable,
		procMap:    pmap,
		err:        err,
	}
}

func (p *DefProcess) Executable() string {
	return p.executable
}

func (p *DefProcess) Exists() (bool, error) { return p.Running() }

func (p *DefProcess) Pids() ([]int, error) {
	var pids []int
	if p.err != nil {
		return pids, p.err
	}
	for _, proc := range p.procMap[p.executable] {
		pids = append(pids, proc.Pid())
	}
	return pids, nil
}

func (p *DefProcess) Running() (bool, error) {
	if p.err != nil {
		return false, p.err
	}
	if _, ok := p.procMap[p.executable]; ok {
		return true, nil
	}
	return false, nil
}

func GetProcs() (map[string][]ps.Process, error) {
	pmap := make(map[string][]ps.Process)
	processes, err := ps.Processes()
	if err != nil {
		return pmap, err
	}
	for _, p := range processes {
		pmap[p.Executable()] = append(pmap[p.Executable()], p)
	}

	return pmap, nil
}
goss-0.4.9/system/service.go000066400000000000000000000004051467505051300160170ustar00rootroot00000000000000package system

import "strings"

type Service interface {
	Service() string
	Exists() (bool, error)
	Enabled() (bool, error)
	Running() (bool, error)
	RunLevels() ([]string, error)
}

func invalidService(s string) bool {
	return strings.ContainsRune(s, '/')
}
goss-0.4.9/system/service_init.go000066400000000000000000000046731467505051300170550ustar00rootroot00000000000000package system

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"regexp"

	"github.com/goss-org/goss/util"
)

type ServiceInit struct {
	service  string
	alpine   bool
	runlevel string
}

func NewServiceInit(_ context.Context, service string, system *System, config util.Config) Service {
	return &ServiceInit{service: service}
}

func NewAlpineServiceInit(_ context.Context, service string, system *System, config util.Config) Service {
	runlevel := config.RunLevel
	if runlevel == "" {
		runlevel = "sysinit"
	}
	return &ServiceInit{service: service, alpine: true, runlevel: runlevel}
}

func (s *ServiceInit) Service() string {
	return s.service
}

func (s *ServiceInit) Exists() (bool, error) {
	if invalidService(s.service) {
		return false, nil
	}
	if _, err := os.Stat(fmt.Sprintf("/etc/init.d/%s", s.service)); err == nil {
		return true, err
	}
	return false, nil
}

func (s *ServiceInit) Enabled() (bool, error) {
	if invalidService(s.service) {
		return false, nil
	}
	var runLevels []string
	var err error
	if s.alpine {
		runLevels, err = alpineServiceRunLevels(s.service)
	} else {
		runLevels, err = initServiceRunLevels(s.service)
	}
	return len(runLevels) != 0, err
}

func (s *ServiceInit) RunLevels() ([]string, error) {
	if invalidService(s.service) {
		return nil, nil
	}
	if s.alpine {
		return alpineServiceRunLevels(s.service)
	} else {
		return initServiceRunLevels(s.service)
	}
}

func (s *ServiceInit) Running() (bool, error) {
	if invalidService(s.service) {
		return false, nil
	}
	cmd := util.NewCommand("service", s.service, "status")
	cmd.Run()
	if cmd.Status == 0 {
		return true, cmd.Err
	}
	return false, nil
}

func initServiceRunLevels(service string) ([]string, error) {
	var runLevels []string
	matches, err := filepath.Glob(fmt.Sprintf("/etc/rc*.d/S[0-9][0-9]%s", service))
	if err != nil {
		return nil, err
	}
	re := regexp.MustCompile("/etc/rc([0-9]+).d/")
	for _, m := range matches {
		matches := re.FindStringSubmatch(m)
		if matches != nil {
			runLevels = append(runLevels, matches[1])
		}
	}
	return runLevels, nil
}

func alpineServiceRunLevels(service string) ([]string, error) {
	var runLevels []string
	matches, err := filepath.Glob(fmt.Sprintf("/etc/runlevels/*/%s", service))
	if err != nil {
		return nil, err
	}
	re := regexp.MustCompile("/etc/runlevels/([^/]+)")
	for _, m := range matches {
		matches := re.FindStringSubmatch(m)
		if matches != nil {
			runLevels = append(runLevels, matches[1])
		}
	}
	return runLevels, nil
}
goss-0.4.9/system/service_systemd.go000066400000000000000000000036131467505051300175730ustar00rootroot00000000000000package system

import (
	"context"
	"fmt"
	"strings"

	"github.com/goss-org/goss/util"
)

type ServiceSystemd struct {
	service string
	legacy  bool
}

func NewServiceSystemd(_ context.Context, service string, system *System, config util.Config) Service {
	return &ServiceSystemd{
		service: service,
	}
}

func NewServiceSystemdLegacy(_ context.Context, service string, system *System, config util.Config) Service {
	return &ServiceSystemd{
		service: service,
		legacy:  true,
	}
}

func (s *ServiceSystemd) Service() string {
	return s.service
}

func (s *ServiceSystemd) Exists() (bool, error) {
	if invalidService(s.service) {
		return false, nil
	}
	cmd := util.NewCommand("systemctl", "-q", "list-unit-files", "--type=service")
	cmd.Run()
	if strings.Contains(cmd.Stdout.String(), fmt.Sprintf("%s.service", s.service)) {
		return true, cmd.Err
	}
	if s.legacy {
		// Fallback on sysv
		sysv := &ServiceInit{service: s.service}
		if e, err := sysv.Exists(); e && err == nil {
			return true, nil
		}
	}
	return false, nil
}

func (s *ServiceSystemd) Enabled() (bool, error) {
	if invalidService(s.service) {
		return false, nil
	}
	cmd := util.NewCommand("systemctl", "-q", "is-enabled", s.service)
	cmd.Run()
	if cmd.Status == 0 {
		return true, cmd.Err
	}
	if s.legacy {
		// Fallback on sysv
		sysv := &ServiceInit{service: s.service}
		if en, err := sysv.Enabled(); en && err == nil {
			return true, nil
		}
	}
	return false, nil
}

func (s *ServiceSystemd) Running() (bool, error) {
	if invalidService(s.service) {
		return false, nil
	}
	cmd := util.NewCommand("systemctl", "-q", "is-active", s.service)
	cmd.Run()
	if cmd.Status == 0 {
		return true, cmd.Err
	}
	if s.legacy {
		// Fallback on sysv
		sysv := &ServiceInit{service: s.service}
		if r, err := sysv.Running(); r && err == nil {
			return true, nil
		}
	}
	return false, nil
}

func (s *ServiceSystemd) RunLevels() ([]string, error) {
	return nil, nil
}
goss-0.4.9/system/service_upstart.go000066400000000000000000000037731467505051300176140ustar00rootroot00000000000000package system

import (
	"bufio"
	"context"
	"fmt"
	"os"
	"regexp"
	"strings"

	"github.com/goss-org/goss/util"
)

type ServiceUpstart struct {
	service string
}

var upstartEnabled = regexp.MustCompile(`^\s*start on`)
var upstartDisabled = regexp.MustCompile(`^manual`)

func NewServiceUpstart(_ context.Context, service string, system *System, config util.Config) Service {
	return &ServiceUpstart{service: service}
}

func (s *ServiceUpstart) Service() string {
	return s.service
}

func (s *ServiceUpstart) Exists() (bool, error) {
	// upstart
	if _, err := os.Stat(fmt.Sprintf("/etc/init/%s.conf", s.service)); err == nil {
		return true, nil
	}
	// Fallback on sysv
	sysv := &ServiceInit{service: s.service}
	if e, err := sysv.Exists(); e && err == nil {
		return true, nil
	}
	return false, nil
}

func (s *ServiceUpstart) Enabled() (bool, error) {
	if fh, err := os.Open(fmt.Sprintf("/etc/init/%s.override", s.service)); err == nil {
		scanner := bufio.NewScanner(fh)
		for scanner.Scan() {
			line := scanner.Text()
			if upstartDisabled.MatchString(line) {
				return false, nil
			}
		}
	}

	// If no /etc/init/.override with `manual` keyword in it has been found
	// Check the contents of the upstart manifest.
	if fh, err := os.Open(fmt.Sprintf("/etc/init/%s.conf", s.service)); err == nil {
		scanner := bufio.NewScanner(fh)
		for scanner.Scan() {
			line := scanner.Text()
			if upstartEnabled.MatchString(line) {
				return true, nil
			}
		}
	}
	// Fallback on sysv
	sysv := &ServiceInit{service: s.service}
	if en, err := sysv.Enabled(); en && err == nil {
		return true, nil
	}
	return false, nil
}

func (s *ServiceUpstart) Running() (bool, error) {
	cmd := util.NewCommand("service", s.service, "status")
	cmd.Run()
	out := cmd.Stdout.String()
	if cmd.Status == 0 && (strings.Contains(out, "running") || strings.Contains(out, "online")) {
		return true, cmd.Err
	}
	return false, nil
}
func (s *ServiceUpstart) RunLevels() ([]string, error) {
	sysv := &ServiceInit{service: s.service}
	return sysv.RunLevels()
}
goss-0.4.9/system/system.go000066400000000000000000000142011467505051300157020ustar00rootroot00000000000000package system

import (
	"bytes"
	"context"
	"os"
	"os/exec"
	"strconv"
	"sync"

	"github.com/goss-org/GOnetstat"
	// This needs a better name
	"github.com/goss-org/go-ps"

	util2 "github.com/goss-org/goss/util"
)

type Resource interface {
	Exists() (bool, error)
}

type System struct {
	NewPackage     func(context.Context, string, *System, util2.Config) Package
	NewFile        func(context.Context, string, *System, util2.Config) File
	NewAddr        func(context.Context, string, *System, util2.Config) Addr
	NewPort        func(context.Context, string, *System, util2.Config) Port
	NewService     func(context.Context, string, *System, util2.Config) Service
	NewUser        func(context.Context, string, *System, util2.Config) User
	NewGroup       func(context.Context, string, *System, util2.Config) Group
	NewCommand     func(context.Context, string, *System, util2.Config) Command
	NewDNS         func(context.Context, string, *System, util2.Config) DNS
	NewProcess     func(context.Context, string, *System, util2.Config) Process
	NewGossfile    func(context.Context, string, *System, util2.Config) Gossfile
	NewKernelParam func(context.Context, string, *System, util2.Config) KernelParam
	NewMount       func(context.Context, string, *System, util2.Config) Mount
	NewInterface   func(context.Context, string, *System, util2.Config) Interface
	NewHTTP        func(context.Context, string, *System, util2.Config) HTTP
	ports          map[string][]GOnetstat.Process
	portsOnce      sync.Once
	procMap        map[string][]ps.Process
	procOnce       sync.Once
}

func (s *System) Ports() map[string][]GOnetstat.Process {
	s.portsOnce.Do(func() {
		s.ports = GetPorts(false)
	})
	return s.ports
}

func (s *System) ProcMap() (map[string][]ps.Process, error) {
	var err error

	s.procOnce.Do(func() {
		s.procMap, err = GetProcs()
	})

	return s.procMap, err
}

func New(packageManager string) *System {
	sys := &System{
		NewFile:        NewDefFile,
		NewAddr:        NewDefAddr,
		NewPort:        NewDefPort,
		NewUser:        NewDefUser,
		NewGroup:       NewDefGroup,
		NewCommand:     NewDefCommand,
		NewDNS:         NewDefDNS,
		NewProcess:     NewDefProcess,
		NewGossfile:    NewDefGossfile,
		NewKernelParam: NewDefKernelParam,
		NewMount:       NewDefMount,
		NewInterface:   NewDefInterface,
		NewHTTP:        NewDefHTTP,
	}

	sys.detectService()
	sys.detectPackage(packageManager)

	return sys
}

// detectPackage adds the correct package creation function to a System struct
func (sys *System) detectPackage(p string) {
	if p != "dpkg" && p != "apk" && p != "pacman" && p != "rpm" {
		p = DetectPackageManager()
	}
	switch p {
	case "dpkg":
		sys.NewPackage = NewDebPackage
	case "apk":
		sys.NewPackage = NewAlpinePackage
	case "pacman":
		sys.NewPackage = NewPacmanPackage
	default:
		sys.NewPackage = NewRpmPackage
	}
}

// detectService adds the correct service creation function to a System struct
func (sys *System) detectService() {
	switch DetectService() {
	case "upstart":
		sys.NewService = NewServiceUpstart
	case "systemd":
		sys.NewService = NewServiceSystemd
	case "systemdlegacy":
		sys.NewService = NewServiceSystemdLegacy
	case "alpineinit":
		sys.NewService = NewAlpineServiceInit
	default:
		sys.NewService = NewServiceInit
	}
}

// SupportedPackageManagers is a list of package managers we support
func SupportedPackageManagers() []string {
	return []string{"apk", "dpkg", "pacman", "rpm"}
}

// IsSupportedPackageManager determines if p is a supported package manager
func IsSupportedPackageManager(p string) bool {
	for _, m := range SupportedPackageManagers() {
		if m == p {
			return true
		}
	}

	return false
}

// DetectPackageManager attempts to detect whether or not the system is using
// "dpkg", "rpm", "apk", or "pacman" package managers. It first attempts to
// detect the distro. If that fails, it falls back to finding package manager
// executables. If that fails, it returns the empty string.
func DetectPackageManager() string {
	switch DetectDistro() {
	case "ubuntu":
		return "dpkg"
	case "redhat":
		return "rpm"
	case "alpine":
		return "apk"
	case "arch":
		return "pacman"
	case "debian":
		return "dpkg"
	}
	for _, manager := range []string{"dpkg", "rpm", "apk", "pacman"} {
		if HasCommand(manager) {
			return manager
		}
	}
	return ""
}

// DetectService attempts to detect what kind of service management the system
// is using, "systemd", "upstart", "alpineinit", or "init". It looks for systemctl
// command to detect systemd, and falls back on DetectDistro otherwise. If it can't
// decide, it returns "init".
func DetectService() string {
	if HasCommand("systemctl") {
		if isLegacySystemd() {
			return "systemdlegacy"
		}
		return "systemd"
	}
	// Centos Docker container doesn't run systemd, so we detect it or use init.
	switch DetectDistro() {
	case "ubuntu":
		return "upstart"
	case "alpine":
		return "alpineinit"
	case "arch":
		return "systemd"
	}
	return "init"
}

// DetectDistro attempts to detect which Linux distribution this computer is
// using. One of "ubuntu", "redhat" (including Centos), "alpine", "arch", or
// "debian". If it can't decide, it returns an empty string.
func DetectDistro() string {
	if b, e := os.ReadFile("/etc/lsb-release"); e == nil && bytes.Contains(b, []byte("Ubuntu")) {
		return "ubuntu"
	} else if isRedhat() {
		return "redhat"
	} else if _, err := os.Stat("/etc/alpine-release"); err == nil {
		return "alpine"
	} else if _, err := os.Stat("/etc/arch-release"); err == nil {
		return "arch"
	} else if _, err := os.Stat("/etc/debian_version"); err == nil {
		return "debian"
	}
	return ""
}

// HasCommand returns whether or not an executable by this name is on the PATH.
func HasCommand(cmd string) bool {
	if _, err := exec.LookPath(cmd); err == nil {
		return true
	}
	return false
}

func isLegacySystemd() bool {
	if b, err := os.ReadFile("/etc/debian_version"); err == nil {
		i := bytes.Index(b, []byte("."))
		if i < 0 {
			return false
		}
		if major, err := strconv.Atoi(string(b[:i])); err == nil {
			return major < 9
		}
	}
	return false
}

func isRedhat() bool {
	if _, err := os.Stat("/etc/redhat-release"); err == nil {
		return true
	} else if _, err := os.Stat("/etc/system-release"); err == nil {
		return true
	}
	return false
}
goss-0.4.9/system/system_test.go000066400000000000000000000021751467505051300167500ustar00rootroot00000000000000package system

import (
	"reflect"
	"runtime"
	"testing"
)

type noInputs func() string

// test that a function with no inputs returns one of the expected strings
func testOutputs(f noInputs, validOutputs []string, t *testing.T) {
	output := f()
	// use reflect to get the name of the function
	name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
	failed := true
	for _, valid := range validOutputs {
		if output == valid {
			failed = false
		}
	}
	if failed {
		t.Errorf("Function %v returned %v, which is not one of %v", name, output, validOutputs)
	}
}

func TestPackageManager(t *testing.T) {
	t.Parallel()
	testOutputs(
		DetectPackageManager,
		[]string{"dpkg", "rpm", "apk", "pacman", ""},
		t,
	)
}

func TestDetectService(t *testing.T) {
	t.Parallel()
	testOutputs(
		DetectService,
		[]string{"systemd", "init", "alpineinit", "upstart", ""},
		t,
	)
}

func TestDetectDistro(t *testing.T) {
	t.Parallel()
	testOutputs(
		DetectDistro,
		[]string{"ubuntu", "redhat", "alpine", "arch", "debian", ""},
		t,
	)
}

func TestHasCommand(t *testing.T) {
	t.Parallel()
	if !HasCommand("sh") {
		t.Error("System didn't have sh!")
	}
}
goss-0.4.9/system/user.go000066400000000000000000000023011467505051300153320ustar00rootroot00000000000000package system

import (
	"context"
	"os/user"
	"strconv"

	"github.com/goss-org/goss/util"
)

type User interface {
	Username() string
	Exists() (bool, error)
	UID() (int, error)
	GID() (int, error)
	Groups() ([]string, error)
	Home() (string, error)
	Shell() (string, error)
}

type DefUser struct {
	username string
}

func NewDefUser(_ context.Context, username string, system *System, config util.Config) User {
	return &DefUser{username: username}
}

func (u *DefUser) Username() string {
	return u.username
}

func (u *DefUser) Exists() (bool, error) {
	_, err := user.Lookup(u.username)
	if err != nil {
		return false, nil
	}
	return true, nil
}

func (u *DefUser) UID() (int, error) {
	user, err := user.Lookup(u.username)
	if err != nil {
		return 0, err
	}

	uid, err := strconv.Atoi(user.Uid)
	if err != nil {
		return 0, err
	}

	return uid, nil
}

func (u *DefUser) GID() (int, error) {
	user, err := user.Lookup(u.username)
	if err != nil {
		return 0, err
	}

	gid, err := strconv.Atoi(user.Gid)
	if err != nil {
		return 0, err
	}

	return gid, nil
}

func (u *DefUser) Home() (string, error) {
	user, err := user.Lookup(u.username)
	if err != nil {
		return "", err
	}

	return user.HomeDir, nil
}
goss-0.4.9/system/user_group_unix.go000066400000000000000000000023251467505051300176170ustar00rootroot00000000000000//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd linux netbsd openbsd solaris

package system

import (
	"bufio"
	"io"
	"os"
	"sort"
	"strconv"
	"strings"
)

func groupsForUser(user string, pgid int, grp io.Reader) ([]string, error) {
	s := bufio.NewScanner(grp)
	out := []string{}

	for s.Scan() {
		if err := s.Err(); err != nil {
			return nil, err
		}

		text := s.Text()
		if text == "" {
			continue
		}

		// see: man 5 group
		//  group_name:password:GID:user_list
		// Name:Pass:Gid:List
		//  root:x:0:root
		//  adm:x:4:root,adm,daemon
		parts := strings.Split(text, ":")
		if len(parts) != 4 {
			continue
		}

		gid, err := strconv.Atoi(parts[2])
		if err == nil {
			if gid == pgid {
				out = append(out, parts[0])
				continue
			}
		}

		for _, g := range strings.Split(parts[3], ",") {
			if g == user {
				out = append(out, parts[0])
				continue
			}
		}
	}

	sort.Strings(out)

	return out, nil
}

func (u *DefUser) Groups() ([]string, error) {
	grp, err := os.Open("/etc/group")
	if err != nil {
		return nil, err
	}
	defer grp.Close()

	pgid, err := u.GID()
	if err != nil {
		return nil, err
	}

	return groupsForUser(u.username, pgid, grp)
}
goss-0.4.9/system/user_group_unix_test.go000066400000000000000000000020161467505051300206530ustar00rootroot00000000000000//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd linux netbsd openbsd solaris

package system

import (
	"strings"
	"testing"
)

func TestGroupsForUser(t *testing.T) {
	grp := `badline
testgrp1:*:100:bob,jack,jill
testgrp2:*:101:bob,jack
testgrp3:*:102:jill
testgrp4:*:103:`

	var cases = []struct {
		user   string
		gid    int
		expect []string
	}{
		{"bob", 100, []string{"testgrp1", "testgrp2"}},
		{"jack", 100, []string{"testgrp1", "testgrp2"}},
		{"jill", 103, []string{"testgrp1", "testgrp3", "testgrp4"}},
		{"other", 103, []string{"testgrp4"}},
		{"other", 105, []string{}},
	}

	for _, c := range cases {
		res, err := groupsForUser(c.user, c.gid, strings.NewReader(grp))
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(res) != len(c.expect) {
			t.Fatalf("result %#v does not match %#v", res, c.expect)
		}
		for i, e := range c.expect {
			if res[i] != e {
				t.Fatalf("result %#v does not match %#v", res, c.expect)
			}
		}
	}
}
goss-0.4.9/system/user_group_windows.go000066400000000000000000000010351467505051300203230ustar00rootroot00000000000000package system

import (
	"fmt"
	"os/user"
	"sort"
)

func (u *DefUser) Groups() ([]string, error) {
	usr, err := user.Lookup(u.username)
	if err != nil {
		return nil, err
	}

	var groupList []string
	ids, err := usr.GroupIds()
	if err != nil {
		return nil, err
	}

	for _, gid := range ids {
		group, err := user.LookupGroupId(gid)
		if err != nil {
			return nil, fmt.Errorf("Unable to find groups for user %v: %v", usr.Username, err)
		}
		groupList = append(groupList, group.Name)
	}

	sort.Strings(groupList)
	return groupList, nil
}
goss-0.4.9/system/user_unix.go000066400000000000000000000012701467505051300164010ustar00rootroot00000000000000//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd linux netbsd openbsd solaris

package system

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func (u *DefUser) Shell() (string, error) {
	passwd, err := os.Open("/etc/passwd")
	if err != nil {
		return "", err
	}
	defer passwd.Close()

	lines := bufio.NewReader(passwd)

	for {
		line, _, err := lines.ReadLine()
		if err != nil {
			break
		}

		fs := strings.Split(string(line), ":")
		if len(fs) != 7 {
			return "", fmt.Errorf("invalid entry in /etc/passwd")
		}

		if fs[0] == u.username {
			return fs[6], nil
		}
	}

	return "", fmt.Errorf("unknown user %s", u.username)
}
goss-0.4.9/system/user_unsupported.go000066400000000000000000000004521467505051300200070ustar00rootroot00000000000000//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris

package system

import (
	"fmt"
)

func (u *DefUser) Shell() (string, error) {
	return "", fmt.Errorf("unsupported operating system")
}
goss-0.4.9/template.go000066400000000000000000000051621467505051300146530ustar00rootroot00000000000000package goss

import (
	"bytes"
	"fmt"
	"os"
	"regexp"
	"strconv"
	"strings"
	"text/template"

	"github.com/Masterminds/sprig/v3"
)

// TemplateFilter is the type of the Goss Template Filter which include custom variables and functions.
type TemplateFilter func([]byte) ([]byte, error)

// NewTemplateFilter creates a new Template Filter based in the file and inline variables.
func NewTemplateFilter(varsFile string, varsInline string) (func([]byte) ([]byte, error), error) {
	vars, err := loadVars(varsFile, varsInline)
	if err != nil {
		return nil, fmt.Errorf("failed while loading vars file %q: %v", varsFile, err)
	}

	tVars := &TmplVars{Vars: vars}

	f := func(data []byte) ([]byte, error) {
		t := template.New("test").Funcs(sprig.TxtFuncMap()).Funcs(funcMap)

		tmpl, err := t.Parse(string(data))
		if err != nil {
			return []byte{}, err
		}

		tmpl.Option("missingkey=error")
		var doc bytes.Buffer

		err = tmpl.Execute(&doc, tVars)
		if err != nil {
			return []byte{}, err
		}

		return doc.Bytes(), nil
	}

	return f, nil
}

func mkSlice(args ...any) []any {
	return args
}

func readFile(f string) (string, error) {
	b, err := os.ReadFile(f)
	if err != nil {
		return "", err

	}
	return strings.TrimSpace(string(b)), nil
}

func getEnv(key string, def ...string) string {
	val := os.Getenv(key)
	if val == "" && len(def) > 0 {
		return def[0]
	}

	return os.Getenv(key)
}

func regexMatch(re, s string) (bool, error) {
	compiled, err := regexp.Compile(re)
	if err != nil {
		return false, err
	}

	return compiled.MatchString(s), nil
}

// return named parenthesized subexpresions, if received, or stringfied (Sprig "get" need strings) keys like array
func findStringSubmatch(pattern, input string) map[string]interface{} {
	re := regexp.MustCompile(pattern)
	els := re.FindStringSubmatch(input)

	elsMap := make(map[string]interface{})
	elsMapNamed := make(map[string]interface{})

	// create always elsMaps but returns elsMapNamed if exists named parenthesized subexps
	for i := 0; i < len(els); i++ {
		// convert i to string according returned (https://github.com/goss-org/goss/pull/895#issuecomment-2075716706)
		elsMap[strconv.Itoa(i)] = els[i]

		if re.SubexpNames()[i] != "" {
			elsMapNamed[re.SubexpNames()[i]] = els[i]
		}
	}

	// returns elsMapNamed if exists named parenthesized subexps
	if len(elsMapNamed) > 0 {
		return elsMapNamed
	}
	return elsMap
}

var funcMap = template.FuncMap{
	"mkSlice":            mkSlice,
	"readFile":           readFile,
	"getEnv":             getEnv,
	"regexMatch":         regexMatch,
	"toUpper":            strings.ToUpper,
	"toLower":            strings.ToLower,
	"findStringSubmatch": findStringSubmatch,
}
goss-0.4.9/testdata/000077500000000000000000000000001467505051300143165ustar00rootroot00000000000000goss-0.4.9/testdata/failing.goss.yaml000066400000000000000000000002221467505051300175610ustar00rootroot00000000000000---
command:
  hello world:
    exit-status: 1
    exec: "echo hello world"
    stdout:
    - did not echo this
    stderr: []
    timeout: 10000
goss-0.4.9/testdata/matching_basic.yaml000066400000000000000000000050221467505051300201340ustar00rootroot00000000000000matching:
  # Basic matchers
  basic_string:
    content: 'this is a test'
    matches: 'this is a test'

  basic_string_oneline:
    content: |
      this is a test1
    matches: |
      this is a test1

  basic_string_multiline:
    content: |
      this is a test1
      this is a test2
      this is a test3
    matches: |
      this is a test1
      this is a test2
      this is a test3

  basic_string_regexp:
    content: 'this is a test'
    matches:
      match-regexp: '^this'

  basic_string_skip:
    skip: true
    content: 'this is a test'
    matches: 'this is a test'

  basic_semver:
    content: '1.2.3'
    matches:
      semver-constraint: '>=1.2.0'

  basic_int:
    content: 42
    matches: 42

  basic_array:
    content:
      - 'group1'
      - 'group2'
      - 'group3'
    matches:
      - 'group1'
      - 'group2'

  basic_array_matchers:
    content: [foo, bar, moo]
    matches:
      and:
        - contain-elements: [foo, bar]
        - [foo, bar] # same as above
        - equal: [foo, bar, moo] # order matters, exact match
        - consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers
        - contain-element:
            have-prefix: b
        - contain-element:
            have-suffix: r

  basic_reader:
    as-reader: true
    content: |
      foo bar
      moo cow
    matches:
      - 'foo'
      - '/^m.*w$/'
      - '!ftw'
      - '!/^ERROR:/'

  # Negated
  negated_basic_string:
    content: 'this is a test'
    matches:
      not: 'this is a failing test'

  negated_basic_string_regexp:
    content: 'this is a test'
    matches:
      not:
        match-regexp: '^foo'

  negated_basic_int:
    content: 42
    matches:
      not: 43

  negated_basic_array:
    content:
      - 'group1'
      - 'group2'
      - 'group3'
    matches:
      not:
        - 'group1'
        - 'group2'
        - 'group2'
        - 'group4'

  negated_basic_array_matchers:
    content: [foo, bar, moo]
    matches:
      and:
        - not:
            contain-elements: [fox, box]
        - not: [fox, bax] # same as above
        - not:
            equal: [fox, bax, mox] # order matters, exact match
        - not:
            consist-of: [have-suffix: x, have-prefix: t, box] # order doesn't matter, can use matchers
        - not:
            contain-element:
              have-prefix: x

  negated_basic_reader:
    as-reader: true
    content: |
      foo bar
      moo cow
    matches:
      not:
        contain-elements:
          - 'fox'
          - '/^t.*w$/'
          - '!foo'
          - '!/^foo/'
goss-0.4.9/testdata/matching_basic_failing.yaml000066400000000000000000000073011467505051300216270ustar00rootroot00000000000000matching:
  # Basic matchers
  basic_string:
    content: 'this is a test'
    matches: 'this is a failing test'

  basic_string_oneline:
    content: |
      this is a test1
    matches: |
      this is a test9

  basic_string_multiline:
    content: |
      this is a test1
      this is a test2
      this is a test3
    matches: |
      this is a test1
      this is a test9
      this is a test3

  basic_string_have_prefix:
    content: 'foo'
    matches:
      have-prefix: 'g'

  basic_string_have_suffix:
    content: 'foo'
    matches:
      have-suffix: 'x'

  basic_string_contain_substring:
    content: 'foo'
    matches:
      contain-substring: 'x'

  basic_string_regexp:
    content: 'this is a test'
    matches:
      match-regexp: '^foo'

  basic_semver:
    content: '1.2.3'
    matches:
      or:
       - semver-constraint: '>=9.9.0'

  basic_len:
    content: "123"
    matches:
      or:
       - have-len: 2

  basic_int:
    content: 42
    matches: 43

  basic_array:
    content:
      - 'group1'
      - 'group2'
      - 'group3'
    matches:
      - 'group1'
      - 'group2'
      - 'group2'
      - 'group4'

  basic_array_matchers:
    content: [foo, bar, moo]
    matches:
      or:
        - contain-elements: [fox, box]
        - [fox, bax] # same as above
        - equal: [fox, bax, mox] # order matters, exact match
        - consist-of: [fox, have-prefix: t, box] # order doesn't matter, can use matchers
        - contain-element:
            have-prefix: x
        - contain-element:
            have-suffix: x

  basic_array_consists_of:
    content: [foo, bar, moo]
    matches:
      consist-of: [fox, have-prefix: t, box] # order doesn't matter, can use matchers

  basic_reader:
    as-reader: true
    content: |
      foo bar
      moo cow
    matches:
      - 'fox'
      - '/^t.*w$/'
      - '!foo'
      - '!/^foo/'

  # Negated
  negated_basic_string:
    content: 'this is a test'
    matches:
      not: 'this is a test'

  negatedbasic_string_regexp:
    content: 'this is a test'
    matches:
      not:
        match-regexp: '^this'

  negatedbasic_string_have_prefix:
    content: 'foo'
    matches:
      not:
        have-prefix: 'f'

  negatedbasic_string_have_suffix:
    content: 'foo'
    matches:
      not:
        have-suffix: 'o'

  negatedbasic_string_contain_substring:
    content: 'foo'
    matches:
      not:
        contain-substring: 'oo'

  negatedbasic_len:
    content: '123'
    matches:
      not:
        have-len: 3

  negated_basic_int:
    content: 42
    matches:
      not: 42

  negated_and:
    content: 42
    matches:
      not:
        and:
          - 42
          - 42

  negated_basic_array:
    content:
      - 'group1'
      - 'group2'
      - 'group3'
    matches:
      not:
        - 'group1'
        - 'group2'
        - 'group3'

  negated_basic_array_matchers:
    content: [foo, bar, moo]
    matches:
      or:
        - not:
            contain-elements: [foo, bar]
        - not:
            [foo, bar] # same as above
        - not:
            equal: [foo, bar, moo] # order matters, exact match
        - not:
            consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers
        - not:
            contain-element:
              have-prefix: b

  negated_basic_array_contain_element:
    content: [foo, bar, moo]
    matches:
      not:
        contain-element: foo

  negated_basic_array_consists_of:
    content: [foo, bar, moo]
    matches:
      not:
        consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers

  negated_basic_reader:
    as-reader: true
    content: |
      foo bar
      moo cow
    matches:
      not:
        - 'foo'
        - '/^m.*w$/'
        - '!ftw'
        - '!/^ERROR:/'
goss-0.4.9/testdata/matching_transformers.yaml000066400000000000000000000047451467505051300216130ustar00rootroot00000000000000matching:
  basic_reader_as_array:
    as-reader: true
    content: |
      foo bar
      moo cow
    matches:
      and:
        - contain-element: {contain-substring: 'foo'}
        - contain-element: {match-regexp: '^m.*w$'}
        - not: {contain-substring: 'ftw'}
        - not: {match-regexp: '^ERROR:'}

  test_numeric_string:
    content: 128
    matches:
      and:
        - '128'
        - have-prefix: '1'
        - have-suffix: '8'
        - match-regexp: '\d{3}'

  test_string_numeric:
    content: '128'
    matches:
      and:
        - 128
        - 128.0
        - le: 128
        - gt: 120

  test_string_float:
    content: '128.3'
    matches:
      and:
        - 128.3
        - le: 129
        - gt: 120.2

  test_array:
    content:
      - '45'
      - '46'
      - '47'
    matches:
      - contain-element: {match-regexp: "4."}
      - '45'
      - and: [{ge: 46}, {le: 50}]

  test_reader_using_string_matchers:
    content: |
      foo bar
      15
      moo cow
    as-reader: true
    matches:
      and:
        - have-len: 19
        - |
          foo bar
          15
          moo cow
        - have-prefix: 'foo'
        - have-suffix: "cow\n"
        - contain-element:
            have-prefix: 'moo'
        - contain-elements:
          - not: 'this_doesnt_exist'
          - lt: 20
          - have-prefix: 'moo'

  test_reader_using_array:
    content: |
      foo bar
      15
      moo cow
    as-reader: true
    matches:
      - "foo bar"
      - "15"
      - "moo cow"


  test_reader_as_single_string:
    content: 'cool'
    as-reader: true
    matches: 'cool'

  test_reader_using_int_matchers:
    content: '40'
    as-reader: true
    matches:
      and:
       - le: 250
       - ge: 20


  test_gjson_transform:
    content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
    as-reader: true
    matches:
      gjson:
        moo.nested: cow
        foo: {have-prefix: b}
        count: {le: 25}
        '@this': {have-key: "foo"}
        moo:
          and:
            - have-key: "nested"
            - {not: {have-key: "nested2"}}

  test_gjson_using_this_and_equal:
    content: '{"foo": "bar", "baz": "bing"}'
    matches:
      gjson:
        '@this':
          equal:
            foo: bar
            baz: bing

  test_gjson_have_key_array:
    content: '{"arr": [{"nested": "cow"}, {"nested2": "moo"}]}'
    matches:
      gjson:
        arr:
          # or tests MarshalJSON
          or:
            - contain-elements:
              - have-key: 'nested'
goss-0.4.9/testdata/matching_transformers_failing.yaml000066400000000000000000000064671467505051300233070ustar00rootroot00000000000000matching:
  basic_reader_as_array:
    as-reader: true
    content: |
      foo bar
      moo cow
    matches:
      and:
        - contain-element: {contain-substring: 'fox'}
        - contain-element: {match-regexp: '^t.*w$'}
        - not: {contain-substring: 'foo'}
        - not: {match-regexp: '^foo'}

  test_numeric_string:
    content: 128
    matches:
      and:
        - '129'
        - have-prefix: '2'
        - have-suffix: '9'
        - match-regexp: '\s{3}'

  test_string_numeric:
    content: '128'
    matches:
      and:
        - 129
        - 129.1
        - le: 127
        - gt: 130

  test_string_float:
    content: '128.3'
    matches:
      and:
        - 129.3
        - le: 127
        - gt: 130.2

  test_array:
    content:
      - '45'
      - '46'
      - '47'
    matches:
      - contain-element: {match-regexp: "5."}
      - '55'
      - and: [{ge: 56}, {le: 30}]

  test_reader_using_string_matchers:
    content: |
      foo bar
      15
      moo cow
    as-reader: true
    matches:
      and:
        - have-len: 15
        - |
          fox bar
          15
          moo cow
        - have-prefix: 'fox'
        - have-suffix: "tow\n"
        - contain-element:
            have-prefix: 'too'
        - contain-elements:
          - not: 'moo cow'
          - lt: 10
          - have-prefix: 'tow'


  test_reader_as_single_string:
    content: 'cool'
    as-reader: true
    matches: 'not-cool'

  test_reader_using_int_matchers:
    content: '40'
    as-reader: true
    matches:
      and:
       - le: 20
       - ge: 50


  test_gjson_transform_simple:
    content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
    as-reader: true
    matches:
      gjson:
        moo.nested: cowx

  test_gjson_transform_nested_prefix:
    content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
    as-reader: true
    matches:
      gjson:
        foo: {have-prefix: x}

  test_gjson_transform_nested_count:
    content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
    as-reader: true
    matches:
      gjson:
        count: {le: 10}

  test_gjson_transform_nested_this:
    content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
    as-reader: true
    matches:
      gjson:
        '@this': {have-key: "nope"}

  test_gjson_transform_nested_and:
    content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
    as-reader: true
    matches:
      gjson:
        moo:
          and:
            - {have-key: "nope"}
            - {not: {have-key: "nested"}}

  test_gjson_transform_not_key:
    content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}'
    as-reader: true
    matches:
      gjson:
        moo:
          not:
            have-key: "nested"

  test_gjson_using_this_and_equal:
    content: '{"foo": "bar", "baz": "bing"}'
    matches:
      gjson:
        '@this':
          equal:
            fox: bar
            baz: bing

  test_gjson_have_key_array:
    content: '{"arr": [{"nested": "cow"}, {"nested2": "moo"}]}'
    matches:
      gjson:
        '@this':
          or:
            - have-key: "fail"

  test_gjson_not_found:
    content: '{"arr": [{"nested": "cow"}, {"nested2": "moo"}]}'
    matches:
      gjson:
        foo: 'bar'

  test_gjson_invalid:
    content: '{"arr"'
    matches:
      gjson:
        '@this':
            - have-key: "arr"
goss-0.4.9/testdata/out_matching_basic.0.documentation000066400000000000000000000035651467505051300231020ustar00rootroot00000000000000Matching: basic_array: matches: matches expectation: ["group1","group2"]
Matching: basic_array_matchers: matches: matches expectation: {"and":[{"contain-elements":["foo","bar"]},["foo","bar"],{"equal":["foo","bar","moo"]},{"consist-of":["foo",{"have-prefix":"m"},"bar"]},{"contain-element":{"have-prefix":"b"}},{"contain-element":{"have-suffix":"r"}}]}
Matching: basic_int: matches: matches expectation: 42
Matching: basic_reader: matches: matches expectation: ["foo","/^m.*w$/","!ftw","!/^ERROR:/"]
Matching: basic_semver: matches: matches expectation: {"semver-constraint":">=1.2.0"}
Matching: basic_string: matches: matches expectation: "this is a test"
Matching: basic_string_multiline: matches: matches expectation: "this is a test1\nthis is a test2\nthis is a test3\n"
Matching: basic_string_oneline: matches: matches expectation: "this is a test1\n"
Matching: basic_string_regexp: matches: matches expectation: {"match-regexp":"^this"}
Matching: basic_string_skip: matches: skipped
Matching: negated_basic_array: matches: matches expectation: {"not":["group1","group2","group2","group4"]}
Matching: negated_basic_array_matchers: matches: matches expectation: {"and":[{"not":{"contain-elements":["fox","box"]}},{"not":["fox","bax"]},{"not":{"equal":["fox","bax","mox"]}},{"not":{"consist-of":[{"have-suffix":"x"},{"have-prefix":"t"},"box"]}},{"not":{"contain-element":{"have-prefix":"x"}}}]}
Matching: negated_basic_int: matches: matches expectation: {"not":43}
Matching: negated_basic_reader: matches: matches expectation: {"not":{"contain-elements":["fox","/^t.*w$/","!foo","!/^foo/"]}}
Matching: negated_basic_string: matches: matches expectation: {"not":"this is a failing test"}
Matching: negated_basic_string_regexp: matches: matches expectation: {"not":{"match-regexp":"^foo"}}


Failures/Skipped:

Matching: basic_string_skip: matches: skipped

Total Duration: 
Count: 16, Failed: 0, Skipped: 1
goss-0.4.9/testdata/out_matching_basic.0.nagios000066400000000000000000000000671467505051300215030ustar00rootroot00000000000000GOSS OK - Count: 16, Failed: 0, Skipped: 1, Duration: 
goss-0.4.9/testdata/out_matching_basic.0.rspecish000066400000000000000000000002061467505051300220360ustar00rootroot00000000000000.........S......

Failures/Skipped:

Matching: basic_string_skip: matches: skipped

Total Duration: 
Count: 16, Failed: 0, Skipped: 1
goss-0.4.9/testdata/out_matching_basic.0.tap000066400000000000000000000036031467505051300210060ustar00rootroot000000000000001..16
ok 1 - Matching: basic_array: matches: matches expectation: ["group1","group2"]
ok 2 - Matching: basic_array_matchers: matches: matches expectation: {"and":[{"contain-elements":["foo","bar"]},["foo","bar"],{"equal":["foo","bar","moo"]},{"consist-of":["foo",{"have-prefix":"m"},"bar"]},{"contain-element":{"have-prefix":"b"}},{"contain-element":{"have-suffix":"r"}}]}
ok 3 - Matching: basic_int: matches: matches expectation: 42
ok 4 - Matching: basic_reader: matches: matches expectation: ["foo","/^m.*w$/","!ftw","!/^ERROR:/"]
ok 5 - Matching: basic_semver: matches: matches expectation: {"semver-constraint":">=1.2.0"}
ok 6 - Matching: basic_string: matches: matches expectation: "this is a test"
ok 7 - Matching: basic_string_multiline: matches: matches expectation: "this is a test1\nthis is a test2\nthis is a test3\n"
ok 8 - Matching: basic_string_oneline: matches: matches expectation: "this is a test1\n"
ok 9 - Matching: basic_string_regexp: matches: matches expectation: {"match-regexp":"^this"}
ok 10 - # SKIP Matching: basic_string_skip: matches: skipped
ok 11 - Matching: negated_basic_array: matches: matches expectation: {"not":["group1","group2","group2","group4"]}
ok 12 - Matching: negated_basic_array_matchers: matches: matches expectation: {"and":[{"not":{"contain-elements":["fox","box"]}},{"not":["fox","bax"]},{"not":{"equal":["fox","bax","mox"]}},{"not":{"consist-of":[{"have-suffix":"x"},{"have-prefix":"t"},"box"]}},{"not":{"contain-element":{"have-prefix":"x"}}}]}
ok 13 - Matching: negated_basic_int: matches: matches expectation: {"not":43}
ok 14 - Matching: negated_basic_reader: matches: matches expectation: {"not":{"contain-elements":["fox","/^t.*w$/","!foo","!/^foo/"]}}
ok 15 - Matching: negated_basic_string: matches: matches expectation: {"not":"this is a failing test"}
ok 16 - Matching: negated_basic_string_regexp: matches: matches expectation: {"not":{"match-regexp":"^foo"}}
goss-0.4.9/testdata/out_matching_basic_failing.1.documentation000066400000000000000000000223731467505051300245720ustar00rootroot00000000000000Matching: basic_array: matches:
Expected
    ["group1","group2","group3"]
to contain elements matching
    ["group1","group2","group2","group4"]
the missing elements were
    ["group2","group4"]
Matching: basic_array_consists_of: matches:
Expected
    ["foo","bar","moo"]
to consist of
    ["fox",{"have-prefix":"t"},"box"]
the missing elements were
    ["fox",{"have-prefix":"t"},"box"]
the extra elements were
    ["foo","bar","moo"]
Matching: basic_array_matchers: matches:
Expected
    ["foo","bar","moo"]
to satisfy at least one of these matchers
    [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}]
Matching: basic_int: matches:
Expected
    42
to be numerically eq
    43
Matching: basic_len: matches:
Expected
    "123"
to satisfy at least one of these matchers
    [{"have-len":2}]
Matching: basic_reader: matches:
Expected
    "object: *strings.Reader"
to have patterns
    ["fox","/^t.*w$/","!foo","!/^foo/"]
the missing elements were
    ["fox","/^t.*w$/","!foo","!/^foo/"]
Matching: basic_semver: matches:
Expected
    "1.2.3"
to satisfy at least one of these matchers
    [{"semver-constraint":">=9.9.0"}]
Matching: basic_string: matches:
Expected
    "this is a test"
to equal
    "this is a failing test"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -this is a failing test
    +this is a test
Matching: basic_string_contain_substring: matches:
Expected
    "foo"
to contain substring
    "x"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -x
    +foo
Matching: basic_string_have_prefix: matches:
Expected
    "foo"
to have prefix
    "g"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -g
    +foo
Matching: basic_string_have_suffix: matches:
Expected
    "foo"
to have suffix
    "x"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -x
    +foo
Matching: basic_string_multiline: matches:
Expected
    "this is a test1\nthis is a test2\nthis is a test3\n"
to equal
    "this is a test1\nthis is a test9\nthis is a test3\n"
diff
    --- test
    +++ actual
    @@ -1,3 +1,3 @@
     this is a test1
    -this is a test9
    +this is a test2
     this is a test3
Matching: basic_string_oneline: matches:
Expected
    "this is a test1\n"
to equal
    "this is a test9\n"
diff
    --- test
    +++ actual
    @@ -1,2 +1,2 @@
    -this is a test9
    +this is a test1
     
Matching: basic_string_regexp: matches:
Expected
    "this is a test"
to match regular expression
    "^foo"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -^foo
    +this is a test
Matching: negated_and: matches:
Expected
    42
not to satisfy all of these matchers
    [{"eq":42},{"eq":42}]
Matching: negated_basic_array: matches:
Expected
    ["group1","group2","group3"]
not to contain elements matching
    ["group1","group2","group3"]
Matching: negated_basic_array_consists_of: matches:
Expected
    ["foo","bar","moo"]
not to consist of
    ["foo",{"have-prefix":"m"},"bar"]
Matching: negated_basic_array_contain_element: matches:
Expected
    ["foo","bar","moo"]
not to contain element matching
    "foo"
Matching: negated_basic_array_matchers: matches:
Expected
    ["foo","bar","moo"]
to satisfy at least one of these matchers
    [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}]
Matching: negated_basic_int: matches:
Expected
    42
not to be numerically eq
    42
Matching: negated_basic_reader: matches:
Error
    ContainElements matcher expects an array/slice/map.  Got:
        : foo bar
        moo cow
        
Matching: negated_basic_string: matches:
Expected
    "this is a test"
not to equal
    "this is a test"
Matching: negatedbasic_len: matches:
Expected
    "123"
not to have length
    3
Matching: negatedbasic_string_contain_substring: matches:
Expected
    "foo"
not to contain substring
    "oo"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -oo
    +foo
Matching: negatedbasic_string_have_prefix: matches:
Expected
    "foo"
not to have prefix
    "f"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -f
    +foo
Matching: negatedbasic_string_have_suffix: matches:
Expected
    "foo"
not to have suffix
    "o"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -o
    +foo
Matching: negatedbasic_string_regexp: matches:
Expected
    "this is a test"
not to match regular expression
    "^this"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -^this
    +this is a test


Failures/Skipped:

Matching: basic_array: matches:
Expected
    ["group1","group2","group3"]
to contain elements matching
    ["group1","group2","group2","group4"]
the missing elements were
    ["group2","group4"]

Matching: basic_array_consists_of: matches:
Expected
    ["foo","bar","moo"]
to consist of
    ["fox",{"have-prefix":"t"},"box"]
the missing elements were
    ["fox",{"have-prefix":"t"},"box"]
the extra elements were
    ["foo","bar","moo"]

Matching: basic_array_matchers: matches:
Expected
    ["foo","bar","moo"]
to satisfy at least one of these matchers
    [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}]

Matching: basic_int: matches:
Expected
    42
to be numerically eq
    43

Matching: basic_len: matches:
Expected
    "123"
to satisfy at least one of these matchers
    [{"have-len":2}]

Matching: basic_reader: matches:
Expected
    "object: *strings.Reader"
to have patterns
    ["fox","/^t.*w$/","!foo","!/^foo/"]
the missing elements were
    ["fox","/^t.*w$/","!foo","!/^foo/"]

Matching: basic_semver: matches:
Expected
    "1.2.3"
to satisfy at least one of these matchers
    [{"semver-constraint":">=9.9.0"}]

Matching: basic_string: matches:
Expected
    "this is a test"
to equal
    "this is a failing test"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -this is a failing test
    +this is a test

Matching: basic_string_contain_substring: matches:
Expected
    "foo"
to contain substring
    "x"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -x
    +foo

Matching: basic_string_have_prefix: matches:
Expected
    "foo"
to have prefix
    "g"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -g
    +foo

Matching: basic_string_have_suffix: matches:
Expected
    "foo"
to have suffix
    "x"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -x
    +foo

Matching: basic_string_multiline: matches:
Expected
    "this is a test1\nthis is a test2\nthis is a test3\n"
to equal
    "this is a test1\nthis is a test9\nthis is a test3\n"
diff
    --- test
    +++ actual
    @@ -1,3 +1,3 @@
     this is a test1
    -this is a test9
    +this is a test2
     this is a test3

Matching: basic_string_oneline: matches:
Expected
    "this is a test1\n"
to equal
    "this is a test9\n"
diff
    --- test
    +++ actual
    @@ -1,2 +1,2 @@
    -this is a test9
    +this is a test1
     

Matching: basic_string_regexp: matches:
Expected
    "this is a test"
to match regular expression
    "^foo"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -^foo
    +this is a test

Matching: negated_and: matches:
Expected
    42
not to satisfy all of these matchers
    [{"eq":42},{"eq":42}]

Matching: negated_basic_array: matches:
Expected
    ["group1","group2","group3"]
not to contain elements matching
    ["group1","group2","group3"]

Matching: negated_basic_array_consists_of: matches:
Expected
    ["foo","bar","moo"]
not to consist of
    ["foo",{"have-prefix":"m"},"bar"]

Matching: negated_basic_array_contain_element: matches:
Expected
    ["foo","bar","moo"]
not to contain element matching
    "foo"

Matching: negated_basic_array_matchers: matches:
Expected
    ["foo","bar","moo"]
to satisfy at least one of these matchers
    [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}]

Matching: negated_basic_int: matches:
Expected
    42
not to be numerically eq
    42

Matching: negated_basic_reader: matches:
Error
    ContainElements matcher expects an array/slice/map.  Got:
        : foo bar
        moo cow
        

Matching: negated_basic_string: matches:
Expected
    "this is a test"
not to equal
    "this is a test"

Matching: negatedbasic_len: matches:
Expected
    "123"
not to have length
    3

Matching: negatedbasic_string_contain_substring: matches:
Expected
    "foo"
not to contain substring
    "oo"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -oo
    +foo

Matching: negatedbasic_string_have_prefix: matches:
Expected
    "foo"
not to have prefix
    "f"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -f
    +foo

Matching: negatedbasic_string_have_suffix: matches:
Expected
    "foo"
not to have suffix
    "o"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -o
    +foo

Matching: negatedbasic_string_regexp: matches:
Expected
    "this is a test"
not to match regular expression
    "^this"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -^this
    +this is a test

Total Duration: 
Count: 27, Failed: 27, Skipped: 0
goss-0.4.9/testdata/out_matching_basic_failing.1.rspecish000066400000000000000000000113121467505051300235300ustar00rootroot00000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFF

Failures/Skipped:

Matching: basic_array: matches:
Expected
    ["group1","group2","group3"]
to contain elements matching
    ["group1","group2","group2","group4"]
the missing elements were
    ["group2","group4"]

Matching: basic_array_consists_of: matches:
Expected
    ["foo","bar","moo"]
to consist of
    ["fox",{"have-prefix":"t"},"box"]
the missing elements were
    ["fox",{"have-prefix":"t"},"box"]
the extra elements were
    ["foo","bar","moo"]

Matching: basic_array_matchers: matches:
Expected
    ["foo","bar","moo"]
to satisfy at least one of these matchers
    [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}]

Matching: basic_int: matches:
Expected
    42
to be numerically eq
    43

Matching: basic_len: matches:
Expected
    "123"
to satisfy at least one of these matchers
    [{"have-len":2}]

Matching: basic_reader: matches:
Expected
    "object: *strings.Reader"
to have patterns
    ["fox","/^t.*w$/","!foo","!/^foo/"]
the missing elements were
    ["fox","/^t.*w$/","!foo","!/^foo/"]

Matching: basic_semver: matches:
Expected
    "1.2.3"
to satisfy at least one of these matchers
    [{"semver-constraint":">=9.9.0"}]

Matching: basic_string: matches:
Expected
    "this is a test"
to equal
    "this is a failing test"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -this is a failing test
    +this is a test

Matching: basic_string_contain_substring: matches:
Expected
    "foo"
to contain substring
    "x"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -x
    +foo

Matching: basic_string_have_prefix: matches:
Expected
    "foo"
to have prefix
    "g"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -g
    +foo

Matching: basic_string_have_suffix: matches:
Expected
    "foo"
to have suffix
    "x"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -x
    +foo

Matching: basic_string_multiline: matches:
Expected
    "this is a test1\nthis is a test2\nthis is a test3\n"
to equal
    "this is a test1\nthis is a test9\nthis is a test3\n"
diff
    --- test
    +++ actual
    @@ -1,3 +1,3 @@
     this is a test1
    -this is a test9
    +this is a test2
     this is a test3

Matching: basic_string_oneline: matches:
Expected
    "this is a test1\n"
to equal
    "this is a test9\n"
diff
    --- test
    +++ actual
    @@ -1,2 +1,2 @@
    -this is a test9
    +this is a test1
     

Matching: basic_string_regexp: matches:
Expected
    "this is a test"
to match regular expression
    "^foo"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -^foo
    +this is a test

Matching: negated_and: matches:
Expected
    42
not to satisfy all of these matchers
    [{"eq":42},{"eq":42}]

Matching: negated_basic_array: matches:
Expected
    ["group1","group2","group3"]
not to contain elements matching
    ["group1","group2","group3"]

Matching: negated_basic_array_consists_of: matches:
Expected
    ["foo","bar","moo"]
not to consist of
    ["foo",{"have-prefix":"m"},"bar"]

Matching: negated_basic_array_contain_element: matches:
Expected
    ["foo","bar","moo"]
not to contain element matching
    "foo"

Matching: negated_basic_array_matchers: matches:
Expected
    ["foo","bar","moo"]
to satisfy at least one of these matchers
    [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}]

Matching: negated_basic_int: matches:
Expected
    42
not to be numerically eq
    42

Matching: negated_basic_reader: matches:
Error
    ContainElements matcher expects an array/slice/map.  Got:
        : foo bar
        moo cow
        

Matching: negated_basic_string: matches:
Expected
    "this is a test"
not to equal
    "this is a test"

Matching: negatedbasic_len: matches:
Expected
    "123"
not to have length
    3

Matching: negatedbasic_string_contain_substring: matches:
Expected
    "foo"
not to contain substring
    "oo"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -oo
    +foo

Matching: negatedbasic_string_have_prefix: matches:
Expected
    "foo"
not to have prefix
    "f"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -f
    +foo

Matching: negatedbasic_string_have_suffix: matches:
Expected
    "foo"
not to have suffix
    "o"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -o
    +foo

Matching: negatedbasic_string_regexp: matches:
Expected
    "this is a test"
not to match regular expression
    "^this"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -^this
    +this is a test

Total Duration: 
Count: 27, Failed: 27, Skipped: 0
goss-0.4.9/testdata/out_matching_basic_failing.1.tap000066400000000000000000000074331467505051300225050ustar00rootroot000000000000001..27
not ok 1 - Matching: basic_array: matches: Expected ["group1","group2","group3"] to contain elements matching ["group1","group2","group2","group4"] the missing elements were ["group2","group4"]
not ok 2 - Matching: basic_array_consists_of: matches: Expected ["foo","bar","moo"] to consist of ["fox",{"have-prefix":"t"},"box"] the missing elements were ["fox",{"have-prefix":"t"},"box"] the extra elements were ["foo","bar","moo"]
not ok 3 - Matching: basic_array_matchers: matches: Expected ["foo","bar","moo"] to satisfy at least one of these matchers [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}]
not ok 4 - Matching: basic_int: matches: Expected 42 to be numerically eq 43
not ok 5 - Matching: basic_len: matches: Expected "123" to satisfy at least one of these matchers [{"have-len":2}]
not ok 6 - Matching: basic_reader: matches: Expected "object: *strings.Reader" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were ["fox","/^t.*w$/","!foo","!/^foo/"]
not ok 7 - Matching: basic_semver: matches: Expected "1.2.3" to satisfy at least one of these matchers [{"semver-constraint":">=9.9.0"}]
not ok 8 - Matching: basic_string: matches: Expected "this is a test" to equal "this is a failing test"
not ok 9 - Matching: basic_string_contain_substring: matches: Expected "foo" to contain substring "x"
not ok 10 - Matching: basic_string_have_prefix: matches: Expected "foo" to have prefix "g"
not ok 11 - Matching: basic_string_have_suffix: matches: Expected "foo" to have suffix "x"
not ok 12 - Matching: basic_string_multiline: matches: Expected "this is a test1\nthis is a test2\nthis is a test3\n" to equal "this is a test1\nthis is a test9\nthis is a test3\n"
not ok 13 - Matching: basic_string_oneline: matches: Expected "this is a test1\n" to equal "this is a test9\n"
not ok 14 - Matching: basic_string_regexp: matches: Expected "this is a test" to match regular expression "^foo"
not ok 15 - Matching: negated_and: matches: Expected 42 not to satisfy all of these matchers [{"eq":42},{"eq":42}]
not ok 16 - Matching: negated_basic_array: matches: Expected ["group1","group2","group3"] not to contain elements matching ["group1","group2","group3"]
not ok 17 - Matching: negated_basic_array_consists_of: matches: Expected ["foo","bar","moo"] not to consist of ["foo",{"have-prefix":"m"},"bar"]
not ok 18 - Matching: negated_basic_array_contain_element: matches: Expected ["foo","bar","moo"] not to contain element matching "foo"
not ok 19 - Matching: negated_basic_array_matchers: matches: Expected ["foo","bar","moo"] to satisfy at least one of these matchers [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}]
not ok 20 - Matching: negated_basic_int: matches: Expected 42 not to be numerically eq 42
not ok 21 - Matching: negated_basic_reader: matches: Error ContainElements matcher expects an array/slice/map. Got: : foo bar moo cow 
not ok 22 - Matching: negated_basic_string: matches: Expected "this is a test" not to equal "this is a test"
not ok 23 - Matching: negatedbasic_len: matches: Expected "123" not to have length 3
not ok 24 - Matching: negatedbasic_string_contain_substring: matches: Expected "foo" not to contain substring "oo"
not ok 25 - Matching: negatedbasic_string_have_prefix: matches: Expected "foo" not to have prefix "f"
not ok 26 - Matching: negatedbasic_string_have_suffix: matches: Expected "foo" not to have suffix "o"
not ok 27 - Matching: negatedbasic_string_regexp: matches: Expected "this is a test" not to match regular expression "^this"
goss-0.4.9/testdata/out_matching_basic_failing.2.nagios000066400000000000000000000000761467505051300231760ustar00rootroot00000000000000GOSS CRITICAL - Count: 27, Failed: 27, Skipped: 0, Duration: 
goss-0.4.9/testdata/out_matching_transformers.0.documentation000066400000000000000000000034531467505051300245420ustar00rootroot00000000000000Matching: basic_reader_as_array: matches: matches expectation: {"and":[{"contain-element":{"contain-substring":"foo"}},{"contain-element":{"match-regexp":"^m.*w$"}},{"not":{"contain-substring":"ftw"}},{"not":{"match-regexp":"^ERROR:"}}]}
Matching: test_array: matches: matches expectation: [{"contain-element":{"match-regexp":"4."}},"45",{"and":[{"ge":46},{"le":50}]}]
Matching: test_gjson_have_key_array: matches: matches expectation: {"gjson":{"arr":{"or":[{"contain-elements":[{"have-key":"nested"}]}]}}}
Matching: test_gjson_transform: matches: matches expectation: {"gjson":{"@this":{"have-key":"foo"},"count":{"le":25},"foo":{"have-prefix":"b"},"moo":{"and":[{"have-key":"nested"},{"not":{"have-key":"nested2"}}]},"moo.nested":"cow"}}
Matching: test_gjson_using_this_and_equal: matches: matches expectation: {"gjson":{"@this":{"equal":{"baz":"bing","foo":"bar"}}}}
Matching: test_numeric_string: matches: matches expectation: {"and":["128",{"have-prefix":"1"},{"have-suffix":"8"},{"match-regexp":"\\d{3}"}]}
Matching: test_reader_as_single_string: matches: matches expectation: "cool"
Matching: test_reader_using_array: matches: matches expectation: ["foo bar","15","moo cow"]
Matching: test_reader_using_int_matchers: matches: matches expectation: {"and":[{"le":250},{"ge":20}]}
Matching: test_reader_using_string_matchers: matches: matches expectation: {"and":[{"have-len":19},"foo bar\n15\nmoo cow\n",{"have-prefix":"foo"},{"have-suffix":"cow\n"},{"contain-element":{"have-prefix":"moo"}},{"contain-elements":[{"not":"this_doesnt_exist"},{"lt":20},{"have-prefix":"moo"}]}]}
Matching: test_string_float: matches: matches expectation: {"and":[128.3,{"le":129},{"gt":120.2}]}
Matching: test_string_numeric: matches: matches expectation: {"and":[128,128,{"le":128},{"gt":120}]}


Total Duration: 
Count: 12, Failed: 0, Skipped: 0
goss-0.4.9/testdata/out_matching_transformers.0.nagios000066400000000000000000000000671467505051300231470ustar00rootroot00000000000000GOSS OK - Count: 12, Failed: 0, Skipped: 0, Duration: 
goss-0.4.9/testdata/out_matching_transformers.0.rspecish000066400000000000000000000001001467505051300234730ustar00rootroot00000000000000............

Total Duration: 
Count: 12, Failed: 0, Skipped: 0
goss-0.4.9/testdata/out_matching_transformers.0.tap000066400000000000000000000035241467505051300224540ustar00rootroot000000000000001..12
ok 1 - Matching: basic_reader_as_array: matches: matches expectation: {"and":[{"contain-element":{"contain-substring":"foo"}},{"contain-element":{"match-regexp":"^m.*w$"}},{"not":{"contain-substring":"ftw"}},{"not":{"match-regexp":"^ERROR:"}}]}
ok 2 - Matching: test_array: matches: matches expectation: [{"contain-element":{"match-regexp":"4."}},"45",{"and":[{"ge":46},{"le":50}]}]
ok 3 - Matching: test_gjson_have_key_array: matches: matches expectation: {"gjson":{"arr":{"or":[{"contain-elements":[{"have-key":"nested"}]}]}}}
ok 4 - Matching: test_gjson_transform: matches: matches expectation: {"gjson":{"@this":{"have-key":"foo"},"count":{"le":25},"foo":{"have-prefix":"b"},"moo":{"and":[{"have-key":"nested"},{"not":{"have-key":"nested2"}}]},"moo.nested":"cow"}}
ok 5 - Matching: test_gjson_using_this_and_equal: matches: matches expectation: {"gjson":{"@this":{"equal":{"baz":"bing","foo":"bar"}}}}
ok 6 - Matching: test_numeric_string: matches: matches expectation: {"and":["128",{"have-prefix":"1"},{"have-suffix":"8"},{"match-regexp":"\\d{3}"}]}
ok 7 - Matching: test_reader_as_single_string: matches: matches expectation: "cool"
ok 8 - Matching: test_reader_using_array: matches: matches expectation: ["foo bar","15","moo cow"]
ok 9 - Matching: test_reader_using_int_matchers: matches: matches expectation: {"and":[{"le":250},{"ge":20}]}
ok 10 - Matching: test_reader_using_string_matchers: matches: matches expectation: {"and":[{"have-len":19},"foo bar\n15\nmoo cow\n",{"have-prefix":"foo"},{"have-suffix":"cow\n"},{"contain-element":{"have-prefix":"moo"}},{"contain-elements":[{"not":"this_doesnt_exist"},{"lt":20},{"have-prefix":"moo"}]}]}
ok 11 - Matching: test_string_float: matches: matches expectation: {"and":[128.3,{"le":129},{"gt":120.2}]}
ok 12 - Matching: test_string_numeric: matches: matches expectation: {"and":[128,128,{"le":128},{"gt":120}]}
goss-0.4.9/testdata/out_matching_transformers_failing.1.documentation000066400000000000000000000204661467505051300262370ustar00rootroot00000000000000Matching: basic_reader_as_array: matches:
Expected
    ["foo bar","moo cow",""]
to contain element matching
    {"contain-substring":"fox"}
the transform chain was
    [{"to-array":{}}]
the raw value was
    "foo bar\nmoo cow\n"
Matching: test_array: matches:
Expected
    ["45","46","47"]
to contain elements matching
    [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
the missing elements were
    [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
Matching: test_gjson_have_key_array: matches:
Expected
    {"arr":[{"nested":"cow"},{"nested2":"moo"}]}
to satisfy at least one of these matchers
    [{"have-key":"fail"}]
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
Matching: test_gjson_invalid: matches:
Error
    matchers.Gjson{Path:"@this"}: Invalid json
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"arr\""
Matching: test_gjson_not_found: matches:
Error
    matchers.Gjson{Path:"foo"}: Path not found: foo
the transform chain was
    [{"gjson":{"Path":"foo"}}]
the raw value was
    "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
Matching: test_gjson_transform_nested_and: matches:
Expected
    {"nested":"cow"}
to have key matching
    "nope"
the transform chain was
    [{"gjson":{"Path":"moo"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
Matching: test_gjson_transform_nested_count: matches:
Expected
    15
to be numerically le
    10
the transform chain was
    [{"gjson":{"Path":"count"}},{"to-numeric":{}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
Matching: test_gjson_transform_nested_prefix: matches:
Expected
    "bar"
to have prefix
    "x"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -x
    +bar
the transform chain was
    [{"gjson":{"Path":"foo"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
Matching: test_gjson_transform_nested_this: matches:
Expected
    {"count":"15","foo":"bar","moo":{"nested":"cow"}}
to have key matching
    "nope"
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
Matching: test_gjson_transform_not_key: matches:
Expected
    {"nested":"cow"}
not to have key matching
    "nested"
the transform chain was
    [{"gjson":{"Path":"moo"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
Matching: test_gjson_transform_simple: matches:
Expected
    "cow"
to equal
    "cowx"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -cowx
    +cow
the transform chain was
    [{"gjson":{"Path":"moo.nested"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
Matching: test_gjson_using_this_and_equal: matches:
Expected
    {"baz":"bing","foo":"bar"}
to equal
    {"baz":"bing","fox":"bar"}
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"foo\": \"bar\", \"baz\": \"bing\"}"
Matching: test_numeric_string: matches:
Expected
    "128"
to equal
    "129"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -129
    +128
the transform chain was
    [{"to-string":{}}]
the raw value was
    128
Matching: test_reader_as_single_string: matches:
Expected
    "cool"
to equal
    "not-cool"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -not-cool
    +cool
Matching: test_reader_using_int_matchers: matches:
Expected
    40
to be numerically le
    20
the transform chain was
    [{"to-numeric":{}}]
the raw value was
    "40"
Matching: test_reader_using_string_matchers: matches:
Expected
    "foo bar\n15\nmoo cow\n"
to have length
    15
Matching: test_string_float: matches:
Expected
    128.3
to be numerically eq
    129.3
the transform chain was
    [{"to-numeric":{}}]
the raw value was
    "128.3"
Matching: test_string_numeric: matches:
Expected
    128
to be numerically eq
    129
the transform chain was
    [{"to-numeric":{}}]
the raw value was
    "128"


Failures/Skipped:

Matching: basic_reader_as_array: matches:
Expected
    ["foo bar","moo cow",""]
to contain element matching
    {"contain-substring":"fox"}
the transform chain was
    [{"to-array":{}}]
the raw value was
    "foo bar\nmoo cow\n"

Matching: test_array: matches:
Expected
    ["45","46","47"]
to contain elements matching
    [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
the missing elements were
    [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]

Matching: test_gjson_have_key_array: matches:
Expected
    {"arr":[{"nested":"cow"},{"nested2":"moo"}]}
to satisfy at least one of these matchers
    [{"have-key":"fail"}]
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"

Matching: test_gjson_invalid: matches:
Error
    matchers.Gjson{Path:"@this"}: Invalid json
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"arr\""

Matching: test_gjson_not_found: matches:
Error
    matchers.Gjson{Path:"foo"}: Path not found: foo
the transform chain was
    [{"gjson":{"Path":"foo"}}]
the raw value was
    "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"

Matching: test_gjson_transform_nested_and: matches:
Expected
    {"nested":"cow"}
to have key matching
    "nope"
the transform chain was
    [{"gjson":{"Path":"moo"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_transform_nested_count: matches:
Expected
    15
to be numerically le
    10
the transform chain was
    [{"gjson":{"Path":"count"}},{"to-numeric":{}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_transform_nested_prefix: matches:
Expected
    "bar"
to have prefix
    "x"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -x
    +bar
the transform chain was
    [{"gjson":{"Path":"foo"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_transform_nested_this: matches:
Expected
    {"count":"15","foo":"bar","moo":{"nested":"cow"}}
to have key matching
    "nope"
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_transform_not_key: matches:
Expected
    {"nested":"cow"}
not to have key matching
    "nested"
the transform chain was
    [{"gjson":{"Path":"moo"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_transform_simple: matches:
Expected
    "cow"
to equal
    "cowx"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -cowx
    +cow
the transform chain was
    [{"gjson":{"Path":"moo.nested"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_using_this_and_equal: matches:
Expected
    {"baz":"bing","foo":"bar"}
to equal
    {"baz":"bing","fox":"bar"}
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"foo\": \"bar\", \"baz\": \"bing\"}"

Matching: test_numeric_string: matches:
Expected
    "128"
to equal
    "129"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -129
    +128
the transform chain was
    [{"to-string":{}}]
the raw value was
    128

Matching: test_reader_as_single_string: matches:
Expected
    "cool"
to equal
    "not-cool"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -not-cool
    +cool

Matching: test_reader_using_int_matchers: matches:
Expected
    40
to be numerically le
    20
the transform chain was
    [{"to-numeric":{}}]
the raw value was
    "40"

Matching: test_reader_using_string_matchers: matches:
Expected
    "foo bar\n15\nmoo cow\n"
to have length
    15

Matching: test_string_float: matches:
Expected
    128.3
to be numerically eq
    129.3
the transform chain was
    [{"to-numeric":{}}]
the raw value was
    "128.3"

Matching: test_string_numeric: matches:
Expected
    128
to be numerically eq
    129
the transform chain was
    [{"to-numeric":{}}]
the raw value was
    "128"

Total Duration: 
Count: 18, Failed: 18, Skipped: 0
goss-0.4.9/testdata/out_matching_transformers_failing.1.rspecish000066400000000000000000000103321467505051300251750ustar00rootroot00000000000000FFFFFFFFFFFFFFFFFF

Failures/Skipped:

Matching: basic_reader_as_array: matches:
Expected
    ["foo bar","moo cow",""]
to contain element matching
    {"contain-substring":"fox"}
the transform chain was
    [{"to-array":{}}]
the raw value was
    "foo bar\nmoo cow\n"

Matching: test_array: matches:
Expected
    ["45","46","47"]
to contain elements matching
    [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
the missing elements were
    [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]

Matching: test_gjson_have_key_array: matches:
Expected
    {"arr":[{"nested":"cow"},{"nested2":"moo"}]}
to satisfy at least one of these matchers
    [{"have-key":"fail"}]
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"

Matching: test_gjson_invalid: matches:
Error
    matchers.Gjson{Path:"@this"}: Invalid json
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"arr\""

Matching: test_gjson_not_found: matches:
Error
    matchers.Gjson{Path:"foo"}: Path not found: foo
the transform chain was
    [{"gjson":{"Path":"foo"}}]
the raw value was
    "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"

Matching: test_gjson_transform_nested_and: matches:
Expected
    {"nested":"cow"}
to have key matching
    "nope"
the transform chain was
    [{"gjson":{"Path":"moo"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_transform_nested_count: matches:
Expected
    15
to be numerically le
    10
the transform chain was
    [{"gjson":{"Path":"count"}},{"to-numeric":{}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_transform_nested_prefix: matches:
Expected
    "bar"
to have prefix
    "x"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -x
    +bar
the transform chain was
    [{"gjson":{"Path":"foo"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_transform_nested_this: matches:
Expected
    {"count":"15","foo":"bar","moo":{"nested":"cow"}}
to have key matching
    "nope"
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_transform_not_key: matches:
Expected
    {"nested":"cow"}
not to have key matching
    "nested"
the transform chain was
    [{"gjson":{"Path":"moo"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_transform_simple: matches:
Expected
    "cow"
to equal
    "cowx"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -cowx
    +cow
the transform chain was
    [{"gjson":{"Path":"moo.nested"}}]
the raw value was
    "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"

Matching: test_gjson_using_this_and_equal: matches:
Expected
    {"baz":"bing","foo":"bar"}
to equal
    {"baz":"bing","fox":"bar"}
the transform chain was
    [{"gjson":{"Path":"@this"}}]
the raw value was
    "{\"foo\": \"bar\", \"baz\": \"bing\"}"

Matching: test_numeric_string: matches:
Expected
    "128"
to equal
    "129"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -129
    +128
the transform chain was
    [{"to-string":{}}]
the raw value was
    128

Matching: test_reader_as_single_string: matches:
Expected
    "cool"
to equal
    "not-cool"
diff
    --- test
    +++ actual
    @@ -1 +1 @@
    -not-cool
    +cool

Matching: test_reader_using_int_matchers: matches:
Expected
    40
to be numerically le
    20
the transform chain was
    [{"to-numeric":{}}]
the raw value was
    "40"

Matching: test_reader_using_string_matchers: matches:
Expected
    "foo bar\n15\nmoo cow\n"
to have length
    15

Matching: test_string_float: matches:
Expected
    128.3
to be numerically eq
    129.3
the transform chain was
    [{"to-numeric":{}}]
the raw value was
    "128.3"

Matching: test_string_numeric: matches:
Expected
    128
to be numerically eq
    129
the transform chain was
    [{"to-numeric":{}}]
the raw value was
    "128"

Total Duration: 
Count: 18, Failed: 18, Skipped: 0
goss-0.4.9/testdata/out_matching_transformers_failing.1.tap000066400000000000000000000074561467505051300241560ustar00rootroot000000000000001..18
not ok 1 - Matching: basic_reader_as_array: matches: Expected ["foo bar","moo cow",""] to contain element matching {"contain-substring":"fox"} the transform chain was [{"to-array":{}}] the raw value was "foo bar\nmoo cow\n"
not ok 2 - Matching: test_array: matches: Expected ["45","46","47"] to contain elements matching [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}] the missing elements were [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}]
not ok 3 - Matching: test_gjson_have_key_array: matches: Expected {"arr":[{"nested":"cow"},{"nested2":"moo"}]} to satisfy at least one of these matchers [{"have-key":"fail"}] the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
not ok 4 - Matching: test_gjson_invalid: matches: Error matchers.Gjson{Path:"@this"}: Invalid json the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"arr\""
not ok 5 - Matching: test_gjson_not_found: matches: Error matchers.Gjson{Path:"foo"}: Path not found: foo the transform chain was [{"gjson":{"Path":"foo"}}] the raw value was "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}"
not ok 6 - Matching: test_gjson_transform_nested_and: matches: Expected {"nested":"cow"} to have key matching "nope" the transform chain was [{"gjson":{"Path":"moo"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
not ok 7 - Matching: test_gjson_transform_nested_count: matches: Expected 15 to be numerically le 10 the transform chain was [{"gjson":{"Path":"count"}},{"to-numeric":{}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
not ok 8 - Matching: test_gjson_transform_nested_prefix: matches: Expected "bar" to have prefix "x" the transform chain was [{"gjson":{"Path":"foo"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
not ok 9 - Matching: test_gjson_transform_nested_this: matches: Expected {"count":"15","foo":"bar","moo":{"nested":"cow"}} to have key matching "nope" the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
not ok 10 - Matching: test_gjson_transform_not_key: matches: Expected {"nested":"cow"} not to have key matching "nested" the transform chain was [{"gjson":{"Path":"moo"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
not ok 11 - Matching: test_gjson_transform_simple: matches: Expected "cow" to equal "cowx" the transform chain was [{"gjson":{"Path":"moo.nested"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}"
not ok 12 - Matching: test_gjson_using_this_and_equal: matches: Expected {"baz":"bing","foo":"bar"} to equal {"baz":"bing","fox":"bar"} the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"foo\": \"bar\", \"baz\": \"bing\"}"
not ok 13 - Matching: test_numeric_string: matches: Expected "128" to equal "129" the transform chain was [{"to-string":{}}] the raw value was 128
not ok 14 - Matching: test_reader_as_single_string: matches: Expected "cool" to equal "not-cool"
not ok 15 - Matching: test_reader_using_int_matchers: matches: Expected 40 to be numerically le 20 the transform chain was [{"to-numeric":{}}] the raw value was "40"
not ok 16 - Matching: test_reader_using_string_matchers: matches: Expected "foo bar\n15\nmoo cow\n" to have length 15
not ok 17 - Matching: test_string_float: matches: Expected 128.3 to be numerically eq 129.3 the transform chain was [{"to-numeric":{}}] the raw value was "128.3"
not ok 18 - Matching: test_string_numeric: matches: Expected 128 to be numerically eq 129 the transform chain was [{"to-numeric":{}}] the raw value was "128"
goss-0.4.9/testdata/out_matching_transformers_failing.2.nagios000066400000000000000000000000761467505051300246420ustar00rootroot00000000000000GOSS CRITICAL - Count: 18, Failed: 18, Skipped: 0, Duration: 
goss-0.4.9/testdata/passing.goss.yaml000066400000000000000000000002141467505051300176150ustar00rootroot00000000000000---
command:
  hello world:
    exit-status: 0
    exec: "echo hello world"
    stdout:
    - hello world
    stderr: []
    timeout: 10000
goss-0.4.9/util/000077500000000000000000000000001467505051300134625ustar00rootroot00000000000000goss-0.4.9/util/build.go000066400000000000000000000000411467505051300151030ustar00rootroot00000000000000package util

var Version string
goss-0.4.9/util/command.go000066400000000000000000000015641467505051300154350ustar00rootroot00000000000000package util

import (
	"bytes"

	//"fmt"
	"os/exec"
	"syscall"
)

type Command struct {
	name           string
	Cmd            *exec.Cmd
	Stdout, Stderr bytes.Buffer
	Err            error
	Status         int
}

func NewCommand(name string, arg ...string) *Command {
	//fmt.Println(arg)
	command := new(Command)
	command.name = name
	command.Cmd = exec.Command(name, arg...)

	return command
}

func (c *Command) Run() error {
	c.Cmd.Stdout = &c.Stdout
	c.Cmd.Stderr = &c.Stderr

	if _, err := exec.LookPath(c.name); err != nil {
		c.Err = err
		return c.Err
	}

	if err := c.Cmd.Start(); err != nil {
		c.Err = err
		return c.Err
	}

	if err := c.Cmd.Wait(); err != nil {
		c.Err = err
		if exiterr, ok := err.(*exec.ExitError); ok {
			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
				c.Status = status.ExitStatus()
			}
		}
	} else {
		c.Status = 0
	}
	return c.Err
}
goss-0.4.9/util/command_windows.go000066400000000000000000000011511467505051300171770ustar00rootroot00000000000000//go:build windows
// +build windows

package util

import (
	"strings"

	//"fmt"
	"os/exec"
	"syscall"
)

func NewCommandForWindowsCmd(name string, arg ...string) *Command {
	//fmt.Println(arg)
	command := new(Command)
	command.name = name

	// cmd.exe has a unique unquoting algorithm
	// provide the full command line in SysProcAttr.CmdLine, leaving Args empty.
	// more information: https://golang.org/pkg/os/exec/#Command
	command.Cmd = exec.Command(name)
	command.Cmd.SysProcAttr = &syscall.SysProcAttr{
		HideWindow:    false,
		CmdLine:       strings.Join(arg, " "),
		CreationFlags: 0,
	}

	return command
}
goss-0.4.9/util/config.go000066400000000000000000000166271467505051300152720ustar00rootroot00000000000000package util

import (
	"encoding/json"
	"fmt"
	"io"
	"reflect"
	"strings"
	"time"

	"github.com/oleiade/reflections"
)

// ConfigOption manipulates Config
type ConfigOption func(c *Config) error

// Config is the runtime configuration for the goss system, the cli.Context gets
// converted to this and it allows other packages to embed goss by creating this
// structure and using it when adding, validating etc.
//
// NewConfig can be used to create this which will default to what the CLI assumes
// and allow manipulation via ConfigOption functions
type Config struct {
	AllowInsecure         bool
	AnnounceToCLI         bool
	Cache                 time.Duration
	Debug                 bool
	Endpoint              string
	FormatOptions         []string
	IgnoreList            []string
	ListenAddress         string
	LocalAddress          string
	LogLevel              string
	MaxConcurrent         int
	Method                string
	NoColor               *bool
	NoFollowRedirects     bool
	OutputFormat          string
	OutputWriter          io.Writer
	PackageManager        string
	Password              string
	RequestBody           string
	Proxy                 string
	RequestHeader         []string
	RetryTimeout          time.Duration
	RunLevel              string
	Server                string
	Sleep                 time.Duration
	Spec                  string
	Timeout               time.Duration
	Username              string
	CAFile                string
	CertFile              string
	KeyFile               string
	Vars                  string
	VarsInline            string
	DisabledResourceTypes []string
}

// TimeOutMilliSeconds is the timeout as milliseconds
func (c *Config) TimeOutMilliSeconds() int {
	return int(c.Timeout / time.Millisecond)
}

// NewConfig creates a default configuration modeled on the defaults the CLI sets, modified using opts
func NewConfig(opts ...ConfigOption) (rc *Config, err error) {
	rc = &Config{
		AllowInsecure:         false,
		AnnounceToCLI:         false,
		Cache:                 5 * time.Second,
		Debug:                 false,
		Endpoint:              "/healthz",
		FormatOptions:         []string{},
		IgnoreList:            []string{},
		DisabledResourceTypes: []string{},
		ListenAddress:         ":8080",
		LocalAddress:          "",
		LogLevel:              "ERROR",
		MaxConcurrent:         50,
		NoColor:               nil,
		NoFollowRedirects:     false,
		OutputFormat:          "structured", // most appropriate for package usage
		PackageManager:        "",
		Password:              "",
		Proxy:                 "",
		RequestHeader:         nil,
		RetryTimeout:          0,
		Server:                "",
		Sleep:                 time.Second,
		Spec:                  "",
		Timeout:               0,
		Username:              "",
		Vars:                  "",
		VarsInline:            "",
	}

	// NewConfig() is likely to be used when embedding goss or using as a package
	// so assuming no color seems like a sane departure from CLI defaults
	WithNoColor()(rc)

	for _, opt := range opts {
		err = opt(rc)
		if err != nil {
			return nil, err
		}
	}

	return rc, nil
}

// WithSpecFile sets the path to the file holding spec contents
func WithSpecFile(f string) ConfigOption {
	return func(c *Config) error {
		c.Spec = f
		return nil
	}
}

// WithOutputFormat is the formatter to use for output
func WithOutputFormat(f string) ConfigOption {
	return func(c *Config) error {
		c.OutputFormat = f

		return nil
	}
}

// WithFormatOptions sets options used by the output format plugins, valid options are output.WithFormatOptions
func WithFormatOptions(opts ...string) ConfigOption {
	return func(c *Config) error {
		c.FormatOptions = append(c.FormatOptions, opts...)
		return nil
	}
}

// WithResultWriter sets the writer to write output format to when validating
func WithResultWriter(w io.Writer) ConfigOption {
	return func(c *Config) error {
		c.OutputWriter = w
		return nil
	}
}

// WithSleep sets the time to sleep between retries when WithRetryTimeout is set
func WithSleep(d time.Duration) ConfigOption {
	return func(c *Config) error {
		c.Sleep = d
		return nil
	}
}

// WithRetryTimeout sets the maximum amount of time checks can be retried, it's runtime + WithSleep
func WithRetryTimeout(d time.Duration) ConfigOption {
	return func(c *Config) error {
		c.RetryTimeout = d
		return nil
	}
}

// WithCache sets how long results may be cached for
func WithCache(d time.Duration) ConfigOption {
	return func(c *Config) error {
		c.Cache = d
		return nil
	}
}

// WithMaxConcurrency is the maximum concurrent test that can be run
func WithMaxConcurrency(mc int) ConfigOption {
	return func(c *Config) error {
		c.MaxConcurrent = mc
		return nil
	}
}

// WithNoColor disables colored output
func WithNoColor() ConfigOption {
	return func(c *Config) error {
		c.NoColor = func(b bool) *bool { return &b }(true)
		return nil
	}
}

// WithColor enables colored output
func WithColor() ConfigOption {
	return func(c *Config) error {
		c.NoColor = func(b bool) *bool { return &b }(false)
		return nil
	}
}

// WithPackageManager overrides the package manager to use
func WithPackageManager(p string) ConfigOption {
	return func(c *Config) error {
		c.PackageManager = p

		return nil
	}
}

// WithDebug enables debug output
func WithDebug() ConfigOption {
	return func(c *Config) error {
		c.Debug = true
		return nil
	}
}

// WithVarsFile is a json or yaml file containing variables to pass to the validator
func WithVarsFile(file string) ConfigOption {
	return func(c *Config) error {
		c.Vars = file
		return nil
	}
}

// WithVarsData uses v as variables to pass to the Validator
func WithVarsData(v any) ConfigOption {
	return func(c *Config) error {
		jv, err := json.Marshal(v)
		if err != nil {
			return err
		}

		c.VarsInline = string(jv)

		return nil
	}
}

// WithVarsBytes is a yaml or json byte stream to use as variables passed to the Validator
func WithVarsBytes(v []byte) ConfigOption {
	return WithVarsString(string(v))
}

// WithVarsString is a yaml or json string to use as variables passed to the Validator
func WithVarsString(v string) ConfigOption {
	return func(c *Config) error {
		c.VarsInline = v
		return nil
	}
}

// WithDisabledResourceTypes ensures that any resource matching types listed will be skipped when validating
func WithDisabledResourceTypes(t ...string) ConfigOption {
	return func(c *Config) error {
		c.DisabledResourceTypes = append(c.DisabledResourceTypes, t...)
		return nil
	}
}

type OutputConfig struct {
	FormatOptions []string
}

type format string

const (
	JSON format = "json"
	YAML format = "yaml"
)

func ValidateSections(unmarshal func(any) error, i any, whitelist map[string]bool) error {
	// Get generic input
	var toValidate map[string]map[string]any
	if err := unmarshal(&toValidate); err != nil {
		return err
	}

	// Run input through whitelist
	typ := reflect.TypeOf(i)
	typs := strings.Split(typ.String(), ".")[1]
	for id, v := range toValidate {
		for k := range v {
			if !whitelist[k] {
				return fmt.Errorf("invalid Attribute for %s:%s: %s", typs, id, k)
			}
		}
	}

	return nil
}

func WhitelistAttrs(i any, format format) (map[string]bool, error) {
	validAttrs := make(map[string]bool)
	tags, err := reflections.Tags(i, string(format))
	if err != nil {
		return nil, err
	}
	for _, v := range tags {
		validAttrs[strings.Split(v, ",")[0]] = true
	}
	return validAttrs, nil
}

func IsValueInList(value string, list []string) bool {
	for _, v := range list {
		if strings.EqualFold(v, value) {
			return true
		}
	}
	return false
}
goss-0.4.9/util/config_test.go000066400000000000000000000017531467505051300163230ustar00rootroot00000000000000package util

import (
	"testing"
)

func TestWithVarsBytes(t *testing.T) {
	vs := `{"hello":"world"}`
	c, err := NewConfig(WithVarsBytes([]byte(vs)))
	if err != nil {
		t.Fatal(err.Error())
	}

	if c.VarsInline != vs {
		t.Fatalf("expected %q got %q", vs, c.VarsInline)
	}
}

func TestWithVarsString(t *testing.T) {
	vs := `{"hello":"world"}`
	c, err := NewConfig(WithVarsString(vs))
	if err != nil {
		t.Fatal(err.Error())
	}

	if c.VarsInline != vs {
		t.Fatalf("expected %q got %q", vs, c.VarsInline)
	}
}

func TestWithVarsFile(t *testing.T) {
	c, err := NewConfig(WithVarsFile("/nonexisting"))
	if err != nil {
		t.Fatal(err.Error())
	}

	if c.Vars != "/nonexisting" {
		t.Fatalf("expected '/nonexisting' got %q", c.Vars)
	}
}

func TestWithVarsData(t *testing.T) {
	c, err := NewConfig(WithVarsData(map[string]string{"hello": "world"}))
	if err != nil {
		t.Fatal(err.Error())
	}

	if c.VarsInline != `{"hello":"world"}` {
		t.Fatalf("expected %q got %q", `{"hello":"world"}`, c.VarsInline)
	}
}
goss-0.4.9/validate.go000066400000000000000000000107351467505051300146330ustar00rootroot00000000000000package goss

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"sync"
	"time"

	"github.com/fatih/color"
	"github.com/onsi/gomega/format"

	"github.com/goss-org/goss/outputs"
	"github.com/goss-org/goss/resource"
	"github.com/goss-org/goss/system"
	"github.com/goss-org/goss/util"
)

func getGossConfig(vars string, varsInline string, specFile string) (cfg *GossConfig, err error) {
	// handle stdin
	var fh *os.File
	var path, source string
	var gossConfig GossConfig

	currentTemplateFilter, err = NewTemplateFilter(vars, varsInline)
	if err != nil {
		return nil, err
	}

	if specFile == "-" {
		source = "STDIN"
		fh = os.Stdin
		data, err := io.ReadAll(fh)
		if err != nil {
			return nil, err
		}
		outStoreFormat, err = getStoreFormatFromData(data)
		if err != nil {
			return nil, err
		}

		gossConfig, err = ReadJSONData(data, true)
		if err != nil {
			return nil, err
		}
	} else {
		source = specFile
		path = filepath.Dir(specFile)
		outStoreFormat, err = getStoreFormatFromFileName(specFile)
		if err != nil {
			return nil, err
		}

		gossConfig, err = ReadJSON(specFile)
		if err != nil {
			return nil, err
		}
	}

	gossConfig, err = mergeJSONData(gossConfig, 0, path)
	if err != nil {
		return nil, err
	}

	if len(gossConfig.Resources()) == 0 {
		return nil, fmt.Errorf("found 0 tests, source: %v", source)
	}

	return &gossConfig, nil
}

func getOutputer(c *bool, format string) (outputs.Outputer, error) {
	if c != nil && *c {
		color.NoColor = true
	}
	if c != nil && !*c {
		color.NoColor = false
	}

	return outputs.GetOutputer(format)
}

// ValidateResults performs validation and provides programmatic access to validation results
// no retries or outputs are supported
func ValidateResults(c *util.Config) (results <-chan []resource.TestResult, err error) {
	gossConfig, err := getGossConfig(c.Vars, c.VarsInline, c.Spec)
	if err != nil {
		return nil, err
	}

	sys := system.New(c.PackageManager)

	return validate(sys, *gossConfig, c.DisabledResourceTypes, c.MaxConcurrent), nil
}

// Validate performs validation, writes formatted output to stdout by default
// and supports retries and more, this is the full featured Validate used
// by the typical CLI invocation and will produce output to StdOut.  Use
// ValidateResults for programmatic access
func Validate(c *util.Config) (code int, err error) {
	err = setLogLevel(c)
	if err != nil {
		return 1, err
	}
	gossConfig, err := getGossConfig(c.Vars, c.VarsInline, c.Spec)
	if err != nil {
		return 78, err
	}
	return ValidateConfig(c, gossConfig)
}

func ValidateConfig(c *util.Config, gossConfig *GossConfig) (code int, err error) {
	// Needed for contains-elements
	// Maybe we don't use this and use custom
	// contain_element_matcher is needed because it's single entry to avoid
	// transform message
	format.UseStringerRepresentation = true
	outputConfig := util.OutputConfig{
		FormatOptions: c.FormatOptions,
	}

	sys := system.New(c.PackageManager)
	outputer, err := getOutputer(c.NoColor, c.OutputFormat)
	if err != nil {
		return 1, err
	}

	var ofh io.Writer
	ofh = os.Stdout
	if c.OutputWriter != nil {
		ofh = c.OutputWriter
	}

	sleep := c.Sleep
	retryTimeout := c.RetryTimeout
	i := 1
	startTime := time.Now()
	for {
		out := validate(sys, *gossConfig, c.DisabledResourceTypes, c.MaxConcurrent)
		exitCode := outputer.Output(ofh, out, outputConfig)
		if retryTimeout == 0 || exitCode == 0 {
			return exitCode, nil
		}
		elapsed := time.Since(startTime)
		if elapsed+sleep > retryTimeout {
			return 3, fmt.Errorf("timeout of %s reached before tests entered a passing state", retryTimeout)
		}
		color.Red("Retrying in %s (elapsed/timeout time: %.3fs/%s)\n\n\n", sleep, elapsed.Seconds(), retryTimeout)
		// Reset cache
		sys = system.New(c.PackageManager)
		time.Sleep(sleep)
		i++
		fmt.Printf("Attempt #%d:\n", i)
	}
}

func validate(sys *system.System, gossConfig GossConfig, skipList []string, maxConcurrent int) <-chan []resource.TestResult {
	out := make(chan []resource.TestResult)
	in := make(chan resource.Resource)

	go func() {
		for _, t := range gossConfig.Resources() {
			if util.IsValueInList(t.TypeName(), skipList) || util.IsValueInList(t.TypeKey(), skipList) {
				t.SetSkip()
			}

			in <- t
		}
		close(in)
	}()

	workerCount := runtime.NumCPU() * 5
	if workerCount > maxConcurrent {
		workerCount = maxConcurrent
	}
	var wg sync.WaitGroup
	for i := 0; i < workerCount; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for f := range in {
				out <- f.Validate(sys)
			}
		}()
	}

	go func() {
		wg.Wait()
		close(out)
	}()

	return out
}