├── .editorconfig ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── dependabot.yml ├── pull_request_template.md ├── stale.yml.disabled └── workflows │ ├── docker-goss.yaml │ ├── docker-integration-tests.yaml │ ├── docs.yaml │ ├── golangci.yaml │ ├── preview-docs.yaml │ ├── release.yaml │ ├── trivy-schedule.yaml │ └── yamllint.yaml ├── .gitignore ├── .golangci.yaml ├── .markdownlint.yaml ├── .readthedocs.yaml ├── .travis.yml ├── .yamllint ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── add.go ├── ci ├── build.sh ├── go-fmt.sh ├── go-test.sh └── lib │ ├── log.sh │ └── setup.sh ├── cmd └── goss │ └── goss.go ├── development ├── README.md ├── build_images.sh ├── debian │ ├── .gitignore │ └── Vagrantfile └── push_images.sh ├── docs ├── .pages ├── changelog.md ├── cli.md ├── container_image.md ├── containers │ ├── docker-compose.md │ ├── docker.md │ └── kubernetes.md ├── contributing.md ├── goss.yaml ├── gossfile.md ├── index.md ├── installation.md ├── license.md ├── migrations.md ├── myapp_gossfile.yaml ├── platforms.md ├── quickstart.md ├── rendered_goss.yaml ├── requirements.txt ├── schema.yaml ├── style.css └── vars.yaml ├── examples ├── goss.yaml ├── goss_awesome_gomega.yaml ├── readme.md └── test.txt ├── extras ├── dcgoss │ ├── README.md │ ├── dcgoss │ └── docker-compose.yml ├── dgoss │ ├── README.md │ └── dgoss └── kgoss │ ├── README.md │ └── kgoss ├── go.mod ├── go.sum ├── goss_config.go ├── goss_test.go ├── install.sh ├── integration-tests ├── Dockerfile_alpine3 ├── Dockerfile_alpine3.md5 ├── Dockerfile_arch ├── Dockerfile_arch.md5 ├── Dockerfile_centos7 ├── Dockerfile_centos7.md5 ├── Dockerfile_rockylinux9 ├── Dockerfile_trusty ├── Dockerfile_trusty.md5 ├── Dockerfile_wheezy ├── Dockerfile_wheezy.md5 ├── Find-AvailablePort.ps1 ├── goss │ ├── alpine3 │ │ ├── goss-aa-expected.yaml │ │ ├── goss-expected-q.yaml │ │ ├── goss-expected.yaml │ │ └── goss.yaml │ ├── arch │ │ └── goss.yaml │ ├── centos7 │ │ ├── goss-aa-expected.yaml │ │ ├── goss-expected-q.yaml │ │ ├── goss-expected.yaml │ │ └── goss.yaml │ ├── darwin │ │ ├── commands │ │ │ ├── add.goss.yaml │ │ │ ├── autoadd.goss.yaml │ │ │ ├── help.goss.yaml │ │ │ ├── validate-input.yaml │ │ │ └── validate.goss.yaml │ │ └── tests │ │ │ ├── addr.goss.yaml │ │ │ ├── command.goss.yaml │ │ │ ├── dns.goss.yaml │ │ │ ├── file.goss.yaml │ │ │ ├── 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 │ ├── generate_goss.sh │ ├── goss-dummy.yaml │ ├── goss-serve.yaml │ ├── goss-service.yaml │ ├── goss-shared.yaml │ ├── goss-wait.yaml │ ├── hellogoss.txt │ ├── rockylinux9 │ │ ├── goss-aa-expected.yaml │ │ ├── goss-expected-q.yaml │ │ ├── goss-expected.yaml │ │ └── goss.yaml │ ├── testdata │ │ └── static-file.txt │ ├── trusty │ │ ├── goss-aa-expected.yaml │ │ ├── goss-expected-q.yaml │ │ ├── goss-expected.yaml │ │ └── goss.yaml │ ├── vars.yaml │ ├── wheezy │ │ ├── goss-aa-expected.yaml │ │ ├── goss-expected-q.yaml │ │ ├── goss-expected.yaml │ │ └── goss.yaml │ └── windows │ │ ├── commands │ │ ├── add.goss.yaml │ │ ├── autoadd.goss.yaml │ │ ├── help.goss.yaml │ │ ├── validate-input.yaml │ │ └── validate.goss.yaml │ │ └── tests │ │ ├── addr.goss.yaml │ │ ├── command.goss.yaml │ │ ├── dns.goss.yaml │ │ ├── file.goss.yaml │ │ ├── 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 ├── run-serve-tests.sh ├── run-tests-alpha.sh ├── run-validate-tests.sh └── test.sh ├── logs.go ├── matcher_test.go ├── matchers ├── and.go ├── be_numerically_matcher.go ├── consist_of.go ├── contain_element_matcher.go ├── contain_elements_matcher.go ├── contain_substring_matcher.go ├── equal_matcher.go ├── have_key_matcher.go ├── have_len_matcher.go ├── have_patterns.go ├── have_prefix_matcher.go ├── have_suffix_matcher.go ├── match_regexp_matcher.go ├── matchers.go ├── not.go ├── or.go ├── semver_constraint.go ├── semver_constraint_test.go ├── type_conversion.go └── with_safe_transform.go ├── mkdocs.yml ├── novendor.sh ├── outputs ├── documentation.go ├── json.go ├── junit.go ├── nagios.go ├── outputs.go ├── outputs_test.go ├── prometheus.go ├── prometheus_test.go ├── rspecish.go ├── silent.go ├── structured.go ├── tap.go └── traces.go ├── release-build.sh ├── resource ├── addr.go ├── command.go ├── dns.go ├── file.go ├── gomega.go ├── gomega_test.go ├── gossfile.go ├── group.go ├── http.go ├── interface.go ├── kernel_param.go ├── matching.go ├── mount.go ├── package.go ├── port.go ├── process.go ├── resource.go ├── resource_list.go ├── resource_list_genny.go ├── service.go ├── user.go ├── validate.go └── validate_test.go ├── serve.go ├── serve_test.go ├── store.go ├── store_test.go ├── system ├── addr.go ├── command.go ├── command_posix.go ├── command_posix_test.go ├── command_windows.go ├── command_windows_test.go ├── dns.go ├── dns_test.go ├── file.go ├── file_posix.go ├── file_windows.go ├── gossfile.go ├── group.go ├── http.go ├── interface.go ├── kernel_param.go ├── log.go ├── mount.go ├── mount_posix.go ├── mount_test.go ├── mount_windows.go ├── package.go ├── package_alpine.go ├── package_deb.go ├── package_pacman.go ├── package_rpm.go ├── package_test.go ├── port.go ├── process.go ├── service.go ├── service_init.go ├── service_systemd.go ├── service_upstart.go ├── system.go ├── system_test.go ├── user.go ├── user_group_unix.go ├── user_group_unix_test.go ├── user_group_windows.go ├── user_unix.go └── user_unsupported.go ├── template.go ├── testdata ├── failing.goss.yaml ├── matching_basic.yaml ├── matching_basic_failing.yaml ├── matching_transformers.yaml ├── matching_transformers_failing.yaml ├── out_matching_basic.0.documentation ├── out_matching_basic.0.nagios ├── out_matching_basic.0.rspecish ├── out_matching_basic.0.tap ├── out_matching_basic_failing.1.documentation ├── out_matching_basic_failing.1.rspecish ├── out_matching_basic_failing.1.tap ├── out_matching_basic_failing.2.nagios ├── out_matching_transformers.0.documentation ├── out_matching_transformers.0.nagios ├── out_matching_transformers.0.rspecish ├── out_matching_transformers.0.tap ├── out_matching_transformers_failing.1.documentation ├── out_matching_transformers_failing.1.rspecish ├── out_matching_transformers_failing.1.tap ├── out_matching_transformers_failing.2.nagios └── passing.goss.yaml ├── util ├── build.go ├── command.go ├── command_windows.go ├── config.go └── config_test.go └── validate.go /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | indent_size = 4 16 | 17 | [Makefile] 18 | indent_style = tab 19 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @aelsabbahy 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **How To Reproduce** 14 | 16 | 17 | **Expected Behavior** 18 | 19 | 20 | **Actual Behavior** 21 | 22 | 23 | **Environment:** 24 | - Version of goss 25 | - OS/Distribution version (if applicable) 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 20 | 21 | **Describe the feature:** 22 | 23 | 24 | **Describe the solution you'd like** 25 | 26 | 27 | **Describe alternatives you've considered** 28 | 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about goss 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://docs.github.com/en/github/administering-a-repository/enabling-and-disabling-version-updates 3 | version: 2 4 | updates: 5 | - package-ecosystem: "gomod" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | day: "saturday" 10 | assignees: 11 | - "aelsabbahy" 12 | reviewers: 13 | - "aelsabbahy" 14 | open-pull-requests-limit: 0 15 | 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | day: "saturday" 21 | 22 | - package-ecosystem: "pip" 23 | directory: "/docs" 24 | schedule: 25 | interval: "weekly" 26 | day: "saturday" 27 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | ##### Checklist 14 | 15 | 16 | - [ ] `make test-all` (UNIX) passes. CI will also test this 17 | - [ ] unit and/or integration tests are included (if applicable) 18 | - [ ] documentation is changed or added (if applicable) 19 | 20 | 21 | 22 | ### Description of change 23 | 27 | -------------------------------------------------------------------------------- /.github/stale.yml.disabled: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - approved 8 | # Label to use when marking an issue as stale 9 | staleLabel: stale 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale because it has not had 13 | recent activity. It will be closed if no further activity occurs. Thank you 14 | for your contributions. 15 | # Comment to post when closing a stale issue. Set to `false` to disable 16 | closeComment: false 17 | -------------------------------------------------------------------------------- /.github/workflows/docker-integration-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Docker images for integration tests 2 | 3 | on: 4 | # push: 5 | # branches: 6 | # - master 7 | workflow_dispatch: 8 | 9 | env: 10 | PLATFORMS: "linux/amd64" 11 | 12 | jobs: 13 | list-dockerfiles: 14 | name: Create list of existing dockerfiles 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Get file list 20 | id: set-matrix 21 | run: | 22 | # lists all Dockerfile_* and ignore (grep) files with extension (e.g. *.md5) 23 | # tranforms the file list in JSON array (StackOverflow#10234327) 24 | # converts the list into objects of dockerfile and image name 25 | ls integration-tests/Dockerfile_* | 26 | grep -Ev "\..{0,3}$" | 27 | jq -R -s 'split("\n")[:-1]' | 28 | jq '. | map({dockerfile: ., image: sub(".*_"; "")})' > filelist.json 29 | echo "matrix=$(jq -c . filelist.json)" >> "$GITHUB_OUTPUT" 30 | outputs: 31 | matrix: ${{ steps.set-matrix.outputs.matrix }} 32 | 33 | docker: 34 | needs: [list-dockerfiles] 35 | name: Build and push Docker image 36 | runs-on: ubuntu-latest 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | include: ${{ fromJson(needs.list-dockerfiles.outputs.matrix) }} 41 | permissions: 42 | packages: write 43 | contents: read 44 | 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | with: 49 | fetch-depth: 0 50 | 51 | - name: Set up QEMU 52 | uses: docker/setup-qemu-action@v3 53 | 54 | - name: Set up Docker Buildx 55 | uses: docker/setup-buildx-action@v3 56 | 57 | - name: Login to GHCR 58 | uses: docker/login-action@v3 59 | with: 60 | registry: ghcr.io 61 | username: ${{ github.repository_owner }} 62 | password: ${{ secrets.GITHUB_TOKEN }} 63 | 64 | - name: MD5 of Dockerfile 65 | id: md5_result 66 | run: | 67 | echo "md5=$(md5sum "${{ matrix.dockerfile }}" | awk '{ print $1 }')" >> $GITHUB_OUTPUT 68 | 69 | - name: Extract metadata (tags, labels) for Docker 70 | id: meta 71 | uses: docker/metadata-action@v5 72 | with: 73 | images: | 74 | ghcr.io/${{ github.repository_owner }}/${{ matrix.image }} 75 | labels: | 76 | rocks.goss.dockerfile-md5=${{ steps.md5_result.outputs.md5 }} 77 | 78 | - name: Build and push tag 79 | uses: docker/build-push-action@v6 80 | with: 81 | context: . 82 | file: ${{ matrix.dockerfile }} 83 | push: true 84 | tags: | 85 | ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:latest 86 | labels: ${{ steps.meta.outputs.labels }} 87 | platforms: ${{ env.PLATFORMS }} 88 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | paths: 9 | - mkdocs.yml 10 | - docs/** 11 | - README.md 12 | - LICENSE 13 | - extras/**/README.md 14 | - .github/CONTRIBUTING.md 15 | workflow_dispatch: 16 | 17 | jobs: 18 | lint: 19 | name: Lint Documentation 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: DavidAnson/markdownlint-cli2-action@v16 24 | with: 25 | globs: | 26 | docs/**/*.md 27 | README.md 28 | extras/**/README.md 29 | .github/CONTRIBUTING.md 30 | 31 | build: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | - uses: actions/setup-python@v5 37 | with: 38 | python-version: "3.12" 39 | cache: 'pip' 40 | - name: Install dependencies 41 | run: | 42 | pip install --upgrade pip 43 | pip install --requirement docs/requirements.txt 44 | - name: Build documentation 45 | run: mkdocs build 46 | # To remove if not using github pages 47 | - name: Upload artifact 48 | uses: actions/upload-pages-artifact@v3 49 | with: 50 | path: site 51 | -------------------------------------------------------------------------------- /.github/workflows/golangci.yaml: -------------------------------------------------------------------------------- 1 | name: Golang ci 2 | on: 3 | # don't build any branch other than master (and prs) when git pushed 4 | pull_request: {} 5 | push: 6 | branches: 7 | - master 8 | - "/^v\\d+\\.\\d+(\\.\\d+)?(-\\S*)?$/" 9 | paths-ignore: 10 | - "**/*.md" 11 | 12 | permissions: 13 | contents: read 14 | pull-requests: read 15 | 16 | jobs: 17 | lint: 18 | name: lint 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-go@v5 23 | with: 24 | go-version-file: go.mod 25 | 26 | - name: golangci-lint 27 | uses: golangci/golangci-lint-action@v6 28 | with: 29 | version: v1.59 30 | 31 | coverage: 32 | needs: [lint] 33 | name: coverage 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-go@v5 38 | with: 39 | go-version-file: go.mod 40 | 41 | - name: Unit tests and coverage 42 | run: make cov 43 | 44 | integration-test: 45 | needs: [coverage] 46 | name: Integration tests 47 | runs-on: ${{ matrix.os }} 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | os: [ubuntu-latest, macos-latest, windows-latest] 52 | steps: 53 | - uses: actions/checkout@v4 54 | - uses: actions/setup-go@v5 55 | with: 56 | go-version-file: go.mod 57 | 58 | - name: Integration tests 59 | shell: bash 60 | run: | 61 | os_name="$(go env GOOS)" 62 | if [[ "${os_name}" == "darwin" || "${os_name}" == "windows" ]]; then 63 | make "test-int-${os_name}-all" 64 | else 65 | # linux runs all tests; 66 | make test-int-all 67 | fi 68 | -------------------------------------------------------------------------------- /.github/workflows/preview-docs.yaml: -------------------------------------------------------------------------------- 1 | name: Preview documentation 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | paths: 7 | - mkdocs.yml 8 | - docs/** 9 | - README.md 10 | - LICENSE 11 | - extras/**/README.md 12 | - .github/CONTRIBUTING.md 13 | 14 | jobs: 15 | pull-request-links: 16 | name: Add preview link to pull-request 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: write 20 | steps: 21 | - uses: readthedocs/actions/preview@v1 22 | with: 23 | project-slug: goss 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: "Build release artifacts" 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version-file: go.mod 21 | 22 | - name: Get version from tag 23 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 24 | run: echo "TRAVIS_TAG=${{ github.ref_name }}" >> $GITHUB_ENV 25 | 26 | - run: make release 27 | - run: make dgoss-sha256 dcgoss-sha256 kgoss-sha256 28 | 29 | - name: "Upload binary as artifact" 30 | uses: actions/upload-artifact@v4 31 | with: 32 | retention-days: 5 33 | if-no-files-found: error 34 | name: build 35 | path: | 36 | release/* 37 | extras/*/*goss 38 | extras/*/*goss.sha256 39 | 40 | attach-assets: 41 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 42 | needs: ["build"] 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Fetch all binaries 46 | uses: actions/download-artifact@v4 47 | - name: Attach to release 48 | uses: softprops/action-gh-release@v2 49 | with: 50 | files: build/** 51 | fail_on_unmatched_files: true 52 | -------------------------------------------------------------------------------- /.github/workflows/trivy-schedule.yaml: -------------------------------------------------------------------------------- 1 | name: Trivy Code Scanning 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 * * 5" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | trivy-scan: 10 | name: Trivy scan 11 | runs-on: ubuntu-latest 12 | permissions: 13 | packages: read 14 | security-events: write 15 | 16 | steps: 17 | - name: Run Trivy vulnerability scanner 18 | uses: aquasecurity/trivy-action@0.24.0 19 | with: 20 | image-ref: ghcr.io/${{ github.repository_owner }}/goss:latest 21 | format: "sarif" 22 | output: "trivy-results.sarif" 23 | 24 | - name: Upload Trivy scan results to GitHub Security tab 25 | uses: github/codeql-action/upload-sarif@v3 26 | with: 27 | sarif_file: "trivy-results.sarif" 28 | -------------------------------------------------------------------------------- /.github/workflows/yamllint.yaml: -------------------------------------------------------------------------------- 1 | name: Validate YAML 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | paths: 9 | - "**/*.ya?ml" 10 | 11 | jobs: 12 | validate-yaml: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Validate YAML file 17 | run: make lint-yaml 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | /main 3 | *.bak 4 | /goss 5 | /release 6 | /integration-tests/goss/goss 7 | /integration-tests/**/*-generated* 8 | /vendor/ 9 | /integration-tests/**/goss-linux-386 10 | /integration-tests/**/goss-linux-amd64 11 | 12 | # Random stuff for my local testing/development that I don't want checked in 13 | tmp/ 14 | /goss.yaml 15 | 16 | /.idea 17 | 18 | /c.out 19 | /c.out.tmp 20 | 21 | # Documentation 22 | ## Virtualenv 23 | /.venv 24 | ## MkDocs rendered site 25 | /site 26 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | # Disable all linters. 3 | # Default: false 4 | disable-all: true 5 | # Enable specific linter 6 | # https://golangci-lint.run/usage/linters/#enabled-by-default 7 | enable: 8 | # default linter 9 | # - errcheck # there are to many failures at the moment 10 | - gosimple 11 | - govet 12 | - ineffassign 13 | - staticcheck 14 | - unused 15 | # custom linter 16 | - gofmt 17 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Enable all rules 3 | default: true 4 | 5 | # Enforce asterisk for unordered lists 6 | # See: https://github.com/DavidAnson/markdownlint/blob/main/doc/md004.md 7 | MD004: 8 | style: asterisk 9 | 10 | # Set list indent level to 4 which Python-Markdown requires 11 | # See: 12 | # - https://github.com/DavidAnson/markdownlint/blob/main/doc/md007.md 13 | # - https://python-markdown.github.io/#differences 14 | MD007: 15 | indent: 4 16 | 17 | # Tune `line-length` 18 | # See: https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md 19 | MD013: 20 | line_length: 120 21 | tables: false 22 | code_blocks: false 23 | 24 | # Disable `blanks-around-list` (to stay close from GitHub-flavored markdown) 25 | # See: 26 | # - https://github.com/DavidAnson/markdownlint/blob/main/doc/md032.md 27 | # - https://python-markdown.github.io/#differences 28 | MD032: false 29 | 30 | # Disable `no-space-in-code` 31 | # Generate lots of false positive with admonitions and code blocks 32 | MD038: false 33 | 34 | # Disable `code-blocks-style` 35 | # Use fenced code blocks everywhere but raise false positives with admonitions 36 | MD046: false 37 | 38 | # Disable `link-fragments` 39 | # Only works for github-rendered markdown (which does not have the same rules as MkDocs) 40 | # See: https://github.com/DavidAnson/markdownlint/blob/main/doc/md051.md 41 | MD051: false 42 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for MkDocs projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | 13 | mkdocs: 14 | configuration: mkdocs.yml 15 | 16 | # Optionally declare the Python requirements required to build your docs 17 | python: 18 | install: 19 | - requirements: docs/requirements.txt 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | 4 | go: 5 | - 1.23.x 6 | 7 | os: 8 | - osx 9 | - linux 10 | - windows 11 | 12 | dist: focal 13 | osx_image: xcode12.2 14 | 15 | services: 16 | - docker 17 | 18 | # don't build any branch other than master (and prs) when git pushed 19 | branches: 20 | only: 21 | - master 22 | - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ 23 | 24 | 25 | before_install: 26 | - if [[ "${TRAVIS_OS_NAME}" == "windows" ]]; then choco install make; fi 27 | # bash from macOS is too old to have readarray. Install newer version. 28 | - if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then HOMEBREW_NO_AUTO_UPDATE=1 brew install bash; fi 29 | 30 | script: 31 | - ./ci/build.sh 32 | 33 | # deploy: 34 | # provider: releases 35 | # api_key: 36 | # secure: ijNltjw/mIHIOx8vLZ6asUun3SbY7D+XZbs5NX8vcIv0jvOiwaaT1hqny7SQBHfGZzqHsYUSS/GYAYJdBqKFFfGmTZsl90hFT6D0RGdz9C71UVxNFX4wQ5KQ/WVvdMT2SrLymGvu9TvoU0VG8OWqWVdxSlUPf6qOTGAagrzg+Tbsbb6czeiG67mlBBL23XSlfMG1p45UxzvI41SZj2R3ElUb0hym1CrFaoC36PBGrb0x41TXzvd8J7cu6xDzgczYhnYQQZpS6f2YcqNV1z0f+P67EQqQiDWIIcK2jE/YG+RgM8cbpLMiMec8CDiwNCsejBA5EbVMlGJlODvBXT5NmMBeugueqfSHEfkl5qZTQG4AOAT7UsqbnM7r0NqzmaE5Lj90igvJK6rNsH1ZRe79WfSsTtuzlkkouHGvyoz0M8gnMSzpbbwoyIy+UT0hhPMoZvIpXfr43en5WkbkPKfop0p4Vjc8NGg0iD45q1JAvIVTtz/WvWTknM1P8e3u+TiDTaZkcJJmFaBqgaeLoWktOGfi54p9nhgQnSyBYt4PyvhWDQs7QFmX0BdKlqJCESvUOJTe1t6zJJsV7Gn/3sGCN7JUEwbnXTsCoMjjFFUvQdm0Ur7t7/2xU3kO+dyfqcdM/5SYFeppQcjHI0ckhI51mIoBTsJsGvaVwKKL1I4cyBU= 37 | # file: 38 | # - release/goss-darwin-amd64 39 | # - release/goss-darwin-amd64.sha256 40 | # - release/goss-darwin-arm64 41 | # - release/goss-darwin-arm64.sha256 42 | # - release/goss-linux-amd64 43 | # - release/goss-linux-amd64.sha256 44 | # - release/goss-linux-386 45 | # - release/goss-linux-386.sha256 46 | # - release/goss-linux-arm 47 | # - release/goss-linux-arm.sha256 48 | # - release/goss-linux-arm64 49 | # - release/goss-linux-arm64.sha256 50 | # - release/goss-linux-s390x 51 | # - release/goss-linux-s390x.sha256 52 | # - release/goss-windows-amd64.exe 53 | # - release/goss-windows-amd64.exe.sha256 54 | # - extras/dgoss/dgoss 55 | # - extras/dgoss/dgoss.sha256 56 | # skip_cleanup: true 57 | # on: 58 | # repo: goss-org/goss 59 | # tags: true 60 | # condition: $TRAVIS_OS_NAME = linux 61 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | ignore: 5 | # uses go templates (these are invalid yaml files) 6 | - integration-tests/goss/goss-service.yaml 7 | - integration-tests/goss/goss-shared.yaml 8 | - docs/goss.yaml 9 | 10 | rules: 11 | braces: 12 | min-spaces-inside: 0 13 | max-spaces-inside: 1 # required for schema.yaml 14 | brackets: 15 | min-spaces-inside: 0 16 | max-spaces-inside: 1 # required for schema.yaml 17 | comments-indentation: disable 18 | indentation: 19 | spaces: consistent 20 | indent-sequences: consistent 21 | line-length: disable 22 | document-start: disable 23 | truthy: 24 | allowed-values: 25 | - "on" # required for github workflows 26 | - "false" 27 | - "true" 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.22 2 | 3 | FROM docker.io/golang:${GO_VERSION}-alpine AS base 4 | 5 | ARG GOSS_VERSION=v0.0.0 6 | WORKDIR /build 7 | 8 | RUN --mount=target=. \ 9 | CGO_ENABLED=0 go build \ 10 | -ldflags "-X github.com/goss-org/goss/util.Version=${GOSS_VERSION} -s -w" \ 11 | -o "/release/goss" \ 12 | ./cmd/goss 13 | 14 | FROM alpine:3.19 15 | 16 | COPY --from=base /release/* /usr/bin/ 17 | 18 | RUN mkdir /goss 19 | VOLUME /goss 20 | -------------------------------------------------------------------------------- /ci/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | os_name="$(go env GOOS)" 5 | 6 | # darwin & windows do not support integration-testing approach via docker. 7 | # platform support is coupled to the travis CI environment, which is stable 'enough'. 8 | if [[ "${os_name}" == "darwin" || "${os_name}" == "windows" ]]; then 9 | make "test-${os_name}-all" 10 | else 11 | # linux runs all tests; unit and integration. 12 | make all 13 | fi 14 | -------------------------------------------------------------------------------- /ci/go-fmt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | os_name="$(go env GOOS)" 5 | 6 | # gofmt must be on PATH 7 | command -v gofmt 8 | 9 | if [[ "${os_name}" == "windows" ]]; then 10 | echo "Skipping go-fmt on Windows because line-endings cause every file to need formatting." 11 | echo "Linux is treated as authoritative." 12 | echo "Exiting 0..." 13 | exit 0 14 | fi 15 | 16 | fmt="$(go fmt github.com/goss-org/goss/...)" 17 | 18 | if [[ -z "${fmt}" ]]; then 19 | echo "valid gofmt" 20 | else 21 | echo "invalid gofmt:" 22 | echo "${fmt}" 23 | exit 1 24 | fi 25 | -------------------------------------------------------------------------------- /ci/go-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | command -v go 5 | 6 | go test -coverpkg=./... ./... -coverprofile="c.out" 7 | 8 | sed 's|github.com/goss-org/goss/||' <"c.out" >"c.out.tmp" 9 | 10 | mv "c.out.tmp" "c.out" 11 | -------------------------------------------------------------------------------- /ci/lib/log.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # comment out the unused-ones so far until they're needed. Otherwise it's a google search to find them again. 4 | NOCOLOUR='\033[0m' 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | ORANGE='\033[0;33m' 8 | # BLUE='\033[0;34m' 9 | # PURPLE='\033[0;35m' 10 | CYAN='\033[0;36m' 11 | # LIGHTGRAY='\033[0;37m' 12 | # DARKGRAY='\033[1;30m' 13 | LIGHTRED='\033[1;31m' 14 | LIGHTGREEN='\033[1;32m' 15 | # YELLOW='\033[1;33m' 16 | # LIGHTBLUE='\033[1;34m' 17 | # LIGHTPURPLE='\033[1;35m' 18 | LIGHTCYAN='\033[1;36m' 19 | # WHITE='\033[1;37m' 20 | 21 | is_ci() { 22 | if [[ "${CI:-}" == "true" ]]; then 23 | echo "true" 24 | else 25 | echo "false" 26 | fi 27 | } 28 | 29 | log_action() { 30 | echo -e "${LIGHTGREEN}${*}${NOCOLOUR}" >&2 31 | } 32 | log_warn() { 33 | echo -e "${ORANGE}${*}${NOCOLOUR}" >&2 34 | } 35 | log_error() { 36 | echo -e "${LIGHTRED}${*}${NOCOLOUR}" >&2 37 | } 38 | log_debug() { 39 | if [[ -n "${SCRIPT_LOG_LEVEL:-}" && "${SCRIPT_LOG_LEVEL}" == "debug" ]]; then 40 | echo -e "${CYAN}${*}${NOCOLOUR}" >&2 41 | fi 42 | } 43 | log_info() { 44 | echo -e "${LIGHTCYAN}${*}${NOCOLOUR}" >&2 45 | } 46 | log_success() { 47 | echo -e "${GREEN}${*}${NOCOLOUR}" >&2 48 | } 49 | log_fatal() { 50 | echo -e "${RED}${*}${NOCOLOUR}" >&2 51 | exit "${2:-"1"}" 52 | } 53 | -------------------------------------------------------------------------------- /ci/lib/setup.sh: -------------------------------------------------------------------------------- 1 | # configure cwd, vars and logging 2 | _setup_env() { 3 | # -ET: propagate DEBUG/RETURN/ERR traps to functions and subshells 4 | set -ET 5 | # exit on unhandled error 6 | set -o errexit 7 | # exit on unset variable 8 | set -o nounset 9 | # pipefail: any failure in a pipe causes the pipe to fail 10 | set -o pipefail 11 | 12 | if [[ -n "${SCRIPT_DEBUG:-}" ]]; then 13 | set -o xtrace 14 | # http://www.skybert.net/bash/debugging-bash-scripts-on-the-command-line/ 15 | export PS4='# ${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]:-}() - [${SHLVL},${BASH_SUBSHELL},$?] ' 16 | fi 17 | trap _err_trap ERR 18 | # shellcheck disable=SC2034 19 | # START_DIR is used elsewhere. 20 | START_DIR="${PWD}" 21 | export START_DIR 22 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[2]}")" && pwd)" 23 | readonly TOP_SCRIPT="${SCRIPT_DIR}/$(basename "${BASH_SOURCE[2]}")" 24 | if [[ -z "${SCRIPT_DIR}" ]]; then 25 | echo >&2 -e "setup.sh:\tFailed to determine directory containing executed script." 26 | return 1 27 | fi 28 | if ! cd "$(dirname "${BASH_SOURCE[0]}")/../.."; then 29 | echo >&2 -e "setup.sh:\tFailed to cd to repository root" 30 | return 1 31 | fi 32 | REPO_ROOT="$(pwd)" 33 | export REPO_ROOT 34 | if ! source ci/lib/log.sh; then 35 | echo >&2 -e "setup.sh:\tFailed to source logging library" 36 | return 1 37 | fi 38 | } 39 | 40 | _err_trap() { 41 | local err=$? 42 | local cmd="${BASH_COMMAND:-}" 43 | # Disable echoing all commands as this makes the traceback really hard to follow 44 | set +x 45 | if [[ -n "${SKIP_BASH_STACKTRACE:-}" ]]; then 46 | log_debug "SKIP_BASH_STACKTRACE was set to something; silencing bash stack-trace." 47 | exit "${err}" 48 | fi 49 | 50 | echo >&2 "panic: uncaught error" 1>&2 51 | print_traceback 1 52 | echo >&2 "${cmd} exited ${err}" 1>&2 53 | } 54 | 55 | _setup_constants() { 56 | export EXIT_SUCCESS=0 57 | export EXIT_INVALID_ARGUMENT=66 58 | export EXIT_FAILED_TO_SOURCE=67 59 | export EXIT_FAILED_TO_CD=68 60 | export EXIT_FAILED_AFTER_RETRY=69 61 | export EXIT_NOT_FOUND=70 62 | } 63 | 64 | # Print traceback of call stack, starting from the call location. 65 | # An optional argument can specify how many additional stack frames to skip. 66 | print_traceback() { 67 | local skip=${1:-0} 68 | local start=$((skip + 1)) 69 | local end=${#BASH_SOURCE[@]} 70 | local curr=0 71 | echo >&2 "Traceback (most recent call first):" 1>&2 72 | for ((curr = start; curr < end; curr++)); do 73 | local prev=$((curr - 1)) 74 | local func="${FUNCNAME[$curr]}" 75 | local file="${BASH_SOURCE[$curr]}" 76 | local line="${BASH_LINENO[$prev]}" 77 | echo >&2 " at ${file}:${line} in ${func}()" 1>&2 78 | done 79 | } 80 | 81 | _setup_env || exit $? 82 | -------------------------------------------------------------------------------- /development/README.md: -------------------------------------------------------------------------------- 1 | # Random development scripts 2 | 3 | Nothing to see here, carry on :) 4 | -------------------------------------------------------------------------------- /development/build_images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeu 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | INTEGRATION_TEST_DIR="$SCRIPT_DIR/../integration-tests/" 7 | CONTAINER_REPOSITORY="aelsabbahy" 8 | 9 | LABEL_DATE=$(date -u +'%Y-%m-%dT%H:%M:%S.%3NZ') 10 | LABEL_URL="https://github.com/goss-org/goss" 11 | LABEL_REVISION=$(git rev-parse HEAD) 12 | 13 | for docker_file in $INTEGRATION_TEST_DIR/Dockerfile_*; do 14 | [[ $docker_file == *.md5 ]] && continue 15 | os=$(cut -d '_' -f2 <<<"$docker_file") 16 | md5=$(md5sum "$docker_file" | awk '{ print $1 }') 17 | docker build \ 18 | --label "org.opencontainers.image.created=$LABEL_DATE" \ 19 | --label "org.opencontainers.image.description=Quick and Easy server testing/validation" \ 20 | --label "org.opencontainers.image.licenses=Apache-2.0" \ 21 | --label "org.opencontainers.image.revision=$LABEL_REVISION" \ 22 | --label "org.opencontainers.image.source=$LABEL_URL" \ 23 | --label "org.opencontainers.image.title=goss" \ 24 | --label "org.opencontainers.image.url=$LABEL_URL" \ 25 | --label "org.opencontainers.image.version=manual" \ 26 | --label "rocks.goss.dockerfile-md5"=$md5 \ 27 | -t "$CONTAINER_REPOSITORY/goss_${os}:latest" - < "$docker_file" 28 | done 29 | -------------------------------------------------------------------------------- /development/debian/.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant/ 2 | -------------------------------------------------------------------------------- /development/push_images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeu 4 | 5 | SCRIPT_DIR=$(readlink -f "$(dirname "$0")") 6 | CONTAINER_REPOSITORY="aelsabbahy" 7 | images=$(docker images | grep "^$CONTAINER_REPOSITORY/goss_.*latest" | awk '$0=$1') 8 | 9 | # Use md5sum to determine if CI needs to do a docker build 10 | pushd "$SCRIPT_DIR/../integration-tests"; 11 | for dockerfile in Dockerfile_*;do 12 | [[ $dockerfile == *.md5 ]] && continue 13 | md5sum "$dockerfile" > "${dockerfile}.md5" 14 | done 15 | popd 16 | 17 | for image in $images; do 18 | docker push "${image}:latest" 19 | done 20 | -------------------------------------------------------------------------------- /docs/.pages: -------------------------------------------------------------------------------- 1 | nav: 2 | - Home: index.md 3 | - installation.md 4 | - quickstart.md 5 | - container_image.md 6 | - Command Reference: cli.md 7 | - The gossfile: gossfile.md 8 | - migrations.md 9 | - platforms.md 10 | - containers 11 | - Contributing: contributing.md 12 | - changelog.md 13 | - license.md 14 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | `Goss` does not (yet?) maintain a changelog file. 4 | However, you can consult [Goss releases](https://github.com/goss-org/goss/releases). 5 | -------------------------------------------------------------------------------- /docs/container_image.md: -------------------------------------------------------------------------------- 1 | # Goss container image 2 | 3 | ## Dockerfiles 4 | 5 | * [latest](https://github.com/goss-org/goss/blob/master/Dockerfile) 6 | 7 | ## Using the base image 8 | 9 | This is a simple alpine image with Goss preinstalled on it. 10 | Can be used as a base image for your projects to allow for easy health checking. 11 | 12 | ### Mount example 13 | 14 | Create the container 15 | 16 | ```sh 17 | docker run --name goss ghcr.io/goss-org/goss goss 18 | ``` 19 | 20 | Create your container and mount goss 21 | 22 | ```sh 23 | docker run --rm -it --volumes-from goss --name weby nginx 24 | ``` 25 | 26 | Run goss inside your container 27 | 28 | ```sh 29 | docker exec weby /goss/goss autoadd nginx 30 | ``` 31 | 32 | ### HEALTHCHECK example 33 | 34 | ```dockerfile 35 | FROM ghcr.io/goss-org/goss:latest 36 | 37 | COPY goss/ /goss/ 38 | HEALTHCHECK --interval=1s --timeout=6s CMD goss -g /goss/goss.yaml validate 39 | 40 | # your stuff.. 41 | ``` 42 | 43 | ### Startup delay example 44 | 45 | ```dockerfile 46 | FROM ghcr.io/goss-org/goss:latest 47 | 48 | COPY goss/ /goss/ 49 | 50 | # Alternatively, the -r option can be set 51 | # using the GOSS_RETRY_TIMEOUT env variable 52 | CMD goss -g /goss/goss.yaml validate -r 5m && exec real_comand.. 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/containers/docker-compose.md: -------------------------------------------------------------------------------- 1 | 2 | --8<-- "extras/dcgoss/README.md" 3 | -------------------------------------------------------------------------------- /docs/containers/docker.md: -------------------------------------------------------------------------------- 1 | 2 | --8<-- "extras/dgoss/README.md" 3 | -------------------------------------------------------------------------------- /docs/containers/kubernetes.md: -------------------------------------------------------------------------------- 1 | 2 | --8<-- "extras/kgoss/README.md" 3 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | 2 | --8<-- ".github/CONTRIBUTING.md" 3 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Goss - Quick and Easy server validation 2 | 3 | --8<-- "README.md:intro" 4 | --8<-- "README.md:about" 5 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | --8<-- "README.md:install" 4 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | 2 | --8<-- "LICENSE" 3 | -------------------------------------------------------------------------------- /docs/migrations.md: -------------------------------------------------------------------------------- 1 | # Migration guide 2 | 3 | ## v4 migration 4 | 5 | ### Array matchers (e.g. user.groups) no longer allows duplicates 6 | 7 | Goss v0.3.X allowed: 8 | 9 | ```yaml 10 | user: 11 | root: 12 | exists: true 13 | groups: 14 | - root 15 | - root 16 | - root 17 | ``` 18 | 19 | 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 20 | contain matchers. The test below is valid for v0.4.x but not valid for v0.3.x 21 | 22 | ```yaml 23 | user: 24 | root: 25 | exists: true 26 | groups: 27 | - have-prefix: r 28 | ``` 29 | 30 | ## rpm now contains the full EVR version 31 | 32 | To enable the ability to compare RPM versions in the future, The version matching of rpm has changed 33 | 34 | from: 35 | 36 | ```console 37 | rpm -q --nosignature --nohdrchk --nodigest --qf '%{VERSION}\n' package_name 38 | ``` 39 | 40 | to: 41 | 42 | ```console 43 | rpm -q --nosignature --nohdrchk --nodigest --qf '%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\n' package_name 44 | ``` 45 | 46 | ## `file.contains` -> `file.contents` 47 | 48 | File contains attribute has been renamed to file.contents 49 | 50 | from: 51 | 52 | ```yaml 53 | file: 54 | /tmp/foo: 55 | exists: true 56 | contains: [] 57 | ``` 58 | 59 | to: 60 | 61 | ```yaml 62 | file: 63 | /tmp/foo: 64 | exists: true 65 | contents: [] 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/myapp_gossfile.yaml: -------------------------------------------------------------------------------- 1 | # This is a sample file referenced by goss.yaml 2 | # Used for render test and Json schema validation. 3 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quick start 2 | 3 | --8<-- "README.md:quickstart" 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs-material==9.5.23 2 | mkdocs-macros-plugin==1.0.5 3 | mkdocs-awesome-pages-plugin==2.9.2 4 | mkdocs-exclude==1.0.2 5 | mdx-breakless-lists==1.0.1 6 | pygments==2.18.0 7 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | .green { 2 | color: green; 3 | } 4 | 5 | .blue { 6 | color: cyan; 7 | } 8 | 9 | .orange { 10 | color: orange; 11 | } 12 | 13 | .red { 14 | color: red; 15 | } 16 | -------------------------------------------------------------------------------- /docs/vars.yaml: -------------------------------------------------------------------------------- 1 | # Sample vars file used in goss.yaml#matching 2 | # Used for render test and Json schema validation. 3 | instance_count: 1 4 | failures: 0 5 | status: "PASS" 6 | -------------------------------------------------------------------------------- /examples/goss.yaml: -------------------------------------------------------------------------------- 1 | gossfile: 2 | goss_awesome_gomega.yaml: {} 3 | 4 | file: 5 | test.txt: 6 | exists: true 7 | contains: | 8 | test file 9 | second line 10 | 11 | command: 12 | echo '15': 13 | exit-status: 0 14 | stdout: 15 | and: 16 | - gt: 10 17 | - lt: 50 18 | - match-regexp: '\d{2}' 19 | timeout: 10000 20 | 21 | http: 22 | https://ifconfig.me: 23 | status: 200 24 | timeout: 5000 25 | body: '{{.Vars.Ip}}' 26 | -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | # How to run this 2 | 3 | Basically, run the following: `goss --vars-inline "Ip: $EXTERNAL_IP" v` 4 | -------------------------------------------------------------------------------- /examples/test.txt: -------------------------------------------------------------------------------- 1 | test file 2 | second line 3 | -------------------------------------------------------------------------------- /extras/dcgoss/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | db: 5 | image: mysql:5.7 6 | volumes: 7 | - db_data:/var/lib/mysql 8 | restart: always 9 | environment: 10 | MYSQL_ROOT_PASSWORD: somewordpress 11 | MYSQL_DATABASE: wordpress 12 | MYSQL_USER: wordpress 13 | MYSQL_PASSWORD: wordpress 14 | 15 | wordpress: 16 | depends_on: 17 | - db 18 | image: wordpress:latest 19 | ports: 20 | - "8000:80" 21 | restart: always 22 | environment: 23 | WORDPRESS_DB_HOST: db:3306 24 | WORDPRESS_DB_USER: wordpress 25 | WORDPRESS_DB_PASSWORD: wordpress 26 | WORDPRESS_DB_NAME: wordpress 27 | volumes: 28 | db_data: {} 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/goss-org/goss 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/Masterminds/sprig/v3 v3.3.0 7 | github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2 8 | github.com/blang/semver/v4 v4.0.0 9 | github.com/cheekybits/genny v1.0.0 10 | github.com/fatih/color v1.17.0 11 | github.com/goss-org/GOnetstat v0.0.0-20230101144325-22be0bd9e64d 12 | github.com/goss-org/go-ps v0.0.0-20230609005227-7b318e6a56e5 13 | github.com/hashicorp/logutils v1.0.0 14 | github.com/miekg/dns v1.1.61 15 | github.com/moby/sys/mountinfo v0.7.1 16 | github.com/oleiade/reflections v1.0.1 17 | github.com/onsi/gomega v1.33.1 18 | github.com/patrickmn/go-cache v2.1.0+incompatible 19 | github.com/pmezard/go-difflib v1.0.0 20 | github.com/prometheus/client_golang v1.19.1 21 | github.com/prometheus/common v0.55.0 22 | github.com/samber/lo v1.46.0 23 | github.com/stretchr/testify v1.9.0 24 | github.com/tidwall/gjson v1.17.1 25 | github.com/urfave/cli v1.22.14 26 | gopkg.in/yaml.v3 v3.0.1 27 | gotest.tools/v3 v3.5.1 28 | ) 29 | 30 | require ( 31 | dario.cat/mergo v1.0.1 // indirect 32 | github.com/Masterminds/goutils v1.1.1 // indirect 33 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 34 | github.com/beorn7/perks v1.0.1 // indirect 35 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 36 | github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect 37 | github.com/davecgh/go-spew v1.1.1 // indirect 38 | github.com/google/go-cmp v0.6.0 // indirect 39 | github.com/google/uuid v1.6.0 // indirect 40 | github.com/huandu/xstrings v1.5.0 // indirect 41 | github.com/mattn/go-colorable v0.1.13 // indirect 42 | github.com/mattn/go-isatty v0.0.20 // indirect 43 | github.com/mitchellh/copystructure v1.2.0 // indirect 44 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 45 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 46 | github.com/prometheus/client_model v0.6.1 // indirect 47 | github.com/prometheus/procfs v0.15.1 // indirect 48 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 49 | github.com/shopspring/decimal v1.4.0 // indirect 50 | github.com/spf13/cast v1.7.0 // indirect 51 | github.com/tidwall/match v1.1.1 // indirect 52 | github.com/tidwall/pretty v1.2.1 // indirect 53 | golang.org/x/crypto v0.26.0 // indirect 54 | golang.org/x/mod v0.19.0 // indirect 55 | golang.org/x/net v0.27.0 // indirect 56 | golang.org/x/sync v0.8.0 // indirect 57 | golang.org/x/sys v0.23.0 // indirect 58 | golang.org/x/text v0.17.0 // indirect 59 | golang.org/x/tools v0.23.0 // indirect 60 | google.golang.org/protobuf v1.34.2 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | { 4 | set -e 5 | 6 | LATEST_URL="https://github.com/goss-org/goss/releases/latest" 7 | LATEST_EFFECTIVE=$(curl -s -L -o /dev/null ${LATEST_URL} -w '%{url_effective}') 8 | LATEST=${LATEST_EFFECTIVE##*/} 9 | 10 | DGOSS_VER=$GOSS_VER 11 | 12 | if [ -z "$GOSS_VER" ]; then 13 | GOSS_VER=${GOSS_VER:-$LATEST} 14 | DGOSS_VER='master' 15 | fi 16 | if [ -z "$GOSS_VER" ]; then 17 | echo "ERROR: Could not automatically detect latest version, set GOSS_VER env var and re-run" 18 | exit 1 19 | fi 20 | GOSS_DST=${GOSS_DST:-/usr/local/bin} 21 | INSTALL_LOC="${GOSS_DST%/}/goss" 22 | DGOSS_INSTALL_LOC="${GOSS_DST%/}/dgoss" 23 | touch "$INSTALL_LOC" || { echo "ERROR: Cannot write to $GOSS_DST set GOSS_DST elsewhere or use sudo"; exit 1; } 24 | 25 | arch="" 26 | if [ "$(uname -m)" = "x86_64" ]; then 27 | arch="amd64" 28 | elif [ "$(uname -m)" = "aarch32" ]; then 29 | arch="arm" 30 | elif [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; then 31 | arch="arm64" 32 | else 33 | arch="386" 34 | fi 35 | 36 | url="https://github.com/goss-org/goss/releases/download/$GOSS_VER/goss-linux-$arch" 37 | 38 | echo "Downloading $url" 39 | curl -L "$url" -o "$INSTALL_LOC" 40 | chmod +rx "$INSTALL_LOC" 41 | echo "Goss $GOSS_VER has been installed to $INSTALL_LOC" 42 | echo "goss --version" 43 | "$INSTALL_LOC" --version 44 | 45 | dgoss_url="https://raw.githubusercontent.com/goss-org/goss/$DGOSS_VER/extras/dgoss/dgoss" 46 | echo "Downloading $dgoss_url" 47 | curl -L "$dgoss_url" -o "$DGOSS_INSTALL_LOC" 48 | chmod +rx "$DGOSS_INSTALL_LOC" 49 | echo "dgoss $DGOSS_VER has been installed to $DGOSS_INSTALL_LOC" 50 | } 51 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_alpine3: -------------------------------------------------------------------------------- 1 | FROM alpine:3.19 2 | LABEL org.opencontainers.image.authors="Ahmed" 3 | 4 | # install apache2 and remove un-needed services 5 | RUN apk update && \ 6 | apk add --no-cache openrc apache2=2.4.59-r0 bash ca-certificates tinyproxy && \ 7 | sed -i 's/Listen 80/Listen 0.0.0.0:80/g' /etc/apache2/httpd.conf && \ 8 | rc-update add apache2 && \ 9 | rc-update add tinyproxy && \ 10 | rm -rf /etc/init.d/networking /etc/init.d/hwdrivers /var/cache/apk/* /tmp/* 11 | RUN mkfifo /pipe 12 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_alpine3.md5: -------------------------------------------------------------------------------- 1 | 3c4e7fbf89cd2edfeae94728e247213d Dockerfile_alpine3 2 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_arch: -------------------------------------------------------------------------------- 1 | FROM archlinux:base 2 | MAINTAINER @siddharthist 3 | 4 | RUN ln -s /does_not_exist /foo && \ 5 | chmod 700 ~root 6 | RUN mkfifo /pipe 7 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_arch.md5: -------------------------------------------------------------------------------- 1 | 8fc3ce0c000f89ab09488cccb3ba8e66 Dockerfile_arch 2 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_centos7: -------------------------------------------------------------------------------- 1 | FROM centos:7.2.1511 2 | LABEL org.opencontainers.image.authors="Ahmed" 3 | 4 | ENV container docker 5 | RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \ 6 | rm -f /lib/systemd/system/multi-user.target.wants/*;\ 7 | rm -f /etc/systemd/system/*.wants/*;\ 8 | rm -f /lib/systemd/system/local-fs.target.wants/*; \ 9 | rm -f /lib/systemd/system/sockets.target.wants/*udev*; \ 10 | rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \ 11 | rm -f /lib/systemd/system/basic.target.wants/*;\ 12 | rm -f /lib/systemd/system/anaconda.target.wants/*; 13 | VOLUME [ "/sys/fs/cgroup" ] 14 | CMD ["/usr/sbin/init"] 15 | 16 | RUN yum -y --disablerepo='*' --enablerepo=base,extras install httpd epel-release && yum clean all 17 | RUN yum -y --disablerepo='*' --enablerepo=base,epel install tinyproxy && yum clean all 18 | 19 | RUN systemctl enable httpd 20 | RUN systemctl enable tinyproxy 21 | RUN chmod 700 ~root 22 | RUN mkfifo /pipe 23 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_centos7.md5: -------------------------------------------------------------------------------- 1 | 148b069bc0a023068cbcdfe8b24fe036 Dockerfile_centos7 2 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_rockylinux9: -------------------------------------------------------------------------------- 1 | FROM rockylinux:9 2 | 3 | ENV container docker 4 | 5 | RUN dnf install -y systemd httpd diffutils 'dnf-command(config-manager)' && \ 6 | dnf config-manager --set-enabled crb && \ 7 | dnf install -y epel-release && \ 8 | dnf install -y tinyproxy && \ 9 | dnf remove -y 'dnf-command(config-manager)' epel-release 10 | 11 | RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \ 12 | rm -f /lib/systemd/system/multi-user.target.wants/*;\ 13 | rm -f /etc/systemd/system/*.wants/*;\ 14 | rm -f /lib/systemd/system/local-fs.target.wants/*; \ 15 | rm -f /lib/systemd/system/sockets.target.wants/*udev*; \ 16 | rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \ 17 | rm -f /lib/systemd/system/basic.target.wants/*;\ 18 | rm -f /lib/systemd/system/anaconda.target.wants/*; 19 | 20 | CMD ["/usr/sbin/init"] 21 | 22 | RUN systemctl enable httpd 23 | RUN systemctl enable tinyproxy 24 | RUN chmod 700 ~root 25 | RUN mkfifo /pipe 26 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_trusty: -------------------------------------------------------------------------------- 1 | FROM ubuntu-upstart:trusty 2 | LABEL org.opencontainers.image.authors="Ahmed" 3 | 4 | RUN apt-get update && \ 5 | apt-get install -y apache2=2.4.7-1ubuntu4.22 tinyproxy && \ 6 | apt-get remove -y vim-tiny && \ 7 | apt-get clean 8 | 9 | RUN sed -i '/reload|force-reload)/i status) pidof tinyproxy > /dev/null && echo "tinyproxy is running";;' /etc/init.d/tinyproxy 10 | RUN sed -i '/start)/a\ touch /var/log/tinyproxy/tinyproxy.log /var/run/tinyproxy/tinyproxy.pid' /etc/init.d/tinyproxy 11 | 12 | RUN update-rc.d apache2 defaults 13 | RUN update-rc.d tinyproxy defaults 14 | RUN mkfifo /pipe 15 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_trusty.md5: -------------------------------------------------------------------------------- 1 | 9db0e607ec52f1fd1290785721733180 Dockerfile_trusty 2 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_wheezy: -------------------------------------------------------------------------------- 1 | FROM debian:wheezy 2 | LABEL org.opencontainers.image.authors="Ahmed" 3 | 4 | RUN echo 'deb http://archive.debian.org/debian wheezy main' > /etc/apt/sources.list 5 | RUN echo 'deb http://archive.debian.org/debian-security wheezy/updates main' >> /etc/apt/sources.list 6 | 7 | RUN apt-get -o Acquire::Check-Valid-Until=false update && apt-get install --yes --force-yes \ 8 | apache2 apache2-doc apache2-utils chkconfig vim-tiny ca-certificates tinyproxy && \ 9 | apt-get remove -y vim-tiny && apt-get clean 10 | 11 | RUN chkconfig apache2 on 12 | RUN chkconfig tinyproxy on 13 | RUN mkfifo /pipe 14 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_wheezy.md5: -------------------------------------------------------------------------------- 1 | 3775dbcd23497095da8f5b7ddb62a540 Dockerfile_wheezy 2 | -------------------------------------------------------------------------------- /integration-tests/Find-AvailablePort.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | # Start port scanning at 3 | [int] $startAt = 1025, 4 | # End port scanning at 5 | [int] $endAt = 65535 6 | ) 7 | for ($port=$startAt; $port -lt $endAt; $port++) { 8 | $listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Any, $port) 9 | try { 10 | $listener.Start() 11 | write-output "$port" 12 | break 13 | } 14 | catch { 15 | write-host "$port busy" 16 | } 17 | finally { 18 | $listener.Stop() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /integration-tests/goss/alpine3/goss-aa-expected.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | apache2: 3 | installed: true 4 | versions: 5 | - 2.4.59-r0 6 | service: 7 | apache2: 8 | enabled: true 9 | running: true 10 | -------------------------------------------------------------------------------- /integration-tests/goss/alpine3/goss-expected-q.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | contents: [] 5 | /tmp/goss/foobar: 6 | exists: false 7 | contents: [] 8 | package: 9 | apache2: 10 | installed: true 11 | foobar: 12 | installed: false 13 | vim-tiny: 14 | installed: false 15 | addr: 16 | tcp://httpbin:22: 17 | reachable: false 18 | timeout: 1000 19 | tcp://httpbin:80: 20 | reachable: true 21 | timeout: 1000 22 | udp://8.8.8.8:53: 23 | reachable: true 24 | timeout: 1000 25 | port: 26 | tcp:80: 27 | listening: true 28 | tcp:9999: 29 | listening: false 30 | tcp6:80: 31 | listening: false 32 | service: 33 | apache2: 34 | enabled: true 35 | running: true 36 | foobar: 37 | enabled: false 38 | running: false 39 | user: 40 | foobar: 41 | exists: false 42 | www-data: 43 | exists: false 44 | group: 45 | foobar: 46 | exists: false 47 | www-data: 48 | exists: true 49 | command: 50 | echo 'hi': 51 | exit-status: 0 52 | stdout: "" 53 | stderr: "" 54 | timeout: 10000 55 | foobar: 56 | exit-status: 127 57 | stdout: "" 58 | stderr: "" 59 | timeout: 10000 60 | dns: 61 | CAA:dnstest.io: 62 | resolvable: true 63 | timeout: 1000 64 | server: 8.8.8.8 65 | CNAME:c.dnstest.io: 66 | resolvable: true 67 | timeout: 1000 68 | server: 8.8.8.8 69 | MX:dnstest.io: 70 | resolvable: true 71 | timeout: 1000 72 | server: 8.8.8.8 73 | NS:dnstest.io: 74 | resolvable: true 75 | timeout: 1000 76 | server: 8.8.8.8 77 | PTR:54.243.154.1: 78 | resolvable: true 79 | timeout: 1000 80 | server: 8.8.8.8 81 | SRV:_https._tcp.dnstest.io: 82 | resolvable: true 83 | timeout: 1000 84 | server: 8.8.8.8 85 | TXT:txt._test.dnstest.io: 86 | resolvable: true 87 | timeout: 1000 88 | server: 8.8.8.8 89 | ip6.dnstest.io: 90 | resolvable: true 91 | timeout: 1000 92 | server: 8.8.8.8 93 | localhost: 94 | resolvable: true 95 | timeout: 1000 96 | process: 97 | apache2: 98 | running: false 99 | foobar: 100 | running: false 101 | kernel-param: 102 | kernel.ostype: 103 | value: Linux 104 | mount: 105 | /dev: 106 | exists: true 107 | timeout: 1000 108 | http: 109 | http://google.com: 110 | status: 301 111 | allow-insecure: false 112 | no-follow-redirects: true 113 | timeout: 5000 114 | body: [] 115 | https://www.apple.com: 116 | status: 200 117 | allow-insecure: false 118 | no-follow-redirects: false 119 | timeout: 5000 120 | body: [] 121 | proxy: http://127.0.0.1:8888 122 | https://www.google.com: 123 | status: 200 124 | allow-insecure: false 125 | no-follow-redirects: false 126 | timeout: 5000 127 | body: [] 128 | -------------------------------------------------------------------------------- /integration-tests/goss/alpine3/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | service: 3 | autofs: 4 | enabled: false 5 | running: false 6 | user: 7 | apache: 8 | exists: true 9 | uid: 100 10 | gid: 101 11 | groups: 12 | - apache 13 | home: "/var/www" 14 | group: 15 | apache: 16 | exists: true 17 | gid: 101 18 | process: 19 | httpd: 20 | running: true 21 | port: 22 | tcp:80: 23 | listening: true 24 | ip: 25 | - "0.0.0.0" 26 | addr: 27 | tcp://127.0.0.1:80: 28 | reachable: true 29 | timeout: 500 30 | local-address: 127.0.0.1 31 | gossfile: 32 | "../goss-s*.yaml": {} 33 | bypath: 34 | file: "../goss-dummy.yaml" 35 | -------------------------------------------------------------------------------- /integration-tests/goss/arch/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | package: 3 | curl: 4 | installed: true 5 | pacman: 6 | installed: true 7 | foobar: 8 | installed: false 9 | user: 10 | root: 11 | exists: true 12 | uid: 0 13 | gid: 0 14 | home: "/root" 15 | file: 16 | "/foo": 17 | exists: true 18 | filetype: symlink 19 | gossfile: 20 | "../goss-shared.yaml": {} 21 | bypath: 22 | file: "../goss-dummy.yaml" 23 | -------------------------------------------------------------------------------- /integration-tests/goss/centos7/goss-aa-expected.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | httpd: 3 | installed: true 4 | versions: 5 | - 2.4.6-95.el7.centos 6 | port: 7 | tcp:80: 8 | listening: true 9 | ip: 10 | - 0.0.0.0 11 | service: 12 | httpd: 13 | enabled: true 14 | running: true 15 | process: 16 | httpd: 17 | running: true 18 | -------------------------------------------------------------------------------- /integration-tests/goss/centos7/goss-expected-q.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | contents: [] 5 | /tmp/goss/foobar: 6 | exists: false 7 | contents: [] 8 | package: 9 | foobar: 10 | installed: false 11 | httpd: 12 | installed: true 13 | vim-tiny: 14 | installed: false 15 | addr: 16 | tcp://httpbin:22: 17 | reachable: false 18 | timeout: 1000 19 | tcp://httpbin:80: 20 | reachable: true 21 | timeout: 1000 22 | udp://8.8.8.8:53: 23 | reachable: true 24 | timeout: 1000 25 | port: 26 | tcp:80: 27 | listening: true 28 | tcp:9999: 29 | listening: false 30 | tcp6:80: 31 | listening: false 32 | service: 33 | foobar: 34 | enabled: false 35 | running: false 36 | httpd: 37 | enabled: true 38 | running: true 39 | user: 40 | apache: 41 | exists: true 42 | foobar: 43 | exists: false 44 | group: 45 | apache: 46 | exists: true 47 | foobar: 48 | exists: false 49 | command: 50 | echo 'hi': 51 | exit-status: 0 52 | stdout: "" 53 | stderr: "" 54 | timeout: 10000 55 | foobar: 56 | exit-status: 127 57 | stdout: "" 58 | stderr: "" 59 | timeout: 10000 60 | dns: 61 | CAA:dnstest.io: 62 | resolvable: true 63 | timeout: 1000 64 | server: 8.8.8.8 65 | CNAME:c.dnstest.io: 66 | resolvable: true 67 | timeout: 1000 68 | server: 8.8.8.8 69 | MX:dnstest.io: 70 | resolvable: true 71 | timeout: 1000 72 | server: 8.8.8.8 73 | NS:dnstest.io: 74 | resolvable: true 75 | timeout: 1000 76 | server: 8.8.8.8 77 | PTR:54.243.154.1: 78 | resolvable: true 79 | timeout: 1000 80 | server: 8.8.8.8 81 | SRV:_https._tcp.dnstest.io: 82 | resolvable: true 83 | timeout: 1000 84 | server: 8.8.8.8 85 | TXT:txt._test.dnstest.io: 86 | resolvable: true 87 | timeout: 1000 88 | server: 8.8.8.8 89 | ip6.dnstest.io: 90 | resolvable: true 91 | timeout: 1000 92 | server: 8.8.8.8 93 | localhost: 94 | resolvable: true 95 | timeout: 1000 96 | process: 97 | foobar: 98 | running: false 99 | httpd: 100 | running: true 101 | kernel-param: 102 | kernel.ostype: 103 | value: Linux 104 | mount: 105 | /dev: 106 | exists: true 107 | timeout: 1000 108 | http: 109 | http://google.com: 110 | status: 301 111 | allow-insecure: false 112 | no-follow-redirects: true 113 | timeout: 5000 114 | body: [] 115 | https://www.apple.com: 116 | status: 200 117 | allow-insecure: false 118 | no-follow-redirects: false 119 | timeout: 5000 120 | body: [] 121 | proxy: http://127.0.0.1:8888 122 | https://www.google.com: 123 | status: 200 124 | allow-insecure: false 125 | no-follow-redirects: false 126 | timeout: 5000 127 | body: [] 128 | -------------------------------------------------------------------------------- /integration-tests/goss/centos7/goss.yaml: -------------------------------------------------------------------------------- 1 | service: 2 | autofs: 3 | enabled: false 4 | running: false 5 | user: 6 | apache: 7 | exists: true 8 | uid: 48 9 | gid: 48 10 | groups: 11 | - apache 12 | home: "/usr/share/httpd" 13 | group: 14 | apache: 15 | exists: true 16 | gid: 48 17 | process: 18 | httpd: 19 | running: true 20 | port: 21 | tcp:80: 22 | listening: true 23 | ip: 24 | - '0.0.0.0' 25 | addr: 26 | tcp://127.0.0.1:80: 27 | reachable: true 28 | timeout: 500 29 | local-address: 127.0.0.1 30 | gossfile: 31 | "../goss-s*.yaml": {} 32 | bypath: 33 | file: "../goss-dummy.yaml" 34 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/commands/add.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # TODO: coverage for the add {test} permutations 3 | command: 4 | "add addr 127.0.0.1": 5 | exit-status: 0 6 | exec: release/goss-darwin-amd64 --use-alpha=1 add addr 127.0.0.1 7 | stdout: 8 | - "timeout: 500" 9 | stderr: [] 10 | timeout: 5000 11 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/commands/autoadd.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | command: 3 | "autoadd /Users/travis": 4 | exit-status: 0 5 | exec: "release/goss-darwin-amd64 --use-alpha=1 autoadd /Users/travis" 6 | stdout: 7 | - 'file:' 8 | - ' exists: true' 9 | - ' filetype: directory' 10 | stderr: [] 11 | timeout: 5000 12 | 13 | # needs implementation 14 | skip: true 15 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/commands/help.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | command: 3 | help: 4 | exit-status: 0 5 | exec: "release/goss-darwin-amd64 help" 6 | stdout: 7 | - alpha 8 | stderr: [] 9 | timeout: 5000 10 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/commands/validate-input.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | file: 3 | non-existent.txt: 4 | exists: false 5 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/commands/validate.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # TODO: coverage for the add {test} permutations 3 | command: 4 | "validate": 5 | exit-status: 0 6 | exec: "release/goss-darwin-amd64 --use-alpha=1 -g integration-tests/goss/darwin/commands/validate-input.yaml validate" 7 | stdout: 8 | - 'Count: 1' 9 | - 'Failed: 0' 10 | - 'Skipped: 0' 11 | stderr: [] 12 | timeout: 5000 13 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/addr.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | addr: 3 | tcp://google.com:443: 4 | reachable: true 5 | timeout: 1000 6 | 7 | # TODO: needs implementation (or figure out a likely listening port on macOS/travis) 8 | # tcp://127.0.0.1:135: 9 | # reachable: true 10 | # timeout: 1000 11 | # local-address: true 12 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/command.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | command: 3 | hello world: 4 | exit-status: 0 5 | exec: "echo hello world" 6 | stdout: 7 | - hello world 8 | stderr: [] 9 | timeout: 10000 10 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/dns.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | dns: 3 | localhost: 4 | resolvable: true 5 | addrs: 6 | - "127.0.0.1" 7 | - ::1 8 | timeout: 500 9 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/file.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | file: 3 | integration-tests/goss/testdata/static-file.txt: 4 | exists: true 5 | mode: "0644" 6 | # user: "" # TODO: not working on Darwin 7 | # group: "" # TODO: not working on Darwin 8 | size: 20 9 | filetype: file 10 | md5: 9dcea4037b1439a2a96e4d206eda63a4 11 | sha256: e73d885411a52a0d29142e830e104e0cc9252fbb1dc3c92a430ef7c369f089ef 12 | contents: 13 | - "nothing to see here" 14 | - "/nothing.*here/" 15 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/gossfile.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # paths are relative to the goss file that includes the gossfile directive. 3 | gossfile: 4 | addr.goss.yaml: {} 5 | command.goss.yaml: {} 6 | dns.goss.yaml: {} 7 | file.goss.yaml: {} 8 | # don't use gossfile; avoid self-referencing 9 | # gossfile.goss.yaml: {} 10 | group.goss.yaml: {} 11 | http.goss.yaml: {} 12 | interface.goss.yaml: {} 13 | # kernel-param.na-goss.yaml: {} 14 | mount.goss.yaml: {} 15 | package.goss.yaml: {} 16 | port.goss.yaml: {} 17 | process.goss.yaml: {} 18 | service.goss.yaml: {} 19 | user.goss.yaml: {} 20 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/group.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | _developers: 4 | exists: true 5 | gid: 0 6 | 7 | # TODO: needs implementation 8 | skip: true 9 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/http.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | http: 3 | https://google.com: 4 | status: 200 5 | allow-insecure: false 6 | no-follow-redirects: false 7 | timeout: 10000 8 | request-headers: 9 | - "Content-Type: text/html" 10 | headers: 11 | - "Content-Type: text/html" 12 | body: 13 | - "google" 14 | username: "" 15 | password: "" 16 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/interface.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | interface: 3 | eth0: 4 | exists: true 5 | addrs: 6 | - '127.0.0.1' 7 | mtu: 1500 8 | 9 | # TODO: needs implementation 10 | skip: true 11 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/kernel-param.na-goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kernel-param: 3 | notapplicable.on-darwin: 4 | value: foobar 5 | 6 | # TODO: need implementation or signal no support on Darwin 7 | skip: true 8 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/mount.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | mount: 3 | '/': 4 | exists: true 5 | filesystem: hdfs 6 | 7 | opts: [] 8 | source: '' 9 | usage: 10 | lt: 95 11 | 12 | # TODO: needs implementation 13 | skip: true 14 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/package.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | package: 3 | golang: 4 | # required attributes 5 | installed: true 6 | # optional attributes 7 | versions: 8 | - 1.14.1 9 | 10 | # needs implementation 11 | # needs discussion + design 12 | # support question for: 13 | # * homebrew 14 | # * macports 15 | skip: true 16 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/port.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | port: 3 | tcp:135: 4 | listening: true 5 | ip: 6 | - 0.0.0.0 7 | 8 | # TODO: needs implementation 9 | skip: true 10 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/process.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | process: 3 | bash: 4 | running: true 5 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/service.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | service: 3 | launchd: 4 | enabled: true 5 | running: true 6 | 7 | # TODO: needs implementation 8 | skip: true 9 | -------------------------------------------------------------------------------- /integration-tests/goss/darwin/tests/user.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | user: 3 | travis: 4 | exists: true 5 | uid: 65534 6 | gid: 65534 7 | groups: 8 | - _developers 9 | home: /Users/travis 10 | shell: /sbin/nologin 11 | 12 | # TODO: needs implementation 13 | skip: true 14 | -------------------------------------------------------------------------------- /integration-tests/goss/goss-dummy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | command: 3 | includetest: 4 | exec: echo 'hi' 5 | exit-status: 0 6 | stdout: 7 | - hi 8 | stderr: [] 9 | -------------------------------------------------------------------------------- /integration-tests/goss/goss-serve.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | command: 3 | hello world: 4 | exec: echo 'hi' 5 | exit-status: 0 6 | stdout: 7 | - hi 8 | stderr: [] 9 | -------------------------------------------------------------------------------- /integration-tests/goss/goss-service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | service: 3 | foobar: 4 | enabled: false 5 | running: false 6 | {{ if .Env.OS | regexMatch "centos[7]|rockylinux[9]" }} 7 | httpd: 8 | {{else}} 9 | apache2: 10 | {{end}} 11 | {{ if .Env.OS | regexMatch "trusty" }} 12 | enabled: false 13 | {{else}} 14 | enabled: true 15 | {{end}} 16 | running: true 17 | skippable: 18 | enabled: true 19 | running: true 20 | skip: true 21 | -------------------------------------------------------------------------------- /integration-tests/goss/goss-wait.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | addr: 3 | tcp://localhost:80: 4 | reachable: true 5 | timeout: 500 6 | tcp://localhost:8888: 7 | reachable: true 8 | timeout: 500 9 | -------------------------------------------------------------------------------- /integration-tests/goss/hellogoss.txt: -------------------------------------------------------------------------------- 1 | Goss Rocks!! 2 | -------------------------------------------------------------------------------- /integration-tests/goss/rockylinux9/goss-aa-expected.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | httpd: 3 | installed: true 4 | versions: 5 | - 2.4.57-11.el9_4.1 6 | port: 7 | tcp:80: 8 | listening: true 9 | ip: 10 | - 0.0.0.0 11 | service: 12 | httpd: 13 | enabled: true 14 | running: true 15 | process: 16 | httpd: 17 | running: true 18 | -------------------------------------------------------------------------------- /integration-tests/goss/rockylinux9/goss-expected-q.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | contents: [] 5 | /tmp/goss/foobar: 6 | exists: false 7 | contents: [] 8 | package: 9 | foobar: 10 | installed: false 11 | httpd: 12 | installed: true 13 | vim-tiny: 14 | installed: false 15 | addr: 16 | tcp://httpbin:22: 17 | reachable: false 18 | timeout: 1000 19 | tcp://httpbin:80: 20 | reachable: true 21 | timeout: 1000 22 | udp://8.8.8.8:53: 23 | reachable: true 24 | timeout: 1000 25 | port: 26 | tcp:80: 27 | listening: true 28 | tcp:9999: 29 | listening: false 30 | tcp6:80: 31 | listening: false 32 | service: 33 | foobar: 34 | enabled: false 35 | running: false 36 | httpd: 37 | enabled: true 38 | running: true 39 | user: 40 | apache: 41 | exists: true 42 | foobar: 43 | exists: false 44 | group: 45 | apache: 46 | exists: true 47 | foobar: 48 | exists: false 49 | command: 50 | echo 'hi': 51 | exit-status: 0 52 | stdout: "" 53 | stderr: "" 54 | timeout: 10000 55 | foobar: 56 | exit-status: 127 57 | stdout: "" 58 | stderr: "" 59 | timeout: 10000 60 | dns: 61 | CAA:dnstest.io: 62 | resolvable: true 63 | timeout: 1000 64 | server: 8.8.8.8 65 | CNAME:c.dnstest.io: 66 | resolvable: true 67 | timeout: 1000 68 | server: 8.8.8.8 69 | MX:dnstest.io: 70 | resolvable: true 71 | timeout: 1000 72 | server: 8.8.8.8 73 | NS:dnstest.io: 74 | resolvable: true 75 | timeout: 1000 76 | server: 8.8.8.8 77 | PTR:54.243.154.1: 78 | resolvable: true 79 | timeout: 1000 80 | server: 8.8.8.8 81 | SRV:_https._tcp.dnstest.io: 82 | resolvable: true 83 | timeout: 1000 84 | server: 8.8.8.8 85 | TXT:txt._test.dnstest.io: 86 | resolvable: true 87 | timeout: 1000 88 | server: 8.8.8.8 89 | ip6.dnstest.io: 90 | resolvable: true 91 | timeout: 1000 92 | server: 8.8.8.8 93 | localhost: 94 | resolvable: true 95 | timeout: 1000 96 | process: 97 | foobar: 98 | running: false 99 | httpd: 100 | running: true 101 | kernel-param: 102 | kernel.ostype: 103 | value: Linux 104 | mount: 105 | /dev: 106 | exists: true 107 | timeout: 1000 108 | http: 109 | http://google.com: 110 | status: 301 111 | allow-insecure: false 112 | no-follow-redirects: true 113 | timeout: 5000 114 | body: [] 115 | https://www.apple.com: 116 | status: 200 117 | allow-insecure: false 118 | no-follow-redirects: false 119 | timeout: 5000 120 | body: [] 121 | proxy: http://127.0.0.1:8888 122 | https://www.google.com: 123 | status: 200 124 | allow-insecure: false 125 | no-follow-redirects: false 126 | timeout: 5000 127 | body: [] 128 | -------------------------------------------------------------------------------- /integration-tests/goss/rockylinux9/goss.yaml: -------------------------------------------------------------------------------- 1 | service: 2 | autofs: 3 | enabled: false 4 | running: false 5 | user: 6 | apache: 7 | exists: true 8 | uid: 48 9 | gid: 48 10 | groups: 11 | - apache 12 | home: "/usr/share/httpd" 13 | group: 14 | apache: 15 | exists: true 16 | gid: 48 17 | process: 18 | httpd: 19 | running: true 20 | port: 21 | tcp:80: 22 | listening: true 23 | ip: 24 | - '0.0.0.0' 25 | addr: 26 | tcp://127.0.0.1:80: 27 | reachable: true 28 | timeout: 500 29 | local-address: 127.0.0.1 30 | gossfile: 31 | "../goss-s*.yaml": {} 32 | bypath: 33 | file: "../goss-dummy.yaml" 34 | -------------------------------------------------------------------------------- /integration-tests/goss/testdata/static-file.txt: -------------------------------------------------------------------------------- 1 | nothing to see here 2 | -------------------------------------------------------------------------------- /integration-tests/goss/trusty/goss-aa-expected.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | apache2: 3 | installed: true 4 | versions: 5 | - 2.4.7-1ubuntu4.22 6 | port: 7 | tcp:80: 8 | listening: true 9 | ip: 10 | - 0.0.0.0 11 | service: 12 | apache2: 13 | enabled: true 14 | running: true 15 | process: 16 | apache2: 17 | running: true 18 | -------------------------------------------------------------------------------- /integration-tests/goss/trusty/goss-expected-q.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | contents: [] 5 | /tmp/goss/foobar: 6 | exists: false 7 | contents: [] 8 | package: 9 | apache2: 10 | installed: true 11 | foobar: 12 | installed: false 13 | vim-tiny: 14 | installed: false 15 | addr: 16 | tcp://httpbin:22: 17 | reachable: false 18 | timeout: 1000 19 | tcp://httpbin:80: 20 | reachable: true 21 | timeout: 1000 22 | udp://8.8.8.8:53: 23 | reachable: true 24 | timeout: 1000 25 | port: 26 | tcp:80: 27 | listening: true 28 | tcp:9999: 29 | listening: false 30 | tcp6:80: 31 | listening: false 32 | service: 33 | apache2: 34 | enabled: true 35 | running: true 36 | foobar: 37 | enabled: false 38 | running: false 39 | user: 40 | foobar: 41 | exists: false 42 | www-data: 43 | exists: true 44 | group: 45 | foobar: 46 | exists: false 47 | www-data: 48 | exists: true 49 | command: 50 | echo 'hi': 51 | exit-status: 0 52 | stdout: "" 53 | stderr: "" 54 | timeout: 10000 55 | foobar: 56 | exit-status: 127 57 | stdout: "" 58 | stderr: "" 59 | timeout: 10000 60 | dns: 61 | CAA:dnstest.io: 62 | resolvable: true 63 | timeout: 1000 64 | server: 8.8.8.8 65 | CNAME:c.dnstest.io: 66 | resolvable: true 67 | timeout: 1000 68 | server: 8.8.8.8 69 | MX:dnstest.io: 70 | resolvable: true 71 | timeout: 1000 72 | server: 8.8.8.8 73 | NS:dnstest.io: 74 | resolvable: true 75 | timeout: 1000 76 | server: 8.8.8.8 77 | PTR:54.243.154.1: 78 | resolvable: true 79 | timeout: 1000 80 | server: 8.8.8.8 81 | SRV:_https._tcp.dnstest.io: 82 | resolvable: true 83 | timeout: 1000 84 | server: 8.8.8.8 85 | TXT:txt._test.dnstest.io: 86 | resolvable: true 87 | timeout: 1000 88 | server: 8.8.8.8 89 | ip6.dnstest.io: 90 | resolvable: true 91 | timeout: 1000 92 | server: 8.8.8.8 93 | localhost: 94 | resolvable: true 95 | timeout: 1000 96 | process: 97 | apache2: 98 | running: true 99 | foobar: 100 | running: false 101 | kernel-param: 102 | kernel.ostype: 103 | value: Linux 104 | mount: 105 | /dev: 106 | exists: true 107 | timeout: 1000 108 | http: 109 | http://google.com: 110 | status: 301 111 | allow-insecure: false 112 | no-follow-redirects: true 113 | timeout: 5000 114 | body: [] 115 | https://www.apple.com: 116 | status: 200 117 | allow-insecure: false 118 | no-follow-redirects: false 119 | timeout: 5000 120 | body: [] 121 | proxy: http://127.0.0.1:8888 122 | https://www.google.com: 123 | status: 200 124 | allow-insecure: false 125 | no-follow-redirects: false 126 | timeout: 5000 127 | body: [] 128 | -------------------------------------------------------------------------------- /integration-tests/goss/trusty/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | service: 3 | tinyproxy: 4 | enabled: true 5 | running: true 6 | user: 7 | www-data: 8 | exists: true 9 | uid: 33 10 | gid: 33 11 | groups: 12 | - www-data 13 | home: "/var/www" 14 | group: 15 | www-data: 16 | exists: true 17 | gid: 33 18 | process: 19 | apache2: 20 | running: true 21 | port: 22 | tcp:80: 23 | listening: true 24 | ip: 25 | - 0.0.0.0 26 | addr: 27 | tcp://127.0.0.1:80: 28 | reachable: true 29 | timeout: 500 30 | local-address: 127.0.0.1 31 | gossfile: 32 | "../goss-s*.yaml": {} 33 | bypath: 34 | file: "../goss-dummy.yaml" 35 | -------------------------------------------------------------------------------- /integration-tests/goss/vars.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | alpine3: 3 | proxy: http://127.0.0.1:8888 4 | packages: 5 | apache2: "2.4.59-r0" 6 | services: 7 | apache2: [sysinit] 8 | arch: 9 | packages: 10 | centos7: 11 | proxy: http://127.0.0.1:8888 12 | packages: 13 | httpd: "2.4.6-95.el7.centos" 14 | services: 15 | httpd: [] 16 | rockylinux9: 17 | proxy: http://127.0.0.1:8888 18 | packages: 19 | httpd: "2.4.57-11.el9_4.1" 20 | services: 21 | httpd: [] 22 | trusty: 23 | proxy: http://127.0.0.1:8888 24 | packages: 25 | apache2: "2.4.7-1ubuntu4.22" 26 | services: 27 | apache2: ["3"] 28 | wheezy: 29 | proxy: http://127.0.0.1:8888 30 | packages: 31 | apache2: "2.2.22-13+deb7u13" 32 | services: 33 | apache2: ["2", "3", "5", "4"] 34 | 35 | overwrite: foo 36 | -------------------------------------------------------------------------------- /integration-tests/goss/wheezy/goss-aa-expected.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | apache2: 3 | installed: true 4 | versions: 5 | - 2.2.22-13+deb7u13 6 | port: 7 | tcp:80: 8 | listening: true 9 | ip: 10 | - 0.0.0.0 11 | service: 12 | apache2: 13 | enabled: true 14 | running: true 15 | process: 16 | apache2: 17 | running: true 18 | -------------------------------------------------------------------------------- /integration-tests/goss/wheezy/goss-expected-q.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | contents: [] 5 | /tmp/goss/foobar: 6 | exists: false 7 | contents: [] 8 | package: 9 | apache2: 10 | installed: true 11 | foobar: 12 | installed: false 13 | vim-tiny: 14 | installed: false 15 | addr: 16 | tcp://httpbin:22: 17 | reachable: false 18 | timeout: 1000 19 | tcp://httpbin:80: 20 | reachable: true 21 | timeout: 1000 22 | udp://8.8.8.8:53: 23 | reachable: true 24 | timeout: 1000 25 | port: 26 | tcp:80: 27 | listening: true 28 | tcp:9999: 29 | listening: false 30 | tcp6:80: 31 | listening: false 32 | service: 33 | apache2: 34 | enabled: true 35 | running: true 36 | foobar: 37 | enabled: false 38 | running: false 39 | user: 40 | foobar: 41 | exists: false 42 | www-data: 43 | exists: true 44 | group: 45 | foobar: 46 | exists: false 47 | www-data: 48 | exists: true 49 | command: 50 | echo 'hi': 51 | exit-status: 0 52 | stdout: "" 53 | stderr: "" 54 | timeout: 10000 55 | foobar: 56 | exit-status: 127 57 | stdout: "" 58 | stderr: "" 59 | timeout: 10000 60 | dns: 61 | CAA:dnstest.io: 62 | resolvable: true 63 | timeout: 1000 64 | server: 8.8.8.8 65 | CNAME:c.dnstest.io: 66 | resolvable: true 67 | timeout: 1000 68 | server: 8.8.8.8 69 | MX:dnstest.io: 70 | resolvable: true 71 | timeout: 1000 72 | server: 8.8.8.8 73 | NS:dnstest.io: 74 | resolvable: true 75 | timeout: 1000 76 | server: 8.8.8.8 77 | PTR:54.243.154.1: 78 | resolvable: true 79 | timeout: 1000 80 | server: 8.8.8.8 81 | SRV:_https._tcp.dnstest.io: 82 | resolvable: true 83 | timeout: 1000 84 | server: 8.8.8.8 85 | TXT:txt._test.dnstest.io: 86 | resolvable: true 87 | timeout: 1000 88 | server: 8.8.8.8 89 | ip6.dnstest.io: 90 | resolvable: true 91 | timeout: 1000 92 | server: 8.8.8.8 93 | localhost: 94 | resolvable: true 95 | timeout: 1000 96 | process: 97 | apache2: 98 | running: true 99 | foobar: 100 | running: false 101 | kernel-param: 102 | kernel.ostype: 103 | value: Linux 104 | mount: 105 | /dev: 106 | exists: true 107 | timeout: 1000 108 | http: 109 | http://google.com: 110 | status: 301 111 | allow-insecure: false 112 | no-follow-redirects: true 113 | timeout: 5000 114 | body: [] 115 | https://www.apple.com: 116 | status: 200 117 | allow-insecure: false 118 | no-follow-redirects: false 119 | timeout: 5000 120 | body: [] 121 | proxy: http://127.0.0.1:8888 122 | https://www.google.com: 123 | status: 200 124 | allow-insecure: false 125 | no-follow-redirects: false 126 | timeout: 5000 127 | body: [] 128 | -------------------------------------------------------------------------------- /integration-tests/goss/wheezy/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | service: 3 | autofs: 4 | enabled: false 5 | running: false 6 | user: 7 | www-data: 8 | exists: true 9 | uid: 33 10 | gid: 33 11 | groups: 12 | - www-data 13 | home: "/var/www" 14 | group: 15 | www-data: 16 | exists: true 17 | gid: 33 18 | process: 19 | apache2: 20 | running: true 21 | port: 22 | tcp:80: 23 | listening: true 24 | ip: 25 | - '0.0.0.0' 26 | addr: 27 | tcp://127.0.0.1:80: 28 | reachable: true 29 | timeout: 500 30 | local-address: 127.0.0.1 31 | gossfile: 32 | "../goss-s*.yaml": {} 33 | bypath: 34 | file: "../goss-dummy.yaml" 35 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/commands/add.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # TODO: coverage for the add {test} permutations 3 | command: 4 | "add addr 127.0.0.1": 5 | exit-status: 0 6 | exec: release\goss-windows-amd64 --use-alpha=1 add addr 127.0.0.1 7 | stdout: 8 | - "timeout: 500" 9 | stderr: [] 10 | timeout: 5000 11 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/commands/autoadd.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | command: 3 | "autoadd Administrator": 4 | exit-status: 0 5 | exec: release\goss-windows-amd64 --use-alpha=1 autoadd Administrator 6 | stdout: 7 | - 'user:' 8 | - ' name: Administrator' 9 | stderr: [] 10 | timeout: 5000 11 | 12 | # needs implementation 13 | skip: true 14 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/commands/help.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | command: 3 | help: 4 | exit-status: 0 5 | exec: release\goss-windows-amd64 help 6 | stdout: 7 | - alpha 8 | stderr: [] 9 | timeout: 5000 10 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/commands/validate-input.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | file: 3 | non-existent.txt: 4 | exists: false 5 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/commands/validate.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # TODO: coverage for the add {test} permutations 3 | command: 4 | "validate": 5 | exit-status: 0 6 | exec: "release\\goss-windows-amd64 --use-alpha=1 -g integration-tests/goss/windows/commands/validate-input.yaml validate" 7 | stdout: 8 | - 'Count: 1' 9 | - 'Failed: 0' 10 | - 'Skipped: 0' 11 | stderr: [] 12 | timeout: 5000 13 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/addr.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | addr: 3 | tcp://google.com:443: 4 | reachable: true 5 | timeout: 1000 6 | 7 | tcp://127.0.0.1:135: 8 | reachable: true 9 | timeout: 1000 10 | local-address: true 11 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/command.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | command: 3 | hello world: 4 | exit-status: 0 5 | exec: "echo hello world" 6 | stdout: 7 | - hello world 8 | stderr: [] 9 | timeout: 10000 10 | wrap a powershell - expect 0 because travis does not restrict anonymous logins: 11 | exec: powershell -noprofile -noninteractive -command (get-itemproperty -path 'HKLM:/SYSTEM/CurrentControlSet/Control/Lsa/').restrictanonymous 12 | exit-status: 0 13 | stdout: 14 | - "0" 15 | stderr: [] 16 | timeout: 10000 17 | wrap a powershell with quotes - expect 0 because travis does not restrict anonymous logins: 18 | exec: powershell -noprofile -noninteractive -command "(get-itemproperty -path 'HKLM:/SYSTEM/CurrentControlSet/Control/Lsa/').restrictanonymous" 19 | exit-status: 0 20 | stdout: 21 | - "0" 22 | stderr: [] 23 | timeout: 10000 24 | powershell with quotes: 25 | exec: powershell /c "(echo '{"b":2, "a":1}' | ConvertFrom-json).a" 26 | exit-status: 0 27 | stdout: 28 | - "1" 29 | stderr: [] 30 | timeout: 10000 31 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/dns.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | dns: 3 | localhost: 4 | resolvable: true 5 | addrs: 6 | - "127.0.0.1" 7 | - ::1 8 | timeout: 500 9 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/file.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | file: 3 | integration-tests\goss\testdata\static-file.txt: 4 | exists: true 5 | # mode: "0000" # not applicable on Windows 6 | # user: "" # not applicable on Windows 7 | # group: "" # not applicable on Windows 8 | size: 21 9 | filetype: file 10 | md5: dc9a07ca9789f866d21d544fe5651954 11 | sha256: aa8b1b4a0d9bf174f5019c8f8a9568858ee2bdf8e0ad16aec54417d49b48df49 12 | contents: 13 | - "nothing to see here" 14 | - "/nothing.*here/" 15 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/gossfile.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # paths are relative to the goss file that includes the gossfile directive. 3 | gossfile: 4 | addr.goss.yaml: {} 5 | command.goss.yaml: {} 6 | dns.goss.yaml: {} 7 | file.goss.yaml: {} 8 | # don't use gossfile; avoid self-referencing 9 | # gossfile.goss.yaml: {} 10 | group.goss.yaml: {} 11 | http.goss.yaml: {} 12 | interface.goss.yaml: {} 13 | # kernel-param.na-goss.yaml: {} 14 | mount.goss.yaml: {} 15 | package.goss.yaml: {} 16 | port.goss.yaml: {} 17 | process.goss.yaml: {} 18 | service.goss.yaml: {} 19 | user.goss.yaml: {} 20 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/group.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | 'Local Users': 4 | exists: true 5 | gid: 0 # not applicable on Windows 6 | skip: true # TODO: implement on Windows 7 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/http.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | http: 3 | https://google.com: 4 | status: 200 5 | allow-insecure: false 6 | no-follow-redirects: false 7 | timeout: 10000 8 | request-headers: 9 | - "Content-Type: text/html" 10 | headers: 11 | - "Content-Type: text/html" 12 | body: 13 | - "google" 14 | username: "" 15 | password: "" 16 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/interface.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | interface: 3 | 'Loopback Pseudo-Interface 1': 4 | exists: false 5 | addrs: 6 | - '127.0.0.1' 7 | mtu: 1500 8 | skip: true 9 | 10 | # https://docs.microsoft.com/en-us/powershell/module/nettcpip/get-netipinterface?view=win10-ps 11 | # Get-NetIPInterface 12 | # https://docs.microsoft.com/en-us/powershell/module/netadapter/get-netadapter?view=win10-ps 13 | # 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 14 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/kernel-param.na-goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Not applicable on Windows 3 | kernel-param: 4 | notapplicable.on-windows: 5 | value: foobar 6 | skip: true 7 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/mount.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | mount: 3 | 'c:': 4 | exists: true 5 | filesystem: ntfs 6 | 7 | opts: [] # not applicable on Windows 8 | source: '' # not applicable on Windows 9 | usage: 10 | lt: 95 11 | 12 | skip: true # needs implementation 13 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/package.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | package: 3 | golang: 4 | # required attributes 5 | installed: true 6 | # optional attributes 7 | versions: 8 | - 1.14.1 9 | 10 | # needs implementation 11 | # needs discussion + design 12 | # support question for: 13 | # * chocolatey https://chocolatey.org 14 | # * scoop https://scoop.sh/ 15 | # * winget-cli https://github.com/microsoft/winget-cli 16 | skip: true 17 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/port.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | port: 3 | tcp:135: 4 | listening: true 5 | ip: 6 | - 0.0.0.0 7 | 8 | # needs implementation 9 | skip: true 10 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/process.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | process: 3 | 'wininit.exe': 4 | running: true 5 | 6 | # note - must use .exe suffix on Windows currently 7 | wininit: 8 | running: false 9 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/service.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | service: 3 | MSDTC: 4 | enabled: true 5 | running: true 6 | 7 | # needs implementation 8 | skip: true 9 | -------------------------------------------------------------------------------- /integration-tests/goss/windows/tests/user.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | user: 3 | Administrator: 4 | exists: true 5 | uid: 65534 # not applicable on Windows 6 | gid: 65534 # not applicable on Windows 7 | groups: 8 | - nfsnobody 9 | home: /var/lib/nfs 10 | shell: /sbin/nologin 11 | 12 | # needs implementation 13 | skip: true 14 | -------------------------------------------------------------------------------- /integration-tests/run-tests-alpha.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck source=../ci/lib/setup.sh 3 | source "$(dirname "${BASH_SOURCE[0]}")/../ci/lib/setup.sh" || exit 67 4 | 5 | platform_spec="${1:?"Must supply name of release binary to build e.g. goss-linux-amd64"}" 6 | # Split platform_spec into platform/arch segments 7 | IFS='- ' read -r -a segments <<< "${platform_spec}" 8 | 9 | os="${segments[0]}" 10 | arch="${segments[1]}" 11 | if [[ "${segments[0]}" == "alpha" ]]; then 12 | os="${segments[1]}" 13 | arch="${segments[2]}" 14 | fi 15 | 16 | repo_root="$(git rev-parse --show-toplevel)" 17 | export GOSS_BINARY="${repo_root}/release/goss-${platform_spec}" 18 | log_info "Using: '${GOSS_BINARY}', cwd: '$(pwd)', os: ${os}" 19 | readarray -t goss_test_files < <(find integration-tests -type f -name "*.goss.yaml" | grep "${os}" | sort | uniq) 20 | 21 | export GOSS_USE_ALPHA=1 22 | for file in "${goss_test_files[@]}"; do 23 | args=( 24 | "-g=${file}" 25 | "validate" 26 | ) 27 | log_action -e "\nTesting \`${GOSS_BINARY} ${args[*]}\` ...\n" 28 | "${GOSS_BINARY}" "${args[@]}" 29 | done 30 | -------------------------------------------------------------------------------- /integration-tests/run-validate-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck source=../ci/lib/setup.sh 3 | source "$(dirname "${BASH_SOURCE[0]}")/../ci/lib/setup.sh" || exit 67 4 | 5 | platform_spec="${1:?"Must supply name of release binary to build e.g. goss-linux-amd64"}" 6 | # Split platform_spec into platform/arch segments 7 | IFS='- ' read -r -a segments <<< "${platform_spec}" 8 | 9 | os="${segments[0]}" 10 | arch="${segments[1]}" 11 | 12 | if [[ "${os}" == "linux" ]]; then 13 | echo "OS is ${os}. This script is not for running tests on the different flavours of linux." 14 | echo "Linux is exercised via the integration-tests/test.sh currently, because linux can be" 15 | echo "verified via docker containers; macOS and Windows cannot." 16 | echo "This script is for macOS and Windows, and runs tests that are expected to pass on" 17 | echo "Travis-CI provided images, running nakedly (no containerisation) on the hosts there." 18 | exit 1 19 | fi 20 | 21 | repo_root="$(git rev-parse --show-toplevel)" 22 | export GOSS_BINARY="${repo_root}/release/goss-${platform_spec}" 23 | log_info "Using: '${GOSS_BINARY}', cwd: '$(pwd)', os: ${os}" 24 | 25 | export GOSS_USE_ALPHA=1 26 | for file in `find integration-tests -type f -name "*.goss.yaml" | grep "${os}" | sort | uniq`; do 27 | args=( 28 | "-g=${file}" 29 | "validate" 30 | ) 31 | log_action "\nTesting \`${GOSS_BINARY} ${args[*]}\` ...\n" 32 | "${GOSS_BINARY}" "${args[@]}" 33 | done 34 | -------------------------------------------------------------------------------- /logs.go: -------------------------------------------------------------------------------- 1 | package goss 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/goss-org/goss/util" 12 | "github.com/hashicorp/logutils" 13 | ) 14 | 15 | func setLogLevel(c *util.Config) error { 16 | filter := &logutils.LevelFilter{ 17 | Levels: []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARN", "ERROR"}, 18 | MinLevel: logutils.LogLevel("INFO"), 19 | Writer: os.Stderr, 20 | } 21 | log.SetFlags(0) // Turn off standard timestamp flags 22 | log.SetOutput(×tampedWriter{filter}) 23 | for _, lvl := range filter.Levels { 24 | cLvl := strings.ToUpper(c.LogLevel) 25 | if string(lvl) == cLvl { 26 | filter.MinLevel = lvl 27 | log.Printf("[DEBUG] Setting log level to %v", cLvl) 28 | return nil 29 | } 30 | } 31 | return fmt.Errorf("Unsupported log level: %s", c.LogLevel) 32 | } 33 | 34 | type timestampedWriter struct { 35 | wrappedWriter io.Writer 36 | } 37 | 38 | func (t *timestampedWriter) Write(b []byte) (int, error) { 39 | timestamp := time.Now().UTC().Format(time.RFC3339) 40 | return fmt.Fprintf(t.wrappedWriter, "%s %s", timestamp, b) 41 | } 42 | -------------------------------------------------------------------------------- /matcher_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package goss 4 | 5 | import ( 6 | "bytes" 7 | "flag" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | "testing" 15 | 16 | "github.com/goss-org/goss/util" 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | var ( 21 | // This will generate the "golden files" prior to running the tests. 22 | // helpful when the output is changed and a user doesn't want to update every single expectation file by hand 23 | update = flag.Bool("update", false, "update the golden files of this test") 24 | ) 25 | 26 | func TestMain(m *testing.M) { 27 | flag.Parse() 28 | os.Exit(m.Run()) 29 | } 30 | 31 | func TestMatchers(t *testing.T) { 32 | files, err := filepath.Glob(filepath.Join("testdata", "out_matching_*")) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | for _, outFile := range files { 38 | outFile := outFile 39 | parts := strings.Split(outFile, ".") 40 | specName := fmt.Sprintf("%s.yaml", strings.TrimPrefix(parts[0], "testdata/out_")) 41 | specFile := filepath.Join("testdata", specName) 42 | outFormat := parts[2] 43 | wantCode, err := strconv.Atoi(parts[1]) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | tn := outFile 48 | t.Run(tn, func(t *testing.T) { 49 | output := &bytes.Buffer{} 50 | 51 | cfg, err := util.NewConfig( 52 | util.WithOutputFormat(outFormat), 53 | util.WithResultWriter(output), 54 | util.WithSpecFile(specFile), 55 | util.WithFormatOptions("sort", "pretty"), 56 | ) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | exitCode, err := Validate(cfg) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | actualOut := output.String() 65 | actualOut = sanitizeOutput(actualOut) 66 | 67 | if *update { 68 | os.WriteFile(outFile, []byte(actualOut), 0644) 69 | } 70 | wantOutB, err := os.ReadFile(outFile) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | wantOut := string(wantOutB) 75 | if actualOut != wantOut { 76 | assert.Equal(t, wantOut, actualOut) 77 | } 78 | if exitCode != wantCode { 79 | assert.Equal(t, wantCode, exitCode) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func sanitizeOutput(s string) string { 86 | // Remove duration time 87 | re := regexp.MustCompile(`\d\.\d\d\ds`) 88 | return re.ReplaceAllString(s, "") 89 | } 90 | -------------------------------------------------------------------------------- /matchers/and.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type AndMatcher struct { 8 | fakeOmegaMatcher 9 | Matchers []GossMatcher 10 | 11 | // state 12 | firstFailedMatcher GossMatcher 13 | } 14 | 15 | func And(ms ...GossMatcher) GossMatcher { 16 | return &AndMatcher{Matchers: ms} 17 | } 18 | 19 | func (m *AndMatcher) Match(actual interface{}) (success bool, err error) { 20 | m.firstFailedMatcher = nil 21 | for _, matcher := range m.Matchers { 22 | success, err := matcher.Match(actual) 23 | if !success || err != nil { 24 | m.firstFailedMatcher = matcher 25 | return false, err 26 | } 27 | } 28 | return true, nil 29 | } 30 | 31 | func (m *AndMatcher) FailureResult(actual interface{}) MatcherResult { 32 | return m.firstFailedMatcher.FailureResult(actual) 33 | } 34 | 35 | func (m *AndMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 36 | return MatcherResult{ 37 | Actual: actual, 38 | Message: "not to satisfy all of these matchers", 39 | Expected: m.Matchers, 40 | } 41 | } 42 | 43 | func (m *AndMatcher) MarshalJSON() ([]byte, error) { 44 | if len(m.Matchers) == 1 { 45 | return json.Marshal(m.Matchers[0]) 46 | } 47 | j := make(map[string]interface{}) 48 | j["and"] = m.Matchers 49 | return json.Marshal(j) 50 | } 51 | -------------------------------------------------------------------------------- /matchers/be_numerically_matcher.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/onsi/gomega/matchers" 8 | ) 9 | 10 | type BeNumericallyMatcher struct { 11 | fakeOmegaMatcher 12 | Comparator string 13 | CompareTo []interface{} 14 | } 15 | 16 | func BeNumerically(comparator string, compareTo ...interface{}) GossMatcher { 17 | return &BeNumericallyMatcher{ 18 | Comparator: comparator, 19 | CompareTo: compareTo, 20 | } 21 | } 22 | func (m *BeNumericallyMatcher) Match(actual interface{}) (success bool, err error) { 23 | comparator, err := strToSymbol(m.Comparator) 24 | if err != nil { 25 | return false, err 26 | } 27 | matcher := &matchers.BeNumericallyMatcher{ 28 | Comparator: comparator, 29 | CompareTo: m.CompareTo, 30 | } 31 | return matcher.Match(actual) 32 | } 33 | 34 | func (m *BeNumericallyMatcher) FailureResult(actual interface{}) MatcherResult { 35 | return MatcherResult{ 36 | Actual: actual, 37 | Message: fmt.Sprintf("to be numerically %s", m.Comparator), 38 | Expected: m.CompareTo[0], 39 | } 40 | } 41 | 42 | func (m *BeNumericallyMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 43 | return MatcherResult{ 44 | Actual: actual, 45 | Message: fmt.Sprintf("not to be numerically %s", m.Comparator), 46 | Expected: m.CompareTo[0], 47 | } 48 | } 49 | 50 | func (m *BeNumericallyMatcher) MarshalJSON() ([]byte, error) { 51 | j := make(map[string]interface{}) 52 | j[m.Comparator] = m.CompareTo[0] 53 | return json.Marshal(j) 54 | } 55 | 56 | func strToSymbol(s string) (string, error) { 57 | comparator, ok := map[string]string{ 58 | "gt": ">", 59 | "ge": ">=", 60 | "lt": "<", 61 | "le": "<=", 62 | "eq": "==", 63 | }[s] 64 | if !ok { 65 | return "", fmt.Errorf("Unknown comparator: %s", s) 66 | } 67 | return comparator, nil 68 | } 69 | -------------------------------------------------------------------------------- /matchers/consist_of.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/onsi/gomega/matchers" 7 | "github.com/samber/lo" 8 | ) 9 | 10 | type ConsistOfMatcher struct { 11 | matchers.ConsistOfMatcher 12 | } 13 | 14 | func ConsistOf(elements ...interface{}) GossMatcher { 15 | return &ConsistOfMatcher{ 16 | matchers.ConsistOfMatcher{ 17 | Elements: elements, 18 | }, 19 | } 20 | } 21 | 22 | func (m *ConsistOfMatcher) FailureResult(actual interface{}) MatcherResult { 23 | missingElements := getUnexported(m, "missingElements") 24 | extraElements := getUnexported(m, "extraElements") 25 | missingEl, ok := missingElements.([]interface{}) 26 | var foundElements any 27 | if ok { 28 | foundElements, _ = lo.Difference(m.Elements, missingEl) 29 | } 30 | return MatcherResult{ 31 | Actual: actual, 32 | Message: "to consist of", 33 | Expected: m.Elements, 34 | MissingElements: missingElements, 35 | ExtraElements: extraElements, 36 | FoundElements: foundElements, 37 | } 38 | } 39 | 40 | func (m *ConsistOfMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 41 | return MatcherResult{ 42 | Actual: actual, 43 | Message: "not to consist of", 44 | Expected: m.Elements, 45 | } 46 | } 47 | 48 | func (m *ConsistOfMatcher) MarshalJSON() ([]byte, error) { 49 | j := make(map[string]interface{}) 50 | j["consist-of"] = m.Elements 51 | return json.Marshal(j) 52 | } 53 | -------------------------------------------------------------------------------- /matchers/contain_element_matcher.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/onsi/gomega/matchers" 7 | ) 8 | 9 | type ContainElementMatcher struct { 10 | matchers.ContainElementMatcher 11 | } 12 | 13 | func ContainElement(element interface{}) GossMatcher { 14 | return &ContainElementMatcher{ 15 | matchers.ContainElementMatcher{ 16 | Element: element, 17 | }, 18 | } 19 | } 20 | 21 | func (m *ContainElementMatcher) FailureResult(actual interface{}) MatcherResult { 22 | return MatcherResult{ 23 | Actual: actual, 24 | Message: "to contain element matching", 25 | Expected: m.Element, 26 | } 27 | } 28 | 29 | func (m *ContainElementMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 30 | return MatcherResult{ 31 | Actual: actual, 32 | Message: "not to contain element matching", 33 | Expected: m.Element, 34 | } 35 | } 36 | 37 | func (m *ContainElementMatcher) MarshalJSON() ([]byte, error) { 38 | j := make(map[string]interface{}) 39 | j["contain-element"] = m.Element 40 | return json.Marshal(j) 41 | } 42 | -------------------------------------------------------------------------------- /matchers/contain_elements_matcher.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/onsi/gomega/matchers" 7 | "github.com/samber/lo" 8 | ) 9 | 10 | type ContainElementsMatcher struct { 11 | matchers.ContainElementsMatcher 12 | } 13 | 14 | func ContainElements(elements ...interface{}) GossMatcher { 15 | return &ContainElementsMatcher{ 16 | matchers.ContainElementsMatcher{ 17 | Elements: elements, 18 | }, 19 | } 20 | } 21 | func (m *ContainElementsMatcher) FailureResult(actual interface{}) MatcherResult { 22 | missingElements := getUnexported(m, "missingElements") 23 | missingEl, ok := missingElements.([]interface{}) 24 | var foundElements any 25 | if ok { 26 | foundElements, _ = lo.Difference(m.Elements, missingEl) 27 | } 28 | return MatcherResult{ 29 | Actual: actual, 30 | Message: "to contain elements matching", 31 | Expected: m.Elements, 32 | MissingElements: missingElements, 33 | FoundElements: foundElements, 34 | } 35 | 36 | } 37 | func (m *ContainElementsMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 38 | return MatcherResult{ 39 | Actual: actual, 40 | Message: "not to contain elements matching", 41 | Expected: m.Elements, 42 | } 43 | 44 | } 45 | 46 | func (m *ContainElementsMatcher) MarshalJSON() ([]byte, error) { 47 | j := make(map[string]interface{}) 48 | j["contain-elements"] = m.Elements 49 | return json.Marshal(j) 50 | } 51 | -------------------------------------------------------------------------------- /matchers/contain_substring_matcher.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/onsi/gomega/matchers" 7 | ) 8 | 9 | type ContainSubstringMatcher struct { 10 | matchers.ContainSubstringMatcher 11 | } 12 | 13 | func ContainSubstring(substr string, args ...interface{}) GossMatcher { 14 | return &ContainSubstringMatcher{ 15 | matchers.ContainSubstringMatcher{ 16 | Substr: substr, 17 | Args: args, 18 | }, 19 | } 20 | } 21 | 22 | func (m *ContainSubstringMatcher) FailureResult(actual interface{}) MatcherResult { 23 | return MatcherResult{ 24 | Actual: actual, 25 | Message: "to contain substring", 26 | Expected: m.Substr, 27 | } 28 | } 29 | 30 | func (m *ContainSubstringMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 31 | return MatcherResult{ 32 | Actual: actual, 33 | Message: "not to contain substring", 34 | Expected: m.Substr, 35 | } 36 | } 37 | 38 | func (m *ContainSubstringMatcher) MarshalJSON() ([]byte, error) { 39 | j := make(map[string]interface{}) 40 | j["contain-substring"] = m.Substr 41 | return json.Marshal(j) 42 | } 43 | -------------------------------------------------------------------------------- /matchers/equal_matcher.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/onsi/gomega/matchers" 7 | ) 8 | 9 | type EqualMatcher struct { 10 | matchers.EqualMatcher 11 | } 12 | 13 | func Equal(element interface{}) GossMatcher { 14 | return &EqualMatcher{ 15 | matchers.EqualMatcher{ 16 | Expected: element, 17 | }, 18 | } 19 | } 20 | 21 | func (m *EqualMatcher) FailureResult(actual interface{}) MatcherResult { 22 | return MatcherResult{ 23 | Actual: actual, 24 | Message: "to equal", 25 | Expected: m.Expected, 26 | } 27 | } 28 | 29 | func (m *EqualMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 30 | return MatcherResult{ 31 | Actual: actual, 32 | Message: "not to equal", 33 | Expected: m.Expected, 34 | } 35 | } 36 | 37 | func (m *EqualMatcher) MarshalJSON() ([]byte, error) { 38 | return json.Marshal(m.Expected) 39 | } 40 | -------------------------------------------------------------------------------- /matchers/have_key_matcher.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/onsi/gomega/matchers" 7 | ) 8 | 9 | type HaveKeyMatcher struct { 10 | matchers.HaveKeyMatcher 11 | } 12 | 13 | func HaveKey(key interface{}) GossMatcher { 14 | return &HaveKeyMatcher{ 15 | matchers.HaveKeyMatcher{ 16 | Key: key, 17 | }, 18 | } 19 | } 20 | 21 | func (m *HaveKeyMatcher) FailureResult(actual interface{}) MatcherResult { 22 | return MatcherResult{ 23 | Actual: actual, 24 | Message: "to have key matching", 25 | Expected: m.Key, 26 | } 27 | } 28 | 29 | func (m *HaveKeyMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 30 | return MatcherResult{ 31 | Actual: actual, 32 | Message: "not to have key matching", 33 | Expected: m.Key, 34 | } 35 | } 36 | 37 | func (m *HaveKeyMatcher) MarshalJSON() ([]byte, error) { 38 | j := make(map[string]interface{}) 39 | j["have-key"] = m.Key 40 | return json.Marshal(j) 41 | } 42 | -------------------------------------------------------------------------------- /matchers/have_len_matcher.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/onsi/gomega/matchers" 7 | ) 8 | 9 | type HaveLenMatcher struct { 10 | matchers.HaveLenMatcher 11 | } 12 | 13 | func HaveLen(count int) GossMatcher { 14 | return &HaveLenMatcher{ 15 | matchers.HaveLenMatcher{ 16 | Count: count, 17 | }, 18 | } 19 | } 20 | 21 | func (m *HaveLenMatcher) FailureResult(actual interface{}) MatcherResult { 22 | return MatcherResult{ 23 | Actual: actual, 24 | Message: "to have length", 25 | Expected: m.Count, 26 | } 27 | } 28 | 29 | func (m *HaveLenMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 30 | return MatcherResult{ 31 | Actual: actual, 32 | Message: "not to have length", 33 | Expected: m.Count, 34 | } 35 | } 36 | 37 | func (m *HaveLenMatcher) MarshalJSON() ([]byte, error) { 38 | j := make(map[string]interface{}) 39 | j["have-len"] = m.Count 40 | return json.Marshal(j) 41 | } 42 | -------------------------------------------------------------------------------- /matchers/have_prefix_matcher.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/onsi/gomega/matchers" 7 | ) 8 | 9 | type HavePrefixMatcher struct { 10 | matchers.HavePrefixMatcher 11 | } 12 | 13 | func HavePrefix(prefix string, args ...interface{}) GossMatcher { 14 | return &HavePrefixMatcher{ 15 | matchers.HavePrefixMatcher{ 16 | Prefix: prefix, 17 | Args: args, 18 | }, 19 | } 20 | } 21 | 22 | func (m *HavePrefixMatcher) FailureResult(actual interface{}) MatcherResult { 23 | return MatcherResult{ 24 | Actual: actual, 25 | Message: "to have prefix", 26 | Expected: m.Prefix, 27 | } 28 | } 29 | 30 | func (m *HavePrefixMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 31 | return MatcherResult{ 32 | Actual: actual, 33 | Message: "not to have prefix", 34 | Expected: m.Prefix, 35 | } 36 | } 37 | 38 | func (m *HavePrefixMatcher) MarshalJSON() ([]byte, error) { 39 | j := make(map[string]interface{}) 40 | j["have-prefix"] = m.Prefix 41 | return json.Marshal(j) 42 | } 43 | -------------------------------------------------------------------------------- /matchers/have_suffix_matcher.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/onsi/gomega/matchers" 7 | ) 8 | 9 | type HaveSuffixMatcher struct { 10 | matchers.HaveSuffixMatcher 11 | } 12 | 13 | func HaveSuffix(prefix string, args ...interface{}) GossMatcher { 14 | return &HaveSuffixMatcher{ 15 | matchers.HaveSuffixMatcher{ 16 | Suffix: prefix, 17 | Args: args, 18 | }, 19 | } 20 | } 21 | 22 | func (m *HaveSuffixMatcher) FailureResult(actual interface{}) MatcherResult { 23 | return MatcherResult{ 24 | Actual: actual, 25 | Message: "to have suffix", 26 | Expected: m.Suffix, 27 | } 28 | } 29 | 30 | func (m *HaveSuffixMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 31 | return MatcherResult{ 32 | Actual: actual, 33 | Message: "not to have suffix", 34 | Expected: m.Suffix, 35 | } 36 | } 37 | 38 | func (m *HaveSuffixMatcher) MarshalJSON() ([]byte, error) { 39 | j := make(map[string]interface{}) 40 | j["have-suffix"] = m.Suffix 41 | return json.Marshal(j) 42 | } 43 | -------------------------------------------------------------------------------- /matchers/match_regexp_matcher.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/onsi/gomega/matchers" 7 | ) 8 | 9 | type MatchRegexpMatcher struct { 10 | matchers.MatchRegexpMatcher 11 | } 12 | 13 | func MatchRegexp(regexp string, args ...interface{}) GossMatcher { 14 | return &MatchRegexpMatcher{ 15 | matchers.MatchRegexpMatcher{ 16 | Regexp: regexp, 17 | Args: args, 18 | }, 19 | } 20 | } 21 | 22 | func (m *MatchRegexpMatcher) FailureResult(actual interface{}) MatcherResult { 23 | return MatcherResult{ 24 | Actual: actual, 25 | Message: "to match regular expression", 26 | Expected: m.Regexp, 27 | } 28 | } 29 | 30 | func (m *MatchRegexpMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 31 | return MatcherResult{ 32 | Actual: actual, 33 | Message: "not to match regular expression", 34 | Expected: m.Regexp, 35 | } 36 | } 37 | 38 | func (m *MatchRegexpMatcher) MarshalJSON() ([]byte, error) { 39 | j := make(map[string]interface{}) 40 | j["match-regexp"] = m.Regexp 41 | return json.Marshal(j) 42 | } 43 | -------------------------------------------------------------------------------- /matchers/matchers.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "unsafe" 7 | 8 | "github.com/onsi/gomega/types" 9 | ) 10 | 11 | type GossMatcher interface { 12 | // This is needed due to oMegaMatcher test in some of the GomegaMatcher logic 13 | types.GomegaMatcher 14 | //Match(actual interface{}) (success bool, err error) 15 | FailureResult(actual interface{}) MatcherResult 16 | NegatedFailureResult(actual interface{}) MatcherResult 17 | // This doesn't seem to make a difference, maybe not needed 18 | json.Marshaler 19 | } 20 | 21 | type MatcherResult struct { 22 | Actual interface{} `json:"actual"` 23 | Message string `json:"message"` 24 | Expected interface{} `json:"expected"` 25 | MissingElements interface{} `json:"missing-elements"` 26 | FoundElements interface{} `json:"found-elements"` 27 | ExtraElements interface{} `json:"extra-elements"` 28 | TransformerChain []Transformer `json:"transform-chain"` 29 | UntransformedValue interface{} `json:"untransformed-value"` 30 | } 31 | 32 | func getUnexported(i interface{}, field string) interface{} { 33 | rs := reflect.ValueOf(i).Elem() 34 | rf := rs.FieldByName(field) 35 | rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem() 36 | return rf.Interface() 37 | } 38 | 39 | type fakeOmegaMatcher struct{} 40 | 41 | // FailureMessage is a stub to honor omegaMatcher interface 42 | func (m *fakeOmegaMatcher) FailureMessage(_ interface{}) (message string) { 43 | return "" 44 | } 45 | 46 | // NegatedFailureMessage is a stub to honor omegaMatcher interface 47 | func (m *fakeOmegaMatcher) NegatedFailureMessage(_ interface{}) (message string) { 48 | return "" 49 | } 50 | -------------------------------------------------------------------------------- /matchers/not.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type NotMatcher struct { 8 | fakeOmegaMatcher 9 | Matcher GossMatcher 10 | } 11 | 12 | func Not(matcher GossMatcher) GossMatcher { 13 | return &NotMatcher{Matcher: matcher} 14 | } 15 | 16 | func (m *NotMatcher) Match(actual interface{}) (bool, error) { 17 | success, err := m.Matcher.Match(actual) 18 | if err != nil { 19 | return false, err 20 | } 21 | return !success, nil 22 | } 23 | 24 | func (m *NotMatcher) FailureResult(actual interface{}) MatcherResult { 25 | return m.Matcher.NegatedFailureResult(actual) 26 | } 27 | 28 | func (m *NotMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 29 | return m.Matcher.FailureResult(actual) 30 | } 31 | 32 | func (m *NotMatcher) MarshalJSON() ([]byte, error) { 33 | j := make(map[string]interface{}) 34 | j["not"] = m.Matcher 35 | return json.Marshal(j) 36 | } 37 | -------------------------------------------------------------------------------- /matchers/or.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type OrMatcher struct { 8 | fakeOmegaMatcher 9 | 10 | Matchers []GossMatcher 11 | 12 | // state 13 | firstSuccessfulMatcher GossMatcher 14 | } 15 | 16 | func Or(ms ...GossMatcher) GossMatcher { 17 | return &OrMatcher{Matchers: ms} 18 | } 19 | 20 | func (m *OrMatcher) Match(actual interface{}) (success bool, err error) { 21 | m.firstSuccessfulMatcher = nil 22 | for _, matcher := range m.Matchers { 23 | success, err := matcher.Match(actual) 24 | if err != nil { 25 | return false, err 26 | } 27 | if success { 28 | m.firstSuccessfulMatcher = matcher 29 | return true, nil 30 | } 31 | } 32 | return false, nil 33 | } 34 | 35 | func (m *OrMatcher) FailureResult(actual interface{}) MatcherResult { 36 | return MatcherResult{ 37 | Actual: actual, 38 | Message: "to satisfy at least one of these matchers", 39 | Expected: m.Matchers, 40 | } 41 | } 42 | 43 | func (m *OrMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 44 | firstSuccessfulMatcher := getUnexported(m, "firstSuccessfulMatcher") 45 | return firstSuccessfulMatcher.(GossMatcher).NegatedFailureResult(actual) 46 | } 47 | 48 | func (m *OrMatcher) MarshalJSON() ([]byte, error) { 49 | j := make(map[string]interface{}) 50 | j["or"] = m.Matchers 51 | return json.Marshal(j) 52 | } 53 | -------------------------------------------------------------------------------- /matchers/with_safe_transform.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | type WithSafeTransformMatcher struct { 10 | fakeOmegaMatcher 11 | 12 | // input 13 | Transform Transformer // must be a function of one parameter that returns one value 14 | Matcher GossMatcher 15 | 16 | // state 17 | transformedValue interface{} 18 | wasTransformed bool 19 | } 20 | 21 | func WithSafeTransform(transform Transformer, matcher GossMatcher) GossMatcher { 22 | 23 | return &WithSafeTransformMatcher{ 24 | Transform: transform, 25 | Matcher: matcher, 26 | } 27 | } 28 | 29 | func (m *WithSafeTransformMatcher) Match(actual interface{}) (bool, error) { 30 | var err error 31 | //log.Printf("%#v: input: %v", m.Transform, actual) 32 | m.transformedValue, err = m.Transform.Transform(actual) 33 | if !reflect.DeepEqual(actual, m.transformedValue) { 34 | m.wasTransformed = true 35 | } 36 | if err != nil { 37 | return false, fmt.Errorf("%#v: %s", m.Transform, err) 38 | } 39 | //log.Printf("%#v: output: %v", m.Transform, m.transformedValue) 40 | return m.Matcher.Match(m.transformedValue) 41 | } 42 | 43 | func (m *WithSafeTransformMatcher) FailureResult(actual interface{}) MatcherResult { 44 | tchain, matcher, tvalue := m.getTransformerChainAndMatcher() 45 | result := matcher.FailureResult(tvalue) 46 | result.TransformerChain = tchain 47 | result.UntransformedValue = actual 48 | return result 49 | } 50 | func (m *WithSafeTransformMatcher) NegatedFailureResult(actual interface{}) MatcherResult { 51 | tchain, matcher, tvalue := m.getTransformerChainAndMatcher() 52 | result := matcher.NegatedFailureResult(tvalue) 53 | result.TransformerChain = tchain 54 | result.UntransformedValue = actual 55 | return result 56 | } 57 | 58 | func (m *WithSafeTransformMatcher) getTransformerChainAndMatcher() (tchain []Transformer, matcher GossMatcher, tvalue interface{}) { 59 | matcher = m 60 | tvalue = m.transformedValue 61 | L: 62 | for { 63 | switch v := matcher.(type) { 64 | case *WithSafeTransformMatcher: 65 | matcher = v.Matcher 66 | tvalue = v.transformedValue 67 | if v.wasTransformed { 68 | tchain = append(tchain, v.Transform) 69 | } 70 | default: 71 | break L 72 | 73 | } 74 | } 75 | return tchain, matcher, tvalue 76 | 77 | } 78 | 79 | func (m *WithSafeTransformMatcher) MarshalJSON() ([]byte, error) { 80 | _, matcher, _ := m.getTransformerChainAndMatcher() 81 | return json.Marshal(matcher) 82 | } 83 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Goss 2 | site_description: Goss is a YAML based serverspec alternative tool for validating a server’s configuration. 3 | site_author: Goss team 4 | site_url: https://goss.readthedocs.io/ 5 | repo_url: https://github.com/goss-org/goss 6 | repo_name: goss-org/goss 7 | edit_uri: edit/master/docs/ 8 | 9 | 10 | theme: 11 | name: material 12 | palette: 13 | - media: "(prefers-color-scheme: light)" 14 | scheme: default 15 | primary: black 16 | accent: amber 17 | toggle: 18 | icon: material/weather-sunny 19 | name: Switch to dark mode 20 | - media: "(prefers-color-scheme: dark)" 21 | scheme: slate 22 | primary: black 23 | accent: indigo 24 | toggle: 25 | icon: material/weather-night 26 | name: Switch to light mode 27 | features: 28 | - content.action.edit 29 | - content.code.copy 30 | - navigation.footer 31 | - navigation.instant 32 | - navigation.instant.progress 33 | - navigation.top 34 | - navigation.tracking 35 | - search.highlight 36 | - search.share 37 | - search.suggest 38 | - toc.follow 39 | 40 | 41 | extra_css: 42 | - style.css 43 | 44 | plugins: 45 | - search 46 | - awesome-pages 47 | - macros: 48 | render_by_default: false 49 | - exclude: 50 | glob: 51 | - requirements.txt 52 | 53 | markdown_extensions: 54 | - abbr 55 | - admonition 56 | - attr_list 57 | - def_list 58 | - md_in_html 59 | - mdx_breakless_lists 60 | - tables 61 | - pymdownx.details 62 | - pymdownx.emoji: 63 | emoji_index: !!python/name:material.extensions.emoji.twemoji 64 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 65 | - pymdownx.highlight: 66 | anchor_linenums: true 67 | line_spans: __span 68 | pygments_lang_class: true 69 | - pymdownx.inlinehilite 70 | - pymdownx.magiclink: 71 | repo_url_shortener: true 72 | social_url_shortener: true 73 | repo_url_shorthand: true 74 | social_url_shorthand: true 75 | user: goss-org 76 | repo: goss 77 | - pymdownx.snippets: 78 | base_path: 79 | - . 80 | - docs/snippets 81 | check_paths: true 82 | - pymdownx.superfences 83 | 84 | copyright: Copyright © 2015 - 2024 Ahmed Elsabbahy 85 | 86 | extra: 87 | social: 88 | - icon: fontawesome/brands/github 89 | link: https://github.com/goss-org/goss 90 | - icon: simple/travisci 91 | link: https://travis-ci.org/goss-org/goss 92 | - icon: fontawesome/brands/medium 93 | link: https://medium.com/@aelsabbahy 94 | 95 | watch: 96 | - README.md 97 | - LICENSE 98 | - .github/CONTRIBUTING.md 99 | - extras/dcgoss/README.md 100 | - extras/dgoss/README.md 101 | - extras/kgoss/README.md 102 | -------------------------------------------------------------------------------- /novendor.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Bash replacement for glide novendor command 5 | # Returns all directories which include go files 6 | 7 | DIRS=$(ls -ld */ . | awk {'print $9'} | grep -v vendor) 8 | 9 | for DIR in ${DIRS}; do 10 | GOFILES=$(git ls-files ${DIR} | grep ".*\.go$") || true 11 | 12 | if [[ ${DIR} == "." ]]; then 13 | echo "." 14 | continue 15 | fi 16 | 17 | if [[ ${GOFILES} != "" ]]; then 18 | echo "./"${DIR}"..." 19 | fi 20 | done 21 | 22 | exit 0 23 | -------------------------------------------------------------------------------- /outputs/documentation.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | 8 | "github.com/goss-org/goss/resource" 9 | "github.com/goss-org/goss/util" 10 | ) 11 | 12 | type Documentation struct{} 13 | 14 | func (r Documentation) ValidOptions() []*formatOption { 15 | return []*formatOption{ 16 | {name: foSort}, 17 | } 18 | } 19 | 20 | func (r Documentation) Output(w io.Writer, results <-chan []resource.TestResult, 21 | outConfig util.OutputConfig) (exitCode int) { 22 | includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) 23 | 24 | sort := util.IsValueInList(foSort, outConfig.FormatOptions) 25 | results = getResults(results, sort) 26 | 27 | var startTime time.Time 28 | var endTime time.Time 29 | testCount := 0 30 | var failedOrSkipped [][]resource.TestResult 31 | var skipped, failed int 32 | for resultGroup := range results { 33 | failedOrSkippedGroup := []resource.TestResult{} 34 | first := resultGroup[0] 35 | header := header(first) 36 | if header != "" { 37 | fmt.Fprint(w, header) 38 | } 39 | for _, testResult := range resultGroup { 40 | if startTime.IsZero() || testResult.StartTime.Before(startTime) { 41 | startTime = testResult.StartTime 42 | } 43 | if endTime.IsZero() || testResult.EndTime.After(endTime) { 44 | endTime = testResult.EndTime 45 | } 46 | switch testResult.Result { 47 | case resource.SUCCESS: 48 | fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw)) 49 | case resource.SKIP: 50 | fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw)) 51 | failedOrSkippedGroup = append(failedOrSkippedGroup, testResult) 52 | skipped++ 53 | case resource.FAIL: 54 | fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw)) 55 | failedOrSkippedGroup = append(failedOrSkippedGroup, testResult) 56 | failed++ 57 | } 58 | testCount++ 59 | } 60 | if len(failedOrSkippedGroup) > 0 { 61 | failedOrSkipped = append(failedOrSkipped, failedOrSkippedGroup) 62 | } 63 | } 64 | 65 | fmt.Fprint(w, "\n\n") 66 | fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped, includeRaw)) 67 | 68 | fmt.Fprint(w, summary(startTime, endTime, testCount, failed, skipped)) 69 | if failed > 0 { 70 | return 1 71 | } 72 | return 0 73 | } 74 | -------------------------------------------------------------------------------- /outputs/json.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "time" 9 | 10 | "github.com/fatih/color" 11 | "github.com/goss-org/goss/resource" 12 | "github.com/goss-org/goss/util" 13 | ) 14 | 15 | type Json struct{} 16 | 17 | func (r Json) ValidOptions() []*formatOption { 18 | return []*formatOption{ 19 | {name: foPretty}, 20 | {name: foSort}, 21 | } 22 | } 23 | 24 | func (r Json) Output(w io.Writer, results <-chan []resource.TestResult, 25 | outConfig util.OutputConfig) (exitCode int) { 26 | 27 | var pretty bool = util.IsValueInList(foPretty, outConfig.FormatOptions) 28 | includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) 29 | 30 | sort := util.IsValueInList(foSort, outConfig.FormatOptions) 31 | results = getResults(results, sort) 32 | 33 | var startTime time.Time 34 | var endTime time.Time 35 | color.NoColor = true 36 | testCount := 0 37 | failed := 0 38 | skipped := 0 39 | var resultsOut []map[string]any 40 | for resultGroup := range results { 41 | for _, testResult := range resultGroup { 42 | if startTime.IsZero() || testResult.StartTime.Before(startTime) { 43 | startTime = testResult.StartTime 44 | } 45 | if endTime.IsZero() || testResult.EndTime.After(endTime) { 46 | endTime = testResult.EndTime 47 | } 48 | if testResult.Result == resource.FAIL { 49 | failed++ 50 | logTrace("TRACE", "FAIL", testResult, true) 51 | } else { 52 | logTrace("TRACE", "SUCCESS", testResult, true) 53 | } 54 | if testResult.Skipped { 55 | skipped++ 56 | } 57 | m := struct2map(testResult) 58 | m["successful"] = testResult.Result != resource.FAIL 59 | m["summary-line"] = humanizeResult(testResult, false, includeRaw) 60 | m["summary-line-compact"] = humanizeResult(testResult, true, includeRaw) 61 | m["duration"] = testResult.Duration.Nanoseconds() 62 | resultsOut = append(resultsOut, m) 63 | testCount++ 64 | } 65 | } 66 | 67 | summary := make(map[string]any) 68 | duration := endTime.Sub(startTime) 69 | summary["test-count"] = testCount 70 | summary["failed-count"] = failed 71 | summary["skipped-count"] = skipped 72 | summary["total-duration"] = duration 73 | summary["summary-line"] = fmt.Sprintf("Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs", testCount, failed, skipped, duration.Seconds()) 74 | 75 | out := make(map[string]any) 76 | out["results"] = resultsOut 77 | out["summary"] = summary 78 | 79 | var j []byte 80 | if pretty { 81 | j, _ = json.MarshalIndent(out, "", " ") 82 | } else { 83 | j, _ = json.Marshal(out) 84 | } 85 | 86 | resstr := string(j) 87 | fmt.Fprintln(w, resstr) 88 | 89 | if failed > 0 { 90 | log.Printf("[DEBUG] FAIL SUMMARY: %s", resstr) 91 | return 1 92 | } 93 | 94 | log.Printf("[DEBUG] OK SUMMARY: %s", resstr) 95 | return 0 96 | } 97 | 98 | func struct2map(i any) map[string]any { 99 | out := make(map[string]any) 100 | j, _ := json.Marshal(i) 101 | json.Unmarshal(j, &out) 102 | return out 103 | } 104 | -------------------------------------------------------------------------------- /outputs/junit.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "fmt" 7 | "io" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/fatih/color" 12 | "github.com/goss-org/goss/resource" 13 | "github.com/goss-org/goss/util" 14 | ) 15 | 16 | type JUnit struct{} 17 | 18 | func (r JUnit) ValidOptions() []*formatOption { 19 | return []*formatOption{ 20 | {name: foSort}, 21 | } 22 | } 23 | 24 | func (r JUnit) Output(w io.Writer, results <-chan []resource.TestResult, 25 | outConfig util.OutputConfig) (exitCode int) { 26 | includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) 27 | 28 | sort := util.IsValueInList(foSort, outConfig.FormatOptions) 29 | results = getResults(results, sort) 30 | 31 | color.NoColor = true 32 | var testCount, failed, skipped int 33 | 34 | // ISO8601 timeformat 35 | timestamp := time.Now().Format(time.RFC3339) 36 | 37 | var summary map[int]string = make(map[int]string) 38 | 39 | var startTime time.Time 40 | var endTime time.Time 41 | for resultGroup := range results { 42 | for _, testResult := range resultGroup { 43 | if startTime.IsZero() || testResult.StartTime.Before(startTime) { 44 | startTime = testResult.StartTime 45 | } 46 | if endTime.IsZero() || testResult.EndTime.After(endTime) { 47 | endTime = testResult.EndTime 48 | } 49 | duration := strconv.FormatFloat(testResult.Duration.Seconds(), 'f', 3, 64) 50 | summary[testCount] = "\n" 55 | if testResult.Result == resource.FAIL { 56 | summary[testCount] += "" + 57 | escapeString(humanizeResult(testResult, true, includeRaw)) + 58 | "\n" 59 | summary[testCount] += "" + 60 | escapeString(humanizeResult(testResult, true, includeRaw)) + 61 | "\n\n" 62 | 63 | failed++ 64 | } else { 65 | if testResult.Result == resource.SKIP { 66 | summary[testCount] += "" 67 | skipped++ 68 | } 69 | summary[testCount] += "" + 70 | escapeString(humanizeResult(testResult, true, includeRaw)) + 71 | "\n\n" 72 | } 73 | testCount++ 74 | } 75 | } 76 | 77 | duration := endTime.Sub(startTime) 78 | fmt.Fprintln(w, "") 79 | fmt.Fprintf(w, "\n", 81 | testCount, failed, skipped, duration.Seconds(), timestamp) 82 | 83 | for i := 0; i < testCount; i++ { 84 | fmt.Fprintf(w, "%s", summary[i]) 85 | } 86 | 87 | fmt.Fprintln(w, "") 88 | 89 | if failed > 0 { 90 | return 1 91 | } 92 | 93 | return 0 94 | } 95 | 96 | func escapeString(str string) string { 97 | buffer := new(bytes.Buffer) 98 | xml.EscapeText(buffer, []byte(str)) 99 | return buffer.String() 100 | } 101 | -------------------------------------------------------------------------------- /outputs/nagios.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/goss-org/goss/resource" 10 | "github.com/goss-org/goss/util" 11 | ) 12 | 13 | type Nagios struct{} 14 | 15 | func (r Nagios) ValidOptions() []*formatOption { 16 | return []*formatOption{ 17 | {name: foPerfData}, 18 | {name: foVerbose}, 19 | } 20 | } 21 | 22 | func (r Nagios) Output(w io.Writer, results <-chan []resource.TestResult, 23 | outConfig util.OutputConfig) (exitCode int) { 24 | 25 | var testCount, failed, skipped int 26 | 27 | var perfdata, verbose bool 28 | perfdata = util.IsValueInList(foPerfData, outConfig.FormatOptions) 29 | verbose = util.IsValueInList(foVerbose, outConfig.FormatOptions) 30 | includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) 31 | 32 | var startTime time.Time 33 | var endTime time.Time 34 | var summary map[int]string = make(map[int]string) 35 | 36 | for resultGroup := range results { 37 | for _, testResult := range resultGroup { 38 | if startTime.IsZero() || testResult.StartTime.Before(startTime) { 39 | startTime = testResult.StartTime 40 | } 41 | if endTime.IsZero() || testResult.EndTime.After(endTime) { 42 | endTime = testResult.EndTime 43 | } 44 | switch testResult.Result { 45 | case resource.FAIL: 46 | if util.IsValueInList(foVerbose, outConfig.FormatOptions) { 47 | summary[failed] = "Fail " + strconv.Itoa(failed+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n" 48 | } 49 | failed++ 50 | case resource.SKIP: 51 | skipped++ 52 | } 53 | testCount++ 54 | } 55 | } 56 | 57 | duration := endTime.Sub(startTime) 58 | if failed > 0 { 59 | fmt.Fprintf(w, "GOSS CRITICAL - Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs", testCount, failed, skipped, duration.Seconds()) 60 | if perfdata { 61 | fmt.Fprintf(w, "|total=%d failed=%d skipped=%d duration=%.3fs", testCount, failed, skipped, duration.Seconds()) 62 | } 63 | fmt.Fprint(w, "\n") 64 | if verbose { 65 | for i := 0; i < failed; i++ { 66 | fmt.Fprintf(w, "%s", summary[i]) 67 | } 68 | } 69 | return 2 70 | } 71 | fmt.Fprintf(w, "GOSS OK - Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs", testCount, failed, skipped, duration.Seconds()) 72 | if perfdata { 73 | fmt.Fprintf(w, "|total=%d failed=%d skipped=%d duration=%.3fs", testCount, failed, skipped, duration.Seconds()) 74 | } 75 | fmt.Fprint(w, "\n") 76 | return 0 77 | } 78 | -------------------------------------------------------------------------------- /outputs/outputs_test.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsValidFormat(t *testing.T) { 10 | if IsValidFormat("ne") { 11 | t.Fatal("'ne' should not be a valid output format") 12 | } 13 | 14 | if !IsValidFormat("json") { 15 | t.Fatal("'json' should be a valid output format") 16 | } 17 | } 18 | 19 | func TestOutputers(t *testing.T) { 20 | list := Outputers() 21 | assert.NotEmpty(t, list) 22 | } 23 | 24 | func TestGetOutputer(t *testing.T) { 25 | t.Run("valid", func(t *testing.T) { 26 | got, err := GetOutputer("rspecish") 27 | assert.NoError(t, err) 28 | assert.NotNil(t, got) 29 | }) 30 | t.Run("not-valid", func(t *testing.T) { 31 | got, err := GetOutputer("gibberish") 32 | assert.Error(t, err) 33 | assert.Nil(t, got) 34 | }) 35 | } 36 | 37 | func TestOutputFormatOptions(t *testing.T) { 38 | list := FormatOptions() 39 | assert.NotEmpty(t, list) 40 | 41 | assert.Contains(t, list, foPerfData) 42 | assert.Contains(t, list, foPretty) 43 | assert.Contains(t, list, foVerbose) 44 | assert.Len(t, list, 4) 45 | } 46 | 47 | func TestOptionsRegistration(t *testing.T) { 48 | registeredOutputs := Outputers() 49 | assert.Contains(t, registeredOutputs, "documentation") 50 | assert.Contains(t, registeredOutputs, "json") 51 | assert.Contains(t, registeredOutputs, "junit") 52 | assert.Contains(t, registeredOutputs, "nagios") 53 | assert.Contains(t, registeredOutputs, "prometheus") 54 | assert.Contains(t, registeredOutputs, "rspecish") 55 | assert.Contains(t, registeredOutputs, "silent") 56 | assert.Contains(t, registeredOutputs, "structured") 57 | assert.Contains(t, registeredOutputs, "tap") 58 | } 59 | -------------------------------------------------------------------------------- /outputs/rspecish.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "strings" 8 | "time" 9 | 10 | "github.com/goss-org/goss/resource" 11 | "github.com/goss-org/goss/util" 12 | ) 13 | 14 | type Rspecish struct{} 15 | 16 | func (r Rspecish) ValidOptions() []*formatOption { 17 | return []*formatOption{} 18 | } 19 | 20 | func (r Rspecish) Output(w io.Writer, results <-chan []resource.TestResult, 21 | outConfig util.OutputConfig) (exitCode int) { 22 | 23 | sort := util.IsValueInList(foSort, outConfig.FormatOptions) 24 | results = getResults(results, sort) 25 | 26 | var startTime time.Time 27 | var endTime time.Time 28 | testCount := 0 29 | var failedOrSkipped [][]resource.TestResult 30 | var skipped, failed int 31 | for resultGroup := range results { 32 | failedOrSkippedGroup := []resource.TestResult{} 33 | for _, testResult := range resultGroup { 34 | // Calculates the start and end times based on the start of the first test 35 | // and end of the last test, this allows the time/duration to be stable 36 | // FIXME: move this to shared code 37 | if startTime.IsZero() || testResult.StartTime.Before(startTime) { 38 | startTime = testResult.StartTime 39 | } 40 | if endTime.IsZero() || testResult.EndTime.After(endTime) { 41 | endTime = testResult.EndTime 42 | } 43 | switch testResult.Result { 44 | case resource.SUCCESS: 45 | logTrace("TRACE", "SUCCESS", testResult, false) 46 | fmt.Fprint(w, green(".")) 47 | case resource.SKIP: 48 | logTrace("TRACE", "SKIP", testResult, false) 49 | fmt.Fprint(w, yellow("S")) 50 | failedOrSkippedGroup = append(failedOrSkippedGroup, testResult) 51 | skipped++ 52 | case resource.FAIL: 53 | logTrace("TRACE", "FAIL", testResult, false) 54 | fmt.Fprint(w, red("F")) 55 | failedOrSkippedGroup = append(failedOrSkippedGroup, testResult) 56 | failed++ 57 | } 58 | testCount++ 59 | } 60 | if len(failedOrSkippedGroup) > 0 { 61 | failedOrSkipped = append(failedOrSkipped, failedOrSkippedGroup) 62 | } 63 | } 64 | 65 | fmt.Fprint(w, "\n\n") 66 | includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) 67 | 68 | fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped, includeRaw)) 69 | 70 | outstr := summary(startTime, endTime, testCount, failed, skipped) 71 | fmt.Fprint(w, outstr) 72 | resstr := strings.ReplaceAll(outstr, "\n", " ") 73 | if failed > 0 { 74 | log.Printf("[DEBUG] FAIL SUMMARY: %s", resstr) 75 | return 1 76 | } 77 | log.Printf("[DEBUG] OK SUMMARY: %s", resstr) 78 | return 0 79 | } 80 | -------------------------------------------------------------------------------- /outputs/silent.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/goss-org/goss/resource" 7 | "github.com/goss-org/goss/util" 8 | ) 9 | 10 | type Silent struct{} 11 | 12 | func (r Silent) ValidOptions() []*formatOption { 13 | return []*formatOption{} 14 | } 15 | 16 | func (r Silent) Output(w io.Writer, results <-chan []resource.TestResult, 17 | outConfig util.OutputConfig) (exitCode int) { 18 | 19 | var failed int 20 | for resultGroup := range results { 21 | for _, testResult := range resultGroup { 22 | switch testResult.Result { 23 | case resource.FAIL: 24 | failed++ 25 | } 26 | } 27 | } 28 | 29 | if failed > 0 { 30 | return 1 31 | } 32 | return 0 33 | } 34 | -------------------------------------------------------------------------------- /outputs/tap.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | 8 | "github.com/goss-org/goss/resource" 9 | "github.com/goss-org/goss/util" 10 | ) 11 | 12 | type Tap struct{} 13 | 14 | func (r Tap) ValidOptions() []*formatOption { 15 | return []*formatOption{ 16 | {name: foSort}, 17 | } 18 | } 19 | 20 | func (r Tap) Output(w io.Writer, results <-chan []resource.TestResult, 21 | outConfig util.OutputConfig) (exitCode int) { 22 | includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) 23 | 24 | sort := util.IsValueInList(foSort, outConfig.FormatOptions) 25 | results = getResults(results, sort) 26 | 27 | testCount := 0 28 | failed := 0 29 | 30 | var summary map[int]string = make(map[int]string) 31 | 32 | for resultGroup := range results { 33 | for _, testResult := range resultGroup { 34 | switch testResult.Result { 35 | case resource.SUCCESS: 36 | summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n" 37 | case resource.FAIL: 38 | summary[testCount] = "not ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n" 39 | failed++ 40 | case resource.SKIP: 41 | summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - # SKIP " + humanizeResult(testResult, true, includeRaw) + "\n" 42 | default: 43 | panic(fmt.Sprintf("Unexpected Result Code: %v\n", testResult.Result)) 44 | } 45 | testCount++ 46 | } 47 | } 48 | 49 | fmt.Fprintf(w, "1..%d\n", testCount) 50 | 51 | for i := 0; i < testCount; i++ { 52 | fmt.Fprintf(w, "%s", summary[i]) 53 | } 54 | 55 | if failed > 0 { 56 | return 1 57 | } 58 | 59 | return 0 60 | } 61 | -------------------------------------------------------------------------------- /outputs/traces.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/goss-org/goss/resource" 7 | ) 8 | 9 | func logTrace(level string, msg string, testResult resource.TestResult, withIntResult bool) { 10 | if withIntResult { 11 | log.Printf("[%s] %s: %s => %s (%s %+v %+v) [%.02f] [%d]", 12 | level, 13 | msg, 14 | testResult.ResourceType, 15 | testResult.ResourceId, 16 | testResult.Property, 17 | testResult.MatcherResult.Expected, 18 | testResult.MatcherResult.Actual, 19 | testResult.Duration.Seconds(), 20 | testResult.Result, 21 | ) 22 | } else { 23 | log.Printf("[%s] %s: %s => %s (%s %+v %+v) [%.02f]", 24 | level, 25 | msg, 26 | testResult.ResourceType, 27 | testResult.ResourceId, 28 | testResult.Property, 29 | testResult.MatcherResult.Expected, 30 | testResult.MatcherResult.Actual, 31 | testResult.Duration.Seconds(), 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /release-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | platform_spec="${1:?"Must supply name of release binary to build e.g. goss-linux-amd64"}" 5 | version_stamp="${TRAVIS_TAG:-"0.0.0"}" 6 | 7 | # Split platform_spec into platform/arch segments 8 | IFS='- ' read -r -a segments <<< "${platform_spec}" 9 | 10 | os="${segments[0]}" 11 | arch="${segments[1]}" 12 | if [[ "${segments[0]}" == "alpha" ]]; then 13 | os="${segments[1]}" 14 | arch="${segments[2]}" 15 | fi 16 | 17 | output_dir="release/" 18 | output_fname="goss-${platform_spec}" 19 | if [[ "${os}" == "windows" ]]; then 20 | output_fname="${output_fname}.exe" 21 | fi 22 | output="${output_dir}/${output_fname}" 23 | 24 | GOOS="${os}" GOARCH="${arch}" CGO_ENABLED=0 go build \ 25 | -ldflags "-X github.com/goss-org/goss/util.Version=${version_stamp} -s -w" \ 26 | -o "${output}" \ 27 | github.com/goss-org/goss/cmd/goss 28 | 29 | chmod +x "${output}" 30 | 31 | function __sha256sum { 32 | if [[ "$OSTYPE" == "darwin"* ]]; then 33 | shasum -a 256 "$1" 34 | else 35 | sha256sum "$1" 36 | fi 37 | } 38 | 39 | (cd "$output_dir" && __sha256sum "${output_fname}" > "${output_fname}.sha256") 40 | -------------------------------------------------------------------------------- /resource/addr.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/goss-org/goss/system" 9 | "github.com/goss-org/goss/util" 10 | ) 11 | 12 | type Addr struct { 13 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 14 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 15 | id string `json:"-" yaml:"-"` 16 | Address string `json:"address,omitempty" yaml:"address,omitempty"` 17 | LocalAddress string `json:"local-address,omitempty" yaml:"local-address,omitempty"` 18 | Reachable matcher `json:"reachable" yaml:"reachable"` 19 | Timeout int `json:"timeout" yaml:"timeout"` 20 | Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` 21 | } 22 | 23 | type idKey struct{} 24 | 25 | const ( 26 | AddrResourceKey = "addr" 27 | AddResourceName = "Addr" 28 | ) 29 | 30 | func init() { 31 | registerResource(AddrResourceKey, &Addr{}) 32 | } 33 | 34 | func (a *Addr) ID() string { 35 | if a.Address != "" && a.Address != a.id { 36 | return fmt.Sprintf("%s: %s", a.id, a.Address) 37 | } 38 | return a.id 39 | } 40 | func (a *Addr) SetID(id string) { a.id = id } 41 | func (a *Addr) SetSkip() { a.Skip = true } 42 | func (a *Addr) TypeKey() string { return AddrResourceKey } 43 | func (a *Addr) TypeName() string { return AddResourceName } 44 | 45 | // FIXME: Can this be refactored? 46 | func (a *Addr) GetTitle() string { return a.Title } 47 | func (a *Addr) GetMeta() meta { return a.Meta } 48 | func (a *Addr) GetAddress() string { 49 | if a.Address != "" { 50 | return a.Address 51 | } 52 | return a.id 53 | } 54 | 55 | func (a *Addr) Validate(sys *system.System) []TestResult { 56 | ctx := context.WithValue(context.Background(), idKey{}, a.ID()) 57 | skip := a.Skip 58 | 59 | if a.Timeout == 0 { 60 | a.Timeout = 500 61 | } 62 | 63 | sysAddr := sys.NewAddr(ctx, a.GetAddress(), sys, util.Config{Timeout: time.Duration(a.Timeout) * time.Millisecond, LocalAddress: a.LocalAddress}) 64 | 65 | var results []TestResult 66 | results = append(results, ValidateValue(a, "reachable", a.Reachable, sysAddr.Reachable, skip)) 67 | return results 68 | } 69 | 70 | func NewAddr(sysAddr system.Addr, config util.Config) (*Addr, error) { 71 | address := sysAddr.Address() 72 | reachable, err := sysAddr.Reachable() 73 | a := &Addr{ 74 | id: address, 75 | Reachable: reachable, 76 | Timeout: config.TimeOutMilliSeconds(), 77 | LocalAddress: config.LocalAddress, 78 | } 79 | return a, err 80 | } 81 | -------------------------------------------------------------------------------- /resource/gossfile.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/goss-org/goss/system" 5 | "github.com/goss-org/goss/util" 6 | ) 7 | 8 | type Gossfile struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | Path string `json:"-" yaml:"-"` 12 | Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` 13 | File string `json:"file,omitempty" yaml:"file,omitempty"` 14 | } 15 | 16 | const ( 17 | GossFileResourceKey = "gossfile" 18 | GossFileResourceName = "Gossfile" 19 | ) 20 | 21 | func init() { 22 | registerResource(GossFileResourceKey, &Gossfile{}) 23 | } 24 | 25 | func (g *Gossfile) ID() string { return g.Path } 26 | func (g *Gossfile) SetID(id string) { g.Path = id } 27 | func (g *Gossfile) SetSkip() {} 28 | func (g *Gossfile) TypeKey() string { return GossFileResourceKey } 29 | func (g *Gossfile) TypeName() string { return GossFileResourceName } 30 | 31 | func (g *Gossfile) GetTitle() string { return g.Title } 32 | func (g *Gossfile) GetMeta() meta { return g.Meta } 33 | 34 | func (g *Gossfile) GetSkip() bool { return g.Skip } 35 | 36 | func (g *Gossfile) GetGossfile() string { 37 | if g.File != "" { 38 | return g.File 39 | } 40 | return g.Path 41 | } 42 | 43 | func (g *Gossfile) Validate(sys *system.System) []TestResult { 44 | return []TestResult{} 45 | } 46 | 47 | func NewGossfile(sysGossfile system.Gossfile, config util.Config) (*Gossfile, error) { 48 | path := sysGossfile.Path() 49 | return &Gossfile{ 50 | Path: path, 51 | }, nil 52 | } 53 | -------------------------------------------------------------------------------- /resource/group.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/goss-org/goss/system" 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type Group struct { 12 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 13 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 14 | id string `json:"-" yaml:"-"` 15 | Groupname string `json:"groupname,omitempty" yaml:"groupname,omitempty"` 16 | Exists matcher `json:"exists" yaml:"exists"` 17 | GID matcher `json:"gid,omitempty" yaml:"gid,omitempty"` 18 | Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` 19 | } 20 | 21 | const ( 22 | GroupResourceKey = "group" 23 | GroupResourceName = "Group" 24 | ) 25 | 26 | func init() { 27 | registerResource(GroupResourceKey, &Group{}) 28 | } 29 | 30 | func (g *Group) ID() string { 31 | if g.Groupname != "" && g.Groupname != g.id { 32 | return fmt.Sprintf("%s: %s", g.id, g.Groupname) 33 | } 34 | return g.id 35 | } 36 | func (g *Group) SetID(id string) { g.id = id } 37 | func (g *Group) SetSkip() { g.Skip = true } 38 | func (g *Group) TypeKey() string { return GroupResourceKey } 39 | func (g *Group) TypeName() string { return GroupResourceName } 40 | func (g *Group) GetTitle() string { return g.Title } 41 | func (g *Group) GetMeta() meta { return g.Meta } 42 | func (g *Group) GetGroupname() string { 43 | if g.Groupname != "" { 44 | return g.Groupname 45 | } 46 | return g.id 47 | } 48 | 49 | func (g *Group) Validate(sys *system.System) []TestResult { 50 | ctx := context.WithValue(context.Background(), idKey{}, g.ID()) 51 | skip := g.Skip 52 | sysgroup := sys.NewGroup(ctx, g.GetGroupname(), sys, util.Config{}) 53 | 54 | var results []TestResult 55 | results = append(results, ValidateValue(g, "exists", g.Exists, sysgroup.Exists, skip)) 56 | if shouldSkip(results) { 57 | skip = true 58 | } 59 | if g.GID != nil { 60 | gGID := deprecateAtoI(g.GID, fmt.Sprintf("%s: group.gid", g.ID())) 61 | results = append(results, ValidateValue(g, "gid", gGID, sysgroup.GID, skip)) 62 | } 63 | return results 64 | } 65 | 66 | func NewGroup(sysGroup system.Group, config util.Config) (*Group, error) { 67 | groupname := sysGroup.Groupname() 68 | exists, _ := sysGroup.Exists() 69 | g := &Group{ 70 | id: groupname, 71 | Exists: exists, 72 | } 73 | if !contains(config.IgnoreList, "stderr") { 74 | if gid, err := sysGroup.GID(); err == nil { 75 | g.GID = gid 76 | } 77 | } 78 | return g, nil 79 | } 80 | -------------------------------------------------------------------------------- /resource/interface.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/goss-org/goss/system" 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type Interface struct { 12 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 13 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 14 | id string `json:"-" yaml:"-"` 15 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 16 | Exists matcher `json:"exists" yaml:"exists"` 17 | Addrs matcher `json:"addrs,omitempty" yaml:"addrs,omitempty"` 18 | MTU matcher `json:"mtu,omitempty" yaml:"mtu,omitempty"` 19 | Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` 20 | } 21 | 22 | const ( 23 | InterfaceResourceKey = "interface" 24 | InterfaceResourceName = "Interface" 25 | ) 26 | 27 | func init() { 28 | registerResource(InterfaceResourceKey, &Interface{}) 29 | } 30 | 31 | func (i *Interface) ID() string { 32 | if i.Name != "" && i.Name != i.id { 33 | return fmt.Sprintf("%s: %s", i.id, i.Name) 34 | } 35 | return i.id 36 | } 37 | func (i *Interface) SetID(id string) { i.id = id } 38 | func (i *Interface) SetSkip() { i.Skip = true } 39 | func (i *Interface) TypeKey() string { return InterfaceResourceKey } 40 | func (i *Interface) TypeName() string { return InterfaceResourceName } 41 | 42 | // FIXME: Can this be refactored? 43 | func (i *Interface) GetTitle() string { return i.Title } 44 | func (i *Interface) GetMeta() meta { return i.Meta } 45 | func (i *Interface) GetName() string { 46 | if i.Name != "" { 47 | return i.Name 48 | } 49 | return i.id 50 | } 51 | 52 | func (i *Interface) Validate(sys *system.System) []TestResult { 53 | ctx := context.WithValue(context.Background(), idKey{}, i.ID()) 54 | skip := i.Skip 55 | sysInterface := sys.NewInterface(ctx, i.GetName(), sys, util.Config{}) 56 | 57 | var results []TestResult 58 | results = append(results, ValidateValue(i, "exists", i.Exists, sysInterface.Exists, skip)) 59 | if shouldSkip(results) { 60 | skip = true 61 | } 62 | if i.Addrs != nil { 63 | results = append(results, ValidateValue(i, "addrs", i.Addrs, sysInterface.Addrs, skip)) 64 | } 65 | if i.MTU != nil { 66 | results = append(results, ValidateValue(i, "mtu", i.MTU, sysInterface.MTU, skip)) 67 | } 68 | return results 69 | } 70 | 71 | func NewInterface(sysInterface system.Interface, config util.Config) (*Interface, error) { 72 | name := sysInterface.Name() 73 | exists, _ := sysInterface.Exists() 74 | i := &Interface{ 75 | id: name, 76 | Exists: exists, 77 | } 78 | if !contains(config.IgnoreList, "addrs") { 79 | if addrs, err := sysInterface.Addrs(); err == nil { 80 | i.Addrs = addrs 81 | } 82 | } 83 | if !contains(config.IgnoreList, "mtu") { 84 | if mtu, err := sysInterface.MTU(); err == nil { 85 | i.MTU = mtu 86 | } 87 | } 88 | return i, nil 89 | } 90 | -------------------------------------------------------------------------------- /resource/kernel_param.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/goss-org/goss/system" 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type KernelParam struct { 12 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 13 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 14 | id string `json:"-" yaml:"-"` 15 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 16 | Key string `json:"-" yaml:"-"` 17 | Value matcher `json:"value" yaml:"value"` 18 | Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` 19 | } 20 | 21 | const ( 22 | KernelParamResourceKey = "kernel-param" 23 | KernelParamResourceName = "KernelParam" 24 | ) 25 | 26 | func init() { 27 | registerResource(KernelParamResourceKey, &KernelParam{}) 28 | } 29 | 30 | func (k *KernelParam) ID() string { 31 | if k.Name != "" && k.Name != k.id { 32 | return fmt.Sprintf("%s: %s", k.id, k.Name) 33 | } 34 | return k.id 35 | } 36 | func (a *KernelParam) SetID(id string) { a.id = id } 37 | 38 | func (a *KernelParam) SetSkip() { a.Skip = true } 39 | func (a *KernelParam) TypeKey() string { return KernelParamResourceKey } 40 | func (a *KernelParam) TypeName() string { return KernelParamResourceName } 41 | 42 | // FIXME: Can this be refactored? 43 | func (k *KernelParam) GetTitle() string { return k.Title } 44 | func (k *KernelParam) GetMeta() meta { return k.Meta } 45 | func (k *KernelParam) GetName() string { 46 | if k.Name != "" { 47 | return k.Name 48 | } 49 | return k.id 50 | } 51 | 52 | func (k *KernelParam) Validate(sys *system.System) []TestResult { 53 | ctx := context.WithValue(context.Background(), idKey{}, k.ID()) 54 | skip := k.Skip 55 | sysKernelParam := sys.NewKernelParam(ctx, k.GetName(), sys, util.Config{}) 56 | 57 | var results []TestResult 58 | results = append(results, ValidateValue(k, "value", k.Value, sysKernelParam.Value, skip)) 59 | return results 60 | } 61 | 62 | func NewKernelParam(sysKernelParam system.KernelParam, config util.Config) (*KernelParam, error) { 63 | key := sysKernelParam.Key() 64 | value, err := sysKernelParam.Value() 65 | a := &KernelParam{ 66 | id: key, 67 | Value: value, 68 | } 69 | return a, err 70 | } 71 | -------------------------------------------------------------------------------- /resource/package.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/goss-org/goss/system" 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type Package struct { 12 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 13 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 14 | id string `json:"-" yaml:"-"` 15 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 16 | Installed matcher `json:"installed" yaml:"installed"` 17 | Versions matcher `json:"versions,omitempty" yaml:"versions,omitempty"` 18 | Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` 19 | } 20 | 21 | const ( 22 | PackageResourceKey = "package" 23 | PackageResourceName = "Package" 24 | ) 25 | 26 | func init() { 27 | registerResource(PackageResourceKey, &Package{}) 28 | } 29 | 30 | func (p *Package) ID() string { 31 | if p.Name != "" && p.Name != p.id { 32 | return fmt.Sprintf("%s: %s", p.id, p.Name) 33 | } 34 | return p.id 35 | } 36 | func (p *Package) SetID(id string) { p.id = id } 37 | func (p *Package) SetSkip() { p.Skip = true } 38 | func (p *Package) TypeKey() string { return PackageResourceKey } 39 | func (p *Package) TypeName() string { return PackageResourceName } 40 | func (p *Package) GetTitle() string { return p.Title } 41 | func (p *Package) GetMeta() meta { return p.Meta } 42 | func (p *Package) GetName() string { 43 | if p.Name != "" { 44 | return p.Name 45 | } 46 | return p.id 47 | } 48 | 49 | func (p *Package) Validate(sys *system.System) []TestResult { 50 | ctx := context.WithValue(context.Background(), idKey{}, p.ID()) 51 | skip := p.Skip 52 | sysPkg := sys.NewPackage(ctx, p.GetName(), sys, util.Config{}) 53 | 54 | var results []TestResult 55 | results = append(results, ValidateValue(p, "installed", p.Installed, sysPkg.Installed, skip)) 56 | if shouldSkip(results) { 57 | skip = true 58 | } 59 | if p.Versions != nil { 60 | results = append(results, ValidateValue(p, "version", p.Versions, sysPkg.Versions, skip)) 61 | } 62 | return results 63 | } 64 | 65 | func NewPackage(sysPackage system.Package, config util.Config) (*Package, error) { 66 | name := sysPackage.Name() 67 | installed, _ := sysPackage.Installed() 68 | p := &Package{ 69 | id: name, 70 | Installed: installed, 71 | } 72 | if !contains(config.IgnoreList, "versions") { 73 | if versions, err := sysPackage.Versions(); err == nil { 74 | p.Versions = versions 75 | } 76 | } 77 | return p, nil 78 | } 79 | -------------------------------------------------------------------------------- /resource/port.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/goss-org/goss/system" 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type Port struct { 12 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 13 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 14 | id string `json:"-" yaml:"-"` 15 | Port string `json:"port,omitempty" yaml:"port,omitempty"` 16 | Listening matcher `json:"listening" yaml:"listening"` 17 | IP matcher `json:"ip,omitempty" yaml:"ip,omitempty"` 18 | Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` 19 | } 20 | 21 | const ( 22 | PortResourceKey = "port" 23 | PortResourceName = "Port" 24 | ) 25 | 26 | func init() { 27 | registerResource(PortResourceKey, &Port{}) 28 | } 29 | 30 | func (p *Port) ID() string { 31 | if p.Port != "" && p.Port != p.id { 32 | return fmt.Sprintf("%s: %s", p.id, p.Port) 33 | } 34 | return p.id 35 | } 36 | func (p *Port) SetID(id string) { p.id = id } 37 | func (p *Port) SetSkip() { p.Skip = true } 38 | func (p *Port) TypeKey() string { return PortResourceKey } 39 | func (p *Port) TypeName() string { return PortResourceName } 40 | func (p *Port) GetTitle() string { return p.Title } 41 | func (p *Port) GetMeta() meta { return p.Meta } 42 | func (p *Port) GetPort() string { 43 | if p.Port != "" { 44 | return p.Port 45 | } 46 | return p.id 47 | } 48 | 49 | func (p *Port) Validate(sys *system.System) []TestResult { 50 | ctx := context.WithValue(context.Background(), idKey{}, p.ID()) 51 | skip := p.Skip 52 | sysPort := sys.NewPort(ctx, p.GetPort(), sys, util.Config{}) 53 | 54 | var results []TestResult 55 | results = append(results, ValidateValue(p, "listening", p.Listening, sysPort.Listening, skip)) 56 | if shouldSkip(results) { 57 | skip = true 58 | } 59 | if p.IP != nil { 60 | results = append(results, ValidateValue(p, "ip", p.IP, sysPort.IP, skip)) 61 | } 62 | return results 63 | } 64 | 65 | func NewPort(sysPort system.Port, config util.Config) (*Port, error) { 66 | port := sysPort.Port() 67 | listening, _ := sysPort.Listening() 68 | p := &Port{ 69 | id: port, 70 | Listening: listening, 71 | } 72 | if !contains(config.IgnoreList, "ip") { 73 | if ip, err := sysPort.IP(); err == nil { 74 | p.IP = ip 75 | } 76 | } 77 | return p, nil 78 | } 79 | -------------------------------------------------------------------------------- /resource/process.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/goss-org/goss/system" 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type Process struct { 12 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 13 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 14 | id string `json:"-" yaml:"-"` 15 | Comm string `json:"comm,omitempty" yaml:"comm,omitempty"` 16 | Running matcher `json:"running" yaml:"running"` 17 | Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` 18 | } 19 | 20 | const ( 21 | ProcessResourceKey = "process" 22 | ProcessResourceName = "Process" 23 | ) 24 | 25 | func init() { 26 | registerResource(ProcessResourceKey, &Process{}) 27 | } 28 | 29 | func (p *Process) ID() string { 30 | if p.Comm != "" && p.Comm != p.id { 31 | return fmt.Sprintf("%s: %s", p.id, p.Comm) 32 | } 33 | return p.id 34 | } 35 | func (p *Process) SetID(id string) { p.id = id } 36 | func (p *Process) SetSkip() { p.Skip = true } 37 | func (p *Process) TypeKey() string { return ProcessResourceKey } 38 | func (p *Process) TypeName() string { return ProcessResourceName } 39 | func (p *Process) GetTitle() string { return p.Title } 40 | func (p *Process) GetMeta() meta { return p.Meta } 41 | func (p *Process) GetComm() string { 42 | if p.Comm != "" { 43 | return p.Comm 44 | } 45 | return p.id 46 | } 47 | 48 | func (p *Process) Validate(sys *system.System) []TestResult { 49 | ctx := context.WithValue(context.Background(), idKey{}, p.ID()) 50 | skip := p.Skip 51 | sysProcess := sys.NewProcess(ctx, p.GetComm(), sys, util.Config{}) 52 | 53 | var results []TestResult 54 | results = append(results, ValidateValue(p, "running", p.Running, sysProcess.Running, skip)) 55 | return results 56 | } 57 | 58 | func NewProcess(sysProcess system.Process, config util.Config) (*Process, error) { 59 | executable := sysProcess.Executable() 60 | running, err := sysProcess.Running() 61 | if err != nil { 62 | return nil, err 63 | } 64 | return &Process{ 65 | id: executable, 66 | Running: running, 67 | }, nil 68 | } 69 | -------------------------------------------------------------------------------- /resource/resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "sync" 9 | 10 | "github.com/goss-org/goss/system" 11 | ) 12 | 13 | type Resource interface { 14 | Validate(sys *system.System) []TestResult 15 | SetID(string) 16 | SetSkip() 17 | TypeKey() string 18 | TypeName() string 19 | } 20 | 21 | var ( 22 | resourcesMu sync.Mutex 23 | resources = map[string]Resource{} 24 | ) 25 | 26 | func registerResource(key string, resource Resource) { 27 | resourcesMu.Lock() 28 | resources[key] = resource 29 | resourcesMu.Unlock() 30 | } 31 | 32 | func Resources() map[string]Resource { 33 | return resources 34 | } 35 | 36 | type ResourceRead interface { 37 | ID() string 38 | GetTitle() string 39 | GetMeta() meta 40 | } 41 | 42 | type matcher any 43 | type meta map[string]any 44 | 45 | func contains(a []string, s string) bool { 46 | for _, e := range a { 47 | if m, _ := filepath.Match(e, s); m { 48 | return true 49 | } 50 | } 51 | return false 52 | } 53 | 54 | func deprecateAtoI(depr any, desc string) any { 55 | s, ok := depr.(string) 56 | if !ok { 57 | return depr 58 | } 59 | fmt.Fprintf(os.Stderr, "DEPRECATION WARNING: %s should be an integer not a string\n", desc) 60 | i, err := strconv.Atoi(s) 61 | if err != nil { 62 | panic(err) 63 | } 64 | return float64(i) 65 | } 66 | 67 | func shouldSkip(results []TestResult) bool { 68 | if len(results) < 1 { 69 | return false 70 | } 71 | if results[0].Err != nil || results[0].Result != SUCCESS || results[0].MatcherResult.Actual == false { 72 | return true 73 | } 74 | return false 75 | } 76 | 77 | func isSet(i interface{}) bool { 78 | switch v := i.(type) { 79 | case []interface{}: 80 | return len(v) > 0 81 | default: 82 | return i != nil 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /resource/service.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/goss-org/goss/system" 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type Service struct { 12 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 13 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 14 | id string `json:"-" yaml:"-"` 15 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 16 | Enabled matcher `json:"enabled" yaml:"enabled"` 17 | Running matcher `json:"running" yaml:"running"` 18 | Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` 19 | RunLevels matcher `json:"runlevels,omitempty" yaml:"runlevels,omitempty"` 20 | } 21 | 22 | const ( 23 | ServiceResourceKey = "service" 24 | ServiceResourceName = "Service" 25 | ) 26 | 27 | func init() { 28 | registerResource(ServiceResourceKey, &Service{}) 29 | } 30 | 31 | func (s *Service) ID() string { 32 | if s.Name != "" && s.Name != s.id { 33 | return fmt.Sprintf("%s: %s", s.id, s.Name) 34 | } 35 | return s.id 36 | } 37 | func (s *Service) SetID(id string) { s.id = id } 38 | func (s *Service) SetSkip() { s.Skip = true } 39 | func (s *Service) TypeKey() string { return ServiceResourceKey } 40 | func (s *Service) TypeName() string { return ServiceResourceName } 41 | func (s *Service) GetTitle() string { return s.Title } 42 | func (s *Service) GetMeta() meta { return s.Meta } 43 | func (s *Service) GetName() string { 44 | if s.Name != "" { 45 | return s.Name 46 | } 47 | return s.id 48 | } 49 | 50 | func (s *Service) Validate(sys *system.System) []TestResult { 51 | ctx := context.WithValue(context.Background(), idKey{}, s.ID()) 52 | skip := s.Skip 53 | sysservice := sys.NewService(ctx, s.GetName(), sys, util.Config{}) 54 | 55 | var results []TestResult 56 | if s.Enabled != nil { 57 | results = append(results, ValidateValue(s, "enabled", s.Enabled, sysservice.Enabled, skip)) 58 | } 59 | if s.Running != nil { 60 | results = append(results, ValidateValue(s, "running", s.Running, sysservice.Running, skip)) 61 | } 62 | if s.RunLevels != nil { 63 | results = append(results, ValidateValue(s, "runlevels", s.RunLevels, sysservice.RunLevels, skip)) 64 | } 65 | return results 66 | } 67 | 68 | func NewService(sysService system.Service, config util.Config) (*Service, error) { 69 | service := sysService.Service() 70 | enabled, err := sysService.Enabled() 71 | if err != nil { 72 | return nil, err 73 | } 74 | running, err := sysService.Running() 75 | if err != nil { 76 | return nil, err 77 | } 78 | return &Service{ 79 | id: service, 80 | Enabled: enabled, 81 | Running: running, 82 | }, nil 83 | } 84 | -------------------------------------------------------------------------------- /system/addr.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "strings" 7 | "time" 8 | 9 | "github.com/goss-org/goss/util" 10 | ) 11 | 12 | type Addr interface { 13 | Address() string 14 | Exists() (bool, error) 15 | Reachable() (bool, error) 16 | } 17 | 18 | type DefAddr struct { 19 | address string 20 | LocalAddress string 21 | Timeout int 22 | } 23 | 24 | func NewDefAddr(_ context.Context, address string, system *System, config util.Config) Addr { 25 | addr := normalizeAddress(address) 26 | return &DefAddr{ 27 | address: addr, 28 | LocalAddress: config.LocalAddress, 29 | Timeout: config.TimeOutMilliSeconds(), 30 | } 31 | } 32 | 33 | func (a *DefAddr) ID() string { 34 | return a.address 35 | } 36 | func (a *DefAddr) Address() string { 37 | return a.address 38 | } 39 | func (a *DefAddr) Exists() (bool, error) { return a.Reachable() } 40 | 41 | func (a *DefAddr) Reachable() (bool, error) { 42 | network, address := splitAddress(a.address) 43 | 44 | var localAddr net.Addr 45 | if network == "udp" { 46 | localAddr = &net.UDPAddr{IP: net.ParseIP(a.LocalAddress)} 47 | } else { 48 | localAddr = &net.TCPAddr{IP: net.ParseIP(a.LocalAddress)} 49 | } 50 | d := net.Dialer{LocalAddr: localAddr, Timeout: time.Duration(a.Timeout) * time.Millisecond} 51 | conn, err := d.Dial(network, address) 52 | if err != nil { 53 | return false, nil 54 | } 55 | conn.Close() 56 | return true, nil 57 | } 58 | 59 | func splitAddress(fulladdress string) (network, address string) { 60 | split := strings.SplitN(fulladdress, "://", 2) 61 | if len(split) == 2 { 62 | return split[0], split[1] 63 | } 64 | return "tcp", fulladdress 65 | } 66 | 67 | func normalizeAddress(fulladdress string) string { 68 | net, addr := splitAddress(fulladdress) 69 | return net + "://" + addr 70 | } 71 | -------------------------------------------------------------------------------- /system/command.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os/exec" 9 | "time" 10 | 11 | "github.com/goss-org/goss/util" 12 | ) 13 | 14 | type Command interface { 15 | Command() string 16 | Exists() (bool, error) 17 | ExitStatus() (int, error) 18 | Stdout() (io.Reader, error) 19 | Stderr() (io.Reader, error) 20 | } 21 | 22 | type DefCommand struct { 23 | Ctx context.Context 24 | command string 25 | exitStatus int 26 | stdout io.Reader 27 | stderr io.Reader 28 | loaded bool 29 | Timeout int 30 | err error 31 | } 32 | 33 | func NewDefCommand(ctx context.Context, command string, system *System, config util.Config) Command { 34 | return &DefCommand{ 35 | Ctx: ctx, 36 | command: command, 37 | Timeout: config.TimeOutMilliSeconds(), 38 | } 39 | } 40 | 41 | func (c *DefCommand) setup() error { 42 | if c.loaded { 43 | return c.err 44 | } 45 | c.loaded = true 46 | 47 | cmd := commandWrapper(c.command) 48 | err := runCommand(cmd, c.Timeout) 49 | 50 | // We don't care about ExitError since it's covered by status 51 | if _, ok := err.(*exec.ExitError); !ok { 52 | c.err = err 53 | } 54 | c.exitStatus = cmd.Status 55 | stdoutB := cmd.Stdout.Bytes() 56 | stderrB := cmd.Stderr.Bytes() 57 | id := c.Ctx.Value("id") 58 | logBytes(stdoutB, fmt.Sprintf("[Command][%s][stdout] ", id)) 59 | logBytes(stderrB, fmt.Sprintf("[Command][%s][stderr] ", id)) 60 | c.stdout = bytes.NewReader(stdoutB) 61 | c.stderr = bytes.NewReader(stderrB) 62 | 63 | return c.err 64 | } 65 | 66 | func (c *DefCommand) Command() string { 67 | return c.command 68 | } 69 | 70 | func (c *DefCommand) ExitStatus() (int, error) { 71 | err := c.setup() 72 | 73 | return c.exitStatus, err 74 | } 75 | 76 | func (c *DefCommand) Stdout() (io.Reader, error) { 77 | err := c.setup() 78 | 79 | return c.stdout, err 80 | } 81 | 82 | func (c *DefCommand) Stderr() (io.Reader, error) { 83 | err := c.setup() 84 | 85 | return c.stderr, err 86 | } 87 | 88 | // Stub out 89 | func (c *DefCommand) Exists() (bool, error) { 90 | return false, nil 91 | } 92 | 93 | func runCommand(cmd *util.Command, timeout int) error { 94 | c1 := make(chan bool, 1) 95 | e1 := make(chan error, 1) 96 | timeoutD := time.Duration(timeout) * time.Millisecond 97 | go func() { 98 | err := cmd.Run() 99 | if err != nil { 100 | e1 <- err 101 | } 102 | c1 <- true 103 | }() 104 | select { 105 | case <-c1: 106 | return nil 107 | case err := <-e1: 108 | return err 109 | case <-time.After(timeoutD): 110 | return fmt.Errorf("Command execution timed out (%s)", timeoutD) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /system/command_posix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || !windows 2 | // +build linux darwin !windows 3 | 4 | package system 5 | 6 | import "github.com/goss-org/goss/util" 7 | 8 | const linuxShell string = "sh" 9 | 10 | func commandWrapper(cmd string) *util.Command { 11 | return util.NewCommand(linuxShell, "-c", cmd) 12 | } 13 | -------------------------------------------------------------------------------- /system/command_posix_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || !windows 2 | // +build linux darwin !windows 3 | 4 | package system 5 | 6 | import ( 7 | "os/exec" 8 | "testing" 9 | ) 10 | 11 | func TestCommandWrapper(t *testing.T) { 12 | t.Parallel() 13 | 14 | c := commandWrapper("echo hello world") 15 | cmdPath, _ := exec.LookPath(linuxShell) 16 | if c.Cmd.Path != cmdPath { 17 | t.Errorf("Command not wrapped properly for OS. got %s, want: %s", c.Cmd.Path, cmdPath) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /system/command_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package system 5 | 6 | import "github.com/goss-org/goss/util" 7 | 8 | const windowsShell string = "cmd" 9 | 10 | func commandWrapper(cmd string) *util.Command { 11 | return util.NewCommandForWindowsCmd(windowsShell, "/c", cmd) 12 | } 13 | -------------------------------------------------------------------------------- /system/command_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package system 5 | 6 | import ( 7 | "os/exec" 8 | "testing" 9 | ) 10 | 11 | func TestCommandWrapper(t *testing.T) { 12 | t.Parallel() 13 | 14 | c := commandWrapper("echo hello world") 15 | cmdPath, _ := exec.LookPath(windowsShell) 16 | if c.Cmd.Path != cmdPath { 17 | t.Errorf("Command not wrapped properly for Windows os. got %s, want: %s", c.Cmd.Path, cmdPath) 18 | } 19 | 20 | if c.Cmd.SysProcAttr.CmdLine != "/c echo hello world" { 21 | t.Errorf("Command not wrapped properly for Windows cmd.exe. got %s, want: %s", c.Cmd.SysProcAttr.CmdLine, "/c echo hello world") 22 | } 23 | 24 | if len(c.Cmd.Args) != 1 { 25 | t.Errorf("Args length should be blank. got: %d, want: %d", len(c.Cmd.Args), 1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /system/dns_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseServerString(t *testing.T) { 8 | 9 | tables := []struct { 10 | x string 11 | n string 12 | }{ 13 | {"127.0.0.1", "127.0.0.1:53"}, 14 | {"127.0.0.1:53", "127.0.0.1:53"}, 15 | {"127.0.0.1:8600", "127.0.0.1:8600"}, 16 | {"1.1.1.1:53", "1.1.1.1:53"}, 17 | } 18 | 19 | for _, table := range tables { 20 | output := parseServerString(table.x) 21 | if output != table.n { 22 | t.Errorf("parseServerString (%s) was incorrect, got: %s, want: %s.", table.x, output, table.n) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /system/file_posix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || !windows 2 | // +build linux darwin !windows 3 | 4 | package system 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "strconv" 10 | "syscall" 11 | ) 12 | 13 | func (f *DefFile) Mode() (string, error) { 14 | mode, err := f.getFileInfo(func(fi os.FileInfo) string { 15 | stat := fi.Sys().(*syscall.Stat_t) 16 | return fmt.Sprintf("%04o", (stat.Mode & 07777)) 17 | }) 18 | if err != nil { 19 | return "", err 20 | } 21 | 22 | return mode, nil 23 | } 24 | 25 | func (f *DefFile) Owner() (string, error) { 26 | uidS, err := f.getFileInfo(func(fi os.FileInfo) string { 27 | return fmt.Sprint(fi.Sys().(*syscall.Stat_t).Uid) 28 | }) 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | uid, err := strconv.Atoi(uidS) 34 | if err != nil { 35 | return "", err 36 | } 37 | return getUserForUid(uid) 38 | } 39 | 40 | func (f *DefFile) Uid() (int, error) { 41 | uidS, err := f.getFileInfo(func(fi os.FileInfo) string { 42 | return fmt.Sprint(fi.Sys().(*syscall.Stat_t).Uid) 43 | }) 44 | if err != nil { 45 | return -1, err 46 | } 47 | 48 | uid, err := strconv.Atoi(uidS) 49 | if err != nil { 50 | return -1, err 51 | } 52 | return uid, nil 53 | } 54 | 55 | func (f *DefFile) Group() (string, error) { 56 | gidS, err := f.getFileInfo(func(fi os.FileInfo) string { 57 | return fmt.Sprint(fi.Sys().(*syscall.Stat_t).Gid) 58 | }) 59 | if err != nil { 60 | return "", err 61 | } 62 | 63 | gid, err := strconv.Atoi(gidS) 64 | if err != nil { 65 | return "", err 66 | } 67 | return getGroupForGid(gid) 68 | } 69 | 70 | func (f *DefFile) Gid() (int, error) { 71 | gidS, err := f.getFileInfo(func(fi os.FileInfo) string { 72 | return fmt.Sprint(fi.Sys().(*syscall.Stat_t).Gid) 73 | }) 74 | if err != nil { 75 | return -1, err 76 | } 77 | 78 | gid, err := strconv.Atoi(gidS) 79 | if err != nil { 80 | return -1, err 81 | } 82 | return gid, nil 83 | } 84 | 85 | func (f *DefFile) getFileInfo(selectorFunc func(os.FileInfo) string) (string, error) { 86 | if err := f.setup(); err != nil { 87 | return "", err 88 | } 89 | 90 | fi, err := os.Lstat(f.realPath) 91 | if err != nil { 92 | return "", err 93 | } 94 | return selectorFunc(fi), nil 95 | } 96 | -------------------------------------------------------------------------------- /system/file_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package system 5 | 6 | func (f *DefFile) Mode() (string, error) { 7 | return "-1", nil // not applicable on Windows 8 | } 9 | 10 | func (f *DefFile) Owner() (string, error) { 11 | return "-1", nil // not applicable on Windows 12 | } 13 | 14 | func (f *DefFile) Uid() (int, error) { 15 | return -1, nil // not applicable on Windows 16 | } 17 | 18 | func (f *DefFile) Group() (string, error) { 19 | return "-1", nil // not applicable on Windows 20 | } 21 | 22 | func (f *DefFile) Gid() (int, error) { 23 | return -1, nil // not applicable on Windows 24 | } 25 | -------------------------------------------------------------------------------- /system/gossfile.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/goss-org/goss/util" 7 | ) 8 | 9 | type Gossfile interface { 10 | Path() string 11 | Exists() (bool, error) 12 | } 13 | 14 | type DefGossfile struct { 15 | path string 16 | } 17 | 18 | func (g *DefGossfile) Path() string { 19 | return g.path 20 | } 21 | 22 | // Stub out 23 | func (g *DefGossfile) Exists() (bool, error) { 24 | return false, nil 25 | } 26 | 27 | func NewDefGossfile(_ context.Context, path string, system *System, config util.Config) Gossfile { 28 | return &DefGossfile{path: path} 29 | } 30 | -------------------------------------------------------------------------------- /system/group.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "os/user" 6 | "strconv" 7 | 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type Group interface { 12 | Groupname() string 13 | Exists() (bool, error) 14 | GID() (int, error) 15 | } 16 | 17 | type DefGroup struct { 18 | groupname string 19 | } 20 | 21 | func NewDefGroup(_ context.Context, groupname string, system *System, config util.Config) Group { 22 | return &DefGroup{groupname: groupname} 23 | } 24 | 25 | func (u *DefGroup) Groupname() string { 26 | return u.groupname 27 | } 28 | 29 | func (u *DefGroup) Exists() (bool, error) { 30 | _, err := user.LookupGroup(u.groupname) 31 | if err != nil { 32 | return false, nil 33 | } 34 | return true, nil 35 | } 36 | 37 | func (u *DefGroup) GID() (int, error) { 38 | group, err := user.LookupGroup(u.groupname) 39 | if err != nil { 40 | return 0, err 41 | } 42 | 43 | gid, err := strconv.Atoi(group.Gid) 44 | if err != nil { 45 | return 0, err 46 | } 47 | 48 | return gid, nil 49 | } 50 | -------------------------------------------------------------------------------- /system/interface.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/goss-org/goss/util" 8 | ) 9 | 10 | type Interface interface { 11 | Name() string 12 | Exists() (bool, error) 13 | Addrs() ([]string, error) 14 | MTU() (int, error) 15 | } 16 | 17 | type DefInterface struct { 18 | name string 19 | loaded bool 20 | exists bool 21 | iface *net.Interface 22 | err error 23 | } 24 | 25 | func NewDefInterface(_ context.Context, name string, systei *System, config util.Config) Interface { 26 | return &DefInterface{ 27 | name: name, 28 | } 29 | } 30 | 31 | func (i *DefInterface) setup() error { 32 | if i.loaded { 33 | return i.err 34 | } 35 | i.loaded = true 36 | 37 | iface, err := net.InterfaceByName(i.name) 38 | if err != nil { 39 | i.exists = false 40 | i.err = err 41 | return i.err 42 | } 43 | i.iface = iface 44 | i.exists = true 45 | return nil 46 | } 47 | 48 | func (i *DefInterface) ID() string { 49 | return i.name 50 | } 51 | 52 | func (i *DefInterface) Name() string { 53 | return i.name 54 | } 55 | 56 | func (i *DefInterface) Exists() (bool, error) { 57 | if err := i.setup(); err != nil { 58 | return false, nil 59 | } 60 | 61 | return i.exists, nil 62 | } 63 | 64 | func (i *DefInterface) Addrs() ([]string, error) { 65 | if err := i.setup(); err != nil { 66 | return nil, err 67 | } 68 | 69 | addrs, err := i.iface.Addrs() 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | var ret []string 75 | for _, addr := range addrs { 76 | ret = append(ret, addr.String()) 77 | } 78 | return ret, nil 79 | } 80 | 81 | func (i *DefInterface) MTU() (int, error) { 82 | if err := i.setup(); err != nil { 83 | return 0, err 84 | } 85 | 86 | return i.iface.MTU, nil 87 | } 88 | -------------------------------------------------------------------------------- /system/kernel_param.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/achanda/go-sysctl" 7 | "github.com/goss-org/goss/util" 8 | ) 9 | 10 | type KernelParam interface { 11 | Key() string 12 | Exists() (bool, error) 13 | Value() (string, error) 14 | } 15 | 16 | type DefKernelParam struct { 17 | key string 18 | } 19 | 20 | func NewDefKernelParam(_ context.Context, key string, system *System, config util.Config) KernelParam { 21 | return &DefKernelParam{ 22 | key: key, 23 | } 24 | } 25 | 26 | func (k *DefKernelParam) ID() string { 27 | return k.key 28 | } 29 | 30 | func (k *DefKernelParam) Key() string { 31 | return k.key 32 | } 33 | 34 | func (k *DefKernelParam) Exists() (bool, error) { 35 | if _, err := k.Value(); err != nil { 36 | return false, nil 37 | } 38 | return true, nil 39 | } 40 | 41 | func (k *DefKernelParam) Value() (string, error) { 42 | return sysctl.Get(k.key) 43 | } 44 | -------------------------------------------------------------------------------- /system/log.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | ) 7 | 8 | func logBytes(b []byte, prefix string) { 9 | if len(b) == 0 { 10 | return 11 | } 12 | lines := bytes.Split(b, []byte("\n")) 13 | for _, l := range lines { 14 | log.Printf("[DEBUG]%s %s", prefix, l) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /system/mount_posix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || !windows 2 | // +build linux darwin !windows 3 | 4 | package system 5 | 6 | import ( 7 | "math" 8 | "syscall" 9 | ) 10 | 11 | func getUsage(mountpoint string) (int, error) { 12 | statfsOut := &syscall.Statfs_t{} 13 | err := syscall.Statfs(mountpoint, statfsOut) 14 | if err != nil { 15 | return -1, err 16 | } 17 | 18 | percentageFree := float64(statfsOut.Bfree) / float64(statfsOut.Blocks) 19 | usage := math.Round((1 - percentageFree) * 100) 20 | 21 | return int(usage), nil 22 | } 23 | -------------------------------------------------------------------------------- /system/mount_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestSplitMountInfo(t *testing.T) { 10 | in := "rw,context=\"system_u:object_r:container_file_t:s0:c174,c741\",size=65536k,mode=755" 11 | want := []string{ 12 | "rw", 13 | "context=\"system_u:object_r:container_file_t:s0:c174,c741\"", 14 | "size=65536k", 15 | "mode=755", 16 | } 17 | 18 | got := splitMountInfo(in) 19 | 20 | assert.DeepEqual(t, got, want) 21 | } 22 | -------------------------------------------------------------------------------- /system/mount_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package system 5 | 6 | import "errors" 7 | 8 | func getUsage(mountpoint string) (int, error) { 9 | return 0, errors.New("Not implemented") 10 | } 11 | -------------------------------------------------------------------------------- /system/package.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/goss-org/goss/util" 8 | ) 9 | 10 | type Package interface { 11 | Name() string 12 | Exists() (bool, error) 13 | Installed() (bool, error) 14 | Versions() ([]string, error) 15 | } 16 | 17 | var ErrNullPackage = errors.New("Could not detect Package type on this system, please use --package flag to explicity set it") 18 | 19 | type NullPackage struct { 20 | name string 21 | } 22 | 23 | func NewNullPackage(_ context.Context, name string, system *System, config util.Config) Package { 24 | return &NullPackage{name: name} 25 | } 26 | 27 | func (p *NullPackage) Name() string { return p.name } 28 | 29 | func (p *NullPackage) Exists() (bool, error) { return p.Installed() } 30 | 31 | func (p *NullPackage) Installed() (bool, error) { 32 | return false, ErrNullPackage 33 | } 34 | 35 | func (p *NullPackage) Versions() ([]string, error) { 36 | return nil, ErrNullPackage 37 | } 38 | -------------------------------------------------------------------------------- /system/package_alpine.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type AlpinePackage struct { 12 | name string 13 | versions []string 14 | loaded bool 15 | installed bool 16 | } 17 | 18 | func NewAlpinePackage(_ context.Context, name string, system *System, config util.Config) Package { 19 | return &AlpinePackage{name: name} 20 | } 21 | 22 | func (p *AlpinePackage) setup() { 23 | if p.loaded { 24 | return 25 | } 26 | p.loaded = true 27 | cmd := util.NewCommand("apk", "version", p.name) 28 | if err := cmd.Run(); err != nil { 29 | return 30 | } 31 | for _, l := range strings.Split(strings.TrimSpace(cmd.Stdout.String()), "\n") { 32 | if strings.HasPrefix(l, "Installed:") || strings.HasPrefix(l, "WARNING") { 33 | continue 34 | } 35 | ver := strings.TrimPrefix(strings.Fields(l)[0], p.name+"-") 36 | p.versions = append(p.versions, ver) 37 | } 38 | 39 | if len(p.versions) > 0 { 40 | p.installed = true 41 | } 42 | } 43 | 44 | func (p *AlpinePackage) Name() string { 45 | return p.name 46 | } 47 | 48 | func (p *AlpinePackage) Exists() (bool, error) { return p.Installed() } 49 | 50 | func (p *AlpinePackage) Installed() (bool, error) { 51 | p.setup() 52 | 53 | return p.installed, nil 54 | } 55 | 56 | func (p *AlpinePackage) Versions() ([]string, error) { 57 | p.setup() 58 | if len(p.versions) == 0 { 59 | return p.versions, errors.New("Package version not found") 60 | } 61 | return p.versions, nil 62 | } 63 | -------------------------------------------------------------------------------- /system/package_deb.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type DebPackage struct { 12 | name string 13 | versions []string 14 | loaded bool 15 | installed bool 16 | } 17 | 18 | func NewDebPackage(_ context.Context, name string, system *System, config util.Config) Package { 19 | return &DebPackage{name: name} 20 | } 21 | 22 | func (p *DebPackage) setup() { 23 | if p.loaded { 24 | return 25 | } 26 | p.loaded = true 27 | cmd := util.NewCommand("dpkg-query", "-f", "${Status} ${Version}\n", "-W", p.name) 28 | if err := cmd.Run(); err != nil { 29 | return 30 | } 31 | for _, l := range strings.Split(strings.TrimSpace(cmd.Stdout.String()), "\n") { 32 | if !(strings.HasPrefix(l, "install ok installed") || strings.HasPrefix(l, "hold ok installed")) { 33 | continue 34 | } 35 | ver := strings.Fields(l)[3] 36 | p.versions = append(p.versions, ver) 37 | } 38 | 39 | if len(p.versions) > 0 { 40 | p.installed = true 41 | } 42 | } 43 | 44 | func (p *DebPackage) Name() string { 45 | return p.name 46 | } 47 | 48 | func (p *DebPackage) Exists() (bool, error) { return p.Installed() } 49 | 50 | func (p *DebPackage) Installed() (bool, error) { 51 | p.setup() 52 | 53 | return p.installed, nil 54 | } 55 | 56 | func (p *DebPackage) Versions() ([]string, error) { 57 | p.setup() 58 | if len(p.versions) == 0 { 59 | return p.versions, errors.New("Package version not found") 60 | } 61 | return p.versions, nil 62 | } 63 | -------------------------------------------------------------------------------- /system/package_pacman.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type PacmanPackage struct { 12 | name string 13 | versions []string 14 | loaded bool 15 | installed bool 16 | } 17 | 18 | func NewPacmanPackage(_ context.Context, name string, system *System, config util.Config) Package { 19 | return &PacmanPackage{name: name} 20 | } 21 | 22 | func (p *PacmanPackage) setup() { 23 | if p.loaded { 24 | return 25 | } 26 | p.loaded = true 27 | // TODO: extract versions 28 | cmd := util.NewCommand("pacman", "-Q", "--color", "never", "--noconfirm", p.name) 29 | if err := cmd.Run(); err != nil { 30 | return 31 | } 32 | p.installed = true 33 | // the output format is "pkgname version\n", so if we split the string on 34 | // whitespace, the version is the second item. 35 | p.versions = []string{strings.Fields(cmd.Stdout.String())[1]} 36 | } 37 | 38 | func (p *PacmanPackage) Name() string { 39 | return p.name 40 | } 41 | 42 | func (p *PacmanPackage) Exists() (bool, error) { return p.Installed() } 43 | 44 | func (p *PacmanPackage) Installed() (bool, error) { 45 | p.setup() 46 | 47 | return p.installed, nil 48 | } 49 | 50 | func (p *PacmanPackage) Versions() ([]string, error) { 51 | p.setup() 52 | if len(p.versions) == 0 { 53 | return p.versions, errors.New("Package version not found") 54 | } 55 | return p.versions, nil 56 | } 57 | -------------------------------------------------------------------------------- /system/package_rpm.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type RpmPackage struct { 12 | name string 13 | versions []string 14 | loaded bool 15 | installed bool 16 | } 17 | 18 | func NewRpmPackage(_ context.Context, name string, system *System, config util.Config) Package { 19 | return &RpmPackage{name: name} 20 | } 21 | 22 | func (p *RpmPackage) setup() { 23 | if p.loaded { 24 | return 25 | } 26 | p.loaded = true 27 | cmd := util.NewCommand("rpm", "-q", "--nosignature", "--nohdrchk", "--nodigest", "--qf", "%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\n", p.name) 28 | if err := cmd.Run(); err != nil { 29 | return 30 | } 31 | p.installed = true 32 | p.versions = strings.Split(strings.TrimSpace(cmd.Stdout.String()), "\n") 33 | } 34 | 35 | func (p *RpmPackage) Name() string { 36 | return p.name 37 | } 38 | 39 | func (p *RpmPackage) Exists() (bool, error) { return p.Installed() } 40 | 41 | func (p *RpmPackage) Installed() (bool, error) { 42 | p.setup() 43 | 44 | return p.installed, nil 45 | } 46 | 47 | func (p *RpmPackage) Versions() ([]string, error) { 48 | p.setup() 49 | if len(p.versions) == 0 { 50 | return p.versions, errors.New("Package version not found") 51 | } 52 | return p.versions, nil 53 | } 54 | -------------------------------------------------------------------------------- /system/package_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIsSupportedPackageManager(t *testing.T) { 8 | if IsSupportedPackageManager("na") { 9 | t.Fatal("na should not be a valid package manager") 10 | } 11 | 12 | if !IsSupportedPackageManager("rpm") { 13 | t.Fatal("rpm should be a valid package manager") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /system/process.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/goss-org/go-ps" 7 | "github.com/goss-org/goss/util" 8 | ) 9 | 10 | type Process interface { 11 | Executable() string 12 | Exists() (bool, error) 13 | Running() (bool, error) 14 | Pids() ([]int, error) 15 | } 16 | 17 | type DefProcess struct { 18 | executable string 19 | procMap map[string][]ps.Process 20 | err error 21 | } 22 | 23 | func NewDefProcess(_ context.Context, executable string, system *System, config util.Config) Process { 24 | pmap, err := system.ProcMap() 25 | return &DefProcess{ 26 | executable: executable, 27 | procMap: pmap, 28 | err: err, 29 | } 30 | } 31 | 32 | func (p *DefProcess) Executable() string { 33 | return p.executable 34 | } 35 | 36 | func (p *DefProcess) Exists() (bool, error) { return p.Running() } 37 | 38 | func (p *DefProcess) Pids() ([]int, error) { 39 | var pids []int 40 | if p.err != nil { 41 | return pids, p.err 42 | } 43 | for _, proc := range p.procMap[p.executable] { 44 | pids = append(pids, proc.Pid()) 45 | } 46 | return pids, nil 47 | } 48 | 49 | func (p *DefProcess) Running() (bool, error) { 50 | if p.err != nil { 51 | return false, p.err 52 | } 53 | if _, ok := p.procMap[p.executable]; ok { 54 | return true, nil 55 | } 56 | return false, nil 57 | } 58 | 59 | func GetProcs() (map[string][]ps.Process, error) { 60 | pmap := make(map[string][]ps.Process) 61 | processes, err := ps.Processes() 62 | if err != nil { 63 | return pmap, err 64 | } 65 | for _, p := range processes { 66 | pmap[p.Executable()] = append(pmap[p.Executable()], p) 67 | } 68 | 69 | return pmap, nil 70 | } 71 | -------------------------------------------------------------------------------- /system/service.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import "strings" 4 | 5 | type Service interface { 6 | Service() string 7 | Exists() (bool, error) 8 | Enabled() (bool, error) 9 | Running() (bool, error) 10 | RunLevels() ([]string, error) 11 | } 12 | 13 | func invalidService(s string) bool { 14 | return strings.ContainsRune(s, '/') 15 | } 16 | -------------------------------------------------------------------------------- /system/service_init.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | 10 | "github.com/goss-org/goss/util" 11 | ) 12 | 13 | type ServiceInit struct { 14 | service string 15 | alpine bool 16 | runlevel string 17 | } 18 | 19 | func NewServiceInit(_ context.Context, service string, system *System, config util.Config) Service { 20 | return &ServiceInit{service: service} 21 | } 22 | 23 | func NewAlpineServiceInit(_ context.Context, service string, system *System, config util.Config) Service { 24 | runlevel := config.RunLevel 25 | if runlevel == "" { 26 | runlevel = "sysinit" 27 | } 28 | return &ServiceInit{service: service, alpine: true, runlevel: runlevel} 29 | } 30 | 31 | func (s *ServiceInit) Service() string { 32 | return s.service 33 | } 34 | 35 | func (s *ServiceInit) Exists() (bool, error) { 36 | if invalidService(s.service) { 37 | return false, nil 38 | } 39 | if _, err := os.Stat(fmt.Sprintf("/etc/init.d/%s", s.service)); err == nil { 40 | return true, err 41 | } 42 | return false, nil 43 | } 44 | 45 | func (s *ServiceInit) Enabled() (bool, error) { 46 | if invalidService(s.service) { 47 | return false, nil 48 | } 49 | var runLevels []string 50 | var err error 51 | if s.alpine { 52 | runLevels, err = alpineServiceRunLevels(s.service) 53 | } else { 54 | runLevels, err = initServiceRunLevels(s.service) 55 | } 56 | return len(runLevels) != 0, err 57 | } 58 | 59 | func (s *ServiceInit) RunLevels() ([]string, error) { 60 | if invalidService(s.service) { 61 | return nil, nil 62 | } 63 | if s.alpine { 64 | return alpineServiceRunLevels(s.service) 65 | } else { 66 | return initServiceRunLevels(s.service) 67 | } 68 | } 69 | 70 | func (s *ServiceInit) Running() (bool, error) { 71 | if invalidService(s.service) { 72 | return false, nil 73 | } 74 | cmd := util.NewCommand("service", s.service, "status") 75 | cmd.Run() 76 | if cmd.Status == 0 { 77 | return true, cmd.Err 78 | } 79 | return false, nil 80 | } 81 | 82 | func initServiceRunLevels(service string) ([]string, error) { 83 | var runLevels []string 84 | matches, err := filepath.Glob(fmt.Sprintf("/etc/rc*.d/S[0-9][0-9]%s", service)) 85 | if err != nil { 86 | return nil, err 87 | } 88 | re := regexp.MustCompile("/etc/rc([0-9]+).d/") 89 | for _, m := range matches { 90 | matches := re.FindStringSubmatch(m) 91 | if matches != nil { 92 | runLevels = append(runLevels, matches[1]) 93 | } 94 | } 95 | return runLevels, nil 96 | } 97 | 98 | func alpineServiceRunLevels(service string) ([]string, error) { 99 | var runLevels []string 100 | matches, err := filepath.Glob(fmt.Sprintf("/etc/runlevels/*/%s", service)) 101 | if err != nil { 102 | return nil, err 103 | } 104 | re := regexp.MustCompile("/etc/runlevels/([^/]+)") 105 | for _, m := range matches { 106 | matches := re.FindStringSubmatch(m) 107 | if matches != nil { 108 | runLevels = append(runLevels, matches[1]) 109 | } 110 | } 111 | return runLevels, nil 112 | } 113 | -------------------------------------------------------------------------------- /system/service_systemd.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type ServiceSystemd struct { 12 | service string 13 | legacy bool 14 | } 15 | 16 | func NewServiceSystemd(_ context.Context, service string, system *System, config util.Config) Service { 17 | return &ServiceSystemd{ 18 | service: service, 19 | } 20 | } 21 | 22 | func NewServiceSystemdLegacy(_ context.Context, service string, system *System, config util.Config) Service { 23 | return &ServiceSystemd{ 24 | service: service, 25 | legacy: true, 26 | } 27 | } 28 | 29 | func (s *ServiceSystemd) Service() string { 30 | return s.service 31 | } 32 | 33 | func (s *ServiceSystemd) Exists() (bool, error) { 34 | if invalidService(s.service) { 35 | return false, nil 36 | } 37 | cmd := util.NewCommand("systemctl", "-q", "list-unit-files", "--type=service") 38 | cmd.Run() 39 | if strings.Contains(cmd.Stdout.String(), fmt.Sprintf("%s.service", s.service)) { 40 | return true, cmd.Err 41 | } 42 | if s.legacy { 43 | // Fallback on sysv 44 | sysv := &ServiceInit{service: s.service} 45 | if e, err := sysv.Exists(); e && err == nil { 46 | return true, nil 47 | } 48 | } 49 | return false, nil 50 | } 51 | 52 | func (s *ServiceSystemd) Enabled() (bool, error) { 53 | if invalidService(s.service) { 54 | return false, nil 55 | } 56 | cmd := util.NewCommand("systemctl", "-q", "is-enabled", s.service) 57 | cmd.Run() 58 | if cmd.Status == 0 { 59 | return true, cmd.Err 60 | } 61 | if s.legacy { 62 | // Fallback on sysv 63 | sysv := &ServiceInit{service: s.service} 64 | if en, err := sysv.Enabled(); en && err == nil { 65 | return true, nil 66 | } 67 | } 68 | return false, nil 69 | } 70 | 71 | func (s *ServiceSystemd) Running() (bool, error) { 72 | if invalidService(s.service) { 73 | return false, nil 74 | } 75 | cmd := util.NewCommand("systemctl", "-q", "is-active", s.service) 76 | cmd.Run() 77 | if cmd.Status == 0 { 78 | return true, cmd.Err 79 | } 80 | if s.legacy { 81 | // Fallback on sysv 82 | sysv := &ServiceInit{service: s.service} 83 | if r, err := sysv.Running(); r && err == nil { 84 | return true, nil 85 | } 86 | } 87 | return false, nil 88 | } 89 | 90 | func (s *ServiceSystemd) RunLevels() ([]string, error) { 91 | return nil, nil 92 | } 93 | -------------------------------------------------------------------------------- /system/service_upstart.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "os" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/goss-org/goss/util" 12 | ) 13 | 14 | type ServiceUpstart struct { 15 | service string 16 | } 17 | 18 | var upstartEnabled = regexp.MustCompile(`^\s*start on`) 19 | var upstartDisabled = regexp.MustCompile(`^manual`) 20 | 21 | func NewServiceUpstart(_ context.Context, service string, system *System, config util.Config) Service { 22 | return &ServiceUpstart{service: service} 23 | } 24 | 25 | func (s *ServiceUpstart) Service() string { 26 | return s.service 27 | } 28 | 29 | func (s *ServiceUpstart) Exists() (bool, error) { 30 | // upstart 31 | if _, err := os.Stat(fmt.Sprintf("/etc/init/%s.conf", s.service)); err == nil { 32 | return true, nil 33 | } 34 | // Fallback on sysv 35 | sysv := &ServiceInit{service: s.service} 36 | if e, err := sysv.Exists(); e && err == nil { 37 | return true, nil 38 | } 39 | return false, nil 40 | } 41 | 42 | func (s *ServiceUpstart) Enabled() (bool, error) { 43 | if fh, err := os.Open(fmt.Sprintf("/etc/init/%s.override", s.service)); err == nil { 44 | scanner := bufio.NewScanner(fh) 45 | for scanner.Scan() { 46 | line := scanner.Text() 47 | if upstartDisabled.MatchString(line) { 48 | return false, nil 49 | } 50 | } 51 | } 52 | 53 | // If no /etc/init/.override with `manual` keyword in it has been found 54 | // Check the contents of the upstart manifest. 55 | if fh, err := os.Open(fmt.Sprintf("/etc/init/%s.conf", s.service)); err == nil { 56 | scanner := bufio.NewScanner(fh) 57 | for scanner.Scan() { 58 | line := scanner.Text() 59 | if upstartEnabled.MatchString(line) { 60 | return true, nil 61 | } 62 | } 63 | } 64 | // Fallback on sysv 65 | sysv := &ServiceInit{service: s.service} 66 | if en, err := sysv.Enabled(); en && err == nil { 67 | return true, nil 68 | } 69 | return false, nil 70 | } 71 | 72 | func (s *ServiceUpstart) Running() (bool, error) { 73 | cmd := util.NewCommand("service", s.service, "status") 74 | cmd.Run() 75 | out := cmd.Stdout.String() 76 | if cmd.Status == 0 && (strings.Contains(out, "running") || strings.Contains(out, "online")) { 77 | return true, cmd.Err 78 | } 79 | return false, nil 80 | } 81 | func (s *ServiceUpstart) RunLevels() ([]string, error) { 82 | sysv := &ServiceInit{service: s.service} 83 | return sysv.RunLevels() 84 | } 85 | -------------------------------------------------------------------------------- /system/system_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "reflect" 5 | "runtime" 6 | "testing" 7 | ) 8 | 9 | type noInputs func() string 10 | 11 | // test that a function with no inputs returns one of the expected strings 12 | func testOutputs(f noInputs, validOutputs []string, t *testing.T) { 13 | output := f() 14 | // use reflect to get the name of the function 15 | name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() 16 | failed := true 17 | for _, valid := range validOutputs { 18 | if output == valid { 19 | failed = false 20 | } 21 | } 22 | if failed { 23 | t.Errorf("Function %v returned %v, which is not one of %v", name, output, validOutputs) 24 | } 25 | } 26 | 27 | func TestPackageManager(t *testing.T) { 28 | t.Parallel() 29 | testOutputs( 30 | DetectPackageManager, 31 | []string{"dpkg", "rpm", "apk", "pacman", ""}, 32 | t, 33 | ) 34 | } 35 | 36 | func TestDetectService(t *testing.T) { 37 | t.Parallel() 38 | testOutputs( 39 | DetectService, 40 | []string{"systemd", "init", "alpineinit", "upstart", ""}, 41 | t, 42 | ) 43 | } 44 | 45 | func TestDetectDistro(t *testing.T) { 46 | t.Parallel() 47 | testOutputs( 48 | DetectDistro, 49 | []string{"ubuntu", "redhat", "alpine", "arch", "debian", ""}, 50 | t, 51 | ) 52 | } 53 | 54 | func TestHasCommand(t *testing.T) { 55 | t.Parallel() 56 | if !HasCommand("sh") { 57 | t.Error("System didn't have sh!") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /system/user.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "os/user" 6 | "strconv" 7 | 8 | "github.com/goss-org/goss/util" 9 | ) 10 | 11 | type User interface { 12 | Username() string 13 | Exists() (bool, error) 14 | UID() (int, error) 15 | GID() (int, error) 16 | Groups() ([]string, error) 17 | Home() (string, error) 18 | Shell() (string, error) 19 | } 20 | 21 | type DefUser struct { 22 | username string 23 | } 24 | 25 | func NewDefUser(_ context.Context, username string, system *System, config util.Config) User { 26 | return &DefUser{username: username} 27 | } 28 | 29 | func (u *DefUser) Username() string { 30 | return u.username 31 | } 32 | 33 | func (u *DefUser) Exists() (bool, error) { 34 | _, err := user.Lookup(u.username) 35 | if err != nil { 36 | return false, nil 37 | } 38 | return true, nil 39 | } 40 | 41 | func (u *DefUser) UID() (int, error) { 42 | user, err := user.Lookup(u.username) 43 | if err != nil { 44 | return 0, err 45 | } 46 | 47 | uid, err := strconv.Atoi(user.Uid) 48 | if err != nil { 49 | return 0, err 50 | } 51 | 52 | return uid, nil 53 | } 54 | 55 | func (u *DefUser) GID() (int, error) { 56 | user, err := user.Lookup(u.username) 57 | if err != nil { 58 | return 0, err 59 | } 60 | 61 | gid, err := strconv.Atoi(user.Gid) 62 | if err != nil { 63 | return 0, err 64 | } 65 | 66 | return gid, nil 67 | } 68 | 69 | func (u *DefUser) Home() (string, error) { 70 | user, err := user.Lookup(u.username) 71 | if err != nil { 72 | return "", err 73 | } 74 | 75 | return user.HomeDir, nil 76 | } 77 | -------------------------------------------------------------------------------- /system/user_group_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris 3 | 4 | package system 5 | 6 | import ( 7 | "bufio" 8 | "io" 9 | "os" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | func groupsForUser(user string, pgid int, grp io.Reader) ([]string, error) { 16 | s := bufio.NewScanner(grp) 17 | out := []string{} 18 | 19 | for s.Scan() { 20 | if err := s.Err(); err != nil { 21 | return nil, err 22 | } 23 | 24 | text := s.Text() 25 | if text == "" { 26 | continue 27 | } 28 | 29 | // see: man 5 group 30 | // group_name:password:GID:user_list 31 | // Name:Pass:Gid:List 32 | // root:x:0:root 33 | // adm:x:4:root,adm,daemon 34 | parts := strings.Split(text, ":") 35 | if len(parts) != 4 { 36 | continue 37 | } 38 | 39 | gid, err := strconv.Atoi(parts[2]) 40 | if err == nil { 41 | if gid == pgid { 42 | out = append(out, parts[0]) 43 | continue 44 | } 45 | } 46 | 47 | for _, g := range strings.Split(parts[3], ",") { 48 | if g == user { 49 | out = append(out, parts[0]) 50 | continue 51 | } 52 | } 53 | } 54 | 55 | sort.Strings(out) 56 | 57 | return out, nil 58 | } 59 | 60 | func (u *DefUser) Groups() ([]string, error) { 61 | grp, err := os.Open("/etc/group") 62 | if err != nil { 63 | return nil, err 64 | } 65 | defer grp.Close() 66 | 67 | pgid, err := u.GID() 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return groupsForUser(u.username, pgid, grp) 73 | } 74 | -------------------------------------------------------------------------------- /system/user_group_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris 3 | 4 | package system 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestGroupsForUser(t *testing.T) { 12 | grp := `badline 13 | testgrp1:*:100:bob,jack,jill 14 | testgrp2:*:101:bob,jack 15 | testgrp3:*:102:jill 16 | testgrp4:*:103:` 17 | 18 | var cases = []struct { 19 | user string 20 | gid int 21 | expect []string 22 | }{ 23 | {"bob", 100, []string{"testgrp1", "testgrp2"}}, 24 | {"jack", 100, []string{"testgrp1", "testgrp2"}}, 25 | {"jill", 103, []string{"testgrp1", "testgrp3", "testgrp4"}}, 26 | {"other", 103, []string{"testgrp4"}}, 27 | {"other", 105, []string{}}, 28 | } 29 | 30 | for _, c := range cases { 31 | res, err := groupsForUser(c.user, c.gid, strings.NewReader(grp)) 32 | if err != nil { 33 | t.Fatalf("unexpected error: %v", err) 34 | } 35 | if len(res) != len(c.expect) { 36 | t.Fatalf("result %#v does not match %#v", res, c.expect) 37 | } 38 | for i, e := range c.expect { 39 | if res[i] != e { 40 | t.Fatalf("result %#v does not match %#v", res, c.expect) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /system/user_group_windows.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "os/user" 6 | "sort" 7 | ) 8 | 9 | func (u *DefUser) Groups() ([]string, error) { 10 | usr, err := user.Lookup(u.username) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | var groupList []string 16 | ids, err := usr.GroupIds() 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | for _, gid := range ids { 22 | group, err := user.LookupGroupId(gid) 23 | if err != nil { 24 | return nil, fmt.Errorf("Unable to find groups for user %v: %v", usr.Username, err) 25 | } 26 | groupList = append(groupList, group.Name) 27 | } 28 | 29 | sort.Strings(groupList) 30 | return groupList, nil 31 | } 32 | -------------------------------------------------------------------------------- /system/user_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris 3 | 4 | package system 5 | 6 | import ( 7 | "bufio" 8 | "fmt" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | func (u *DefUser) Shell() (string, error) { 14 | passwd, err := os.Open("/etc/passwd") 15 | if err != nil { 16 | return "", err 17 | } 18 | defer passwd.Close() 19 | 20 | lines := bufio.NewReader(passwd) 21 | 22 | for { 23 | line, _, err := lines.ReadLine() 24 | if err != nil { 25 | break 26 | } 27 | 28 | fs := strings.Split(string(line), ":") 29 | if len(fs) != 7 { 30 | return "", fmt.Errorf("invalid entry in /etc/passwd") 31 | } 32 | 33 | if fs[0] == u.username { 34 | return fs[6], nil 35 | } 36 | } 37 | 38 | return "", fmt.Errorf("unknown user %s", u.username) 39 | } 40 | -------------------------------------------------------------------------------- /system/user_unsupported.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris 2 | // +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris 3 | 4 | package system 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func (u *DefUser) Shell() (string, error) { 11 | return "", fmt.Errorf("unsupported operating system") 12 | } 13 | -------------------------------------------------------------------------------- /testdata/failing.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | command: 3 | hello world: 4 | exit-status: 1 5 | exec: "echo hello world" 6 | stdout: 7 | - did not echo this 8 | stderr: [] 9 | timeout: 10000 10 | -------------------------------------------------------------------------------- /testdata/out_matching_basic.0.documentation: -------------------------------------------------------------------------------- 1 | Matching: basic_array: matches: matches expectation: ["group1","group2"] 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"}}]} 3 | Matching: basic_int: matches: matches expectation: 42 4 | Matching: basic_reader: matches: matches expectation: ["foo","/^m.*w$/","!ftw","!/^ERROR:/"] 5 | Matching: basic_semver: matches: matches expectation: {"semver-constraint":">=1.2.0"} 6 | Matching: basic_string: matches: matches expectation: "this is a test" 7 | Matching: basic_string_multiline: matches: matches expectation: "this is a test1\nthis is a test2\nthis is a test3\n" 8 | Matching: basic_string_oneline: matches: matches expectation: "this is a test1\n" 9 | Matching: basic_string_regexp: matches: matches expectation: {"match-regexp":"^this"} 10 | Matching: basic_string_skip: matches: skipped 11 | Matching: negated_basic_array: matches: matches expectation: {"not":["group1","group2","group2","group4"]} 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"}}}]} 13 | Matching: negated_basic_int: matches: matches expectation: {"not":43} 14 | Matching: negated_basic_reader: matches: matches expectation: {"not":{"contain-elements":["fox","/^t.*w$/","!foo","!/^foo/"]}} 15 | Matching: negated_basic_string: matches: matches expectation: {"not":"this is a failing test"} 16 | Matching: negated_basic_string_regexp: matches: matches expectation: {"not":{"match-regexp":"^foo"}} 17 | 18 | 19 | Failures/Skipped: 20 | 21 | Matching: basic_string_skip: matches: skipped 22 | 23 | Total Duration: 24 | Count: 16, Failed: 0, Skipped: 1 25 | -------------------------------------------------------------------------------- /testdata/out_matching_basic.0.nagios: -------------------------------------------------------------------------------- 1 | GOSS OK - Count: 16, Failed: 0, Skipped: 1, Duration: 2 | -------------------------------------------------------------------------------- /testdata/out_matching_basic.0.rspecish: -------------------------------------------------------------------------------- 1 | .........S...... 2 | 3 | Failures/Skipped: 4 | 5 | Matching: basic_string_skip: matches: skipped 6 | 7 | Total Duration: 8 | Count: 16, Failed: 0, Skipped: 1 9 | -------------------------------------------------------------------------------- /testdata/out_matching_basic.0.tap: -------------------------------------------------------------------------------- 1 | 1..16 2 | ok 1 - Matching: basic_array: matches: matches expectation: ["group1","group2"] 3 | 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"}}]} 4 | ok 3 - Matching: basic_int: matches: matches expectation: 42 5 | ok 4 - Matching: basic_reader: matches: matches expectation: ["foo","/^m.*w$/","!ftw","!/^ERROR:/"] 6 | ok 5 - Matching: basic_semver: matches: matches expectation: {"semver-constraint":">=1.2.0"} 7 | ok 6 - Matching: basic_string: matches: matches expectation: "this is a test" 8 | ok 7 - Matching: basic_string_multiline: matches: matches expectation: "this is a test1\nthis is a test2\nthis is a test3\n" 9 | ok 8 - Matching: basic_string_oneline: matches: matches expectation: "this is a test1\n" 10 | ok 9 - Matching: basic_string_regexp: matches: matches expectation: {"match-regexp":"^this"} 11 | ok 10 - # SKIP Matching: basic_string_skip: matches: skipped 12 | ok 11 - Matching: negated_basic_array: matches: matches expectation: {"not":["group1","group2","group2","group4"]} 13 | 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"}}}]} 14 | ok 13 - Matching: negated_basic_int: matches: matches expectation: {"not":43} 15 | ok 14 - Matching: negated_basic_reader: matches: matches expectation: {"not":{"contain-elements":["fox","/^t.*w$/","!foo","!/^foo/"]}} 16 | ok 15 - Matching: negated_basic_string: matches: matches expectation: {"not":"this is a failing test"} 17 | ok 16 - Matching: negated_basic_string_regexp: matches: matches expectation: {"not":{"match-regexp":"^foo"}} 18 | -------------------------------------------------------------------------------- /testdata/out_matching_basic_failing.2.nagios: -------------------------------------------------------------------------------- 1 | GOSS CRITICAL - Count: 27, Failed: 27, Skipped: 0, Duration: 2 | -------------------------------------------------------------------------------- /testdata/out_matching_transformers.0.documentation: -------------------------------------------------------------------------------- 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:"}}]} 2 | Matching: test_array: matches: matches expectation: [{"contain-element":{"match-regexp":"4."}},"45",{"and":[{"ge":46},{"le":50}]}] 3 | Matching: test_gjson_have_key_array: matches: matches expectation: {"gjson":{"arr":{"or":[{"contain-elements":[{"have-key":"nested"}]}]}}} 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"}} 5 | Matching: test_gjson_using_this_and_equal: matches: matches expectation: {"gjson":{"@this":{"equal":{"baz":"bing","foo":"bar"}}}} 6 | Matching: test_numeric_string: matches: matches expectation: {"and":["128",{"have-prefix":"1"},{"have-suffix":"8"},{"match-regexp":"\\d{3}"}]} 7 | Matching: test_reader_as_single_string: matches: matches expectation: "cool" 8 | Matching: test_reader_using_array: matches: matches expectation: ["foo bar","15","moo cow"] 9 | Matching: test_reader_using_int_matchers: matches: matches expectation: {"and":[{"le":250},{"ge":20}]} 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"}]}]} 11 | Matching: test_string_float: matches: matches expectation: {"and":[128.3,{"le":129},{"gt":120.2}]} 12 | Matching: test_string_numeric: matches: matches expectation: {"and":[128,128,{"le":128},{"gt":120}]} 13 | 14 | 15 | Total Duration: 16 | Count: 12, Failed: 0, Skipped: 0 17 | -------------------------------------------------------------------------------- /testdata/out_matching_transformers.0.nagios: -------------------------------------------------------------------------------- 1 | GOSS OK - Count: 12, Failed: 0, Skipped: 0, Duration: 2 | -------------------------------------------------------------------------------- /testdata/out_matching_transformers.0.rspecish: -------------------------------------------------------------------------------- 1 | ............ 2 | 3 | Total Duration: 4 | Count: 12, Failed: 0, Skipped: 0 5 | -------------------------------------------------------------------------------- /testdata/out_matching_transformers.0.tap: -------------------------------------------------------------------------------- 1 | 1..12 2 | 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:"}}]} 3 | ok 2 - Matching: test_array: matches: matches expectation: [{"contain-element":{"match-regexp":"4."}},"45",{"and":[{"ge":46},{"le":50}]}] 4 | ok 3 - Matching: test_gjson_have_key_array: matches: matches expectation: {"gjson":{"arr":{"or":[{"contain-elements":[{"have-key":"nested"}]}]}}} 5 | 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"}} 6 | ok 5 - Matching: test_gjson_using_this_and_equal: matches: matches expectation: {"gjson":{"@this":{"equal":{"baz":"bing","foo":"bar"}}}} 7 | ok 6 - Matching: test_numeric_string: matches: matches expectation: {"and":["128",{"have-prefix":"1"},{"have-suffix":"8"},{"match-regexp":"\\d{3}"}]} 8 | ok 7 - Matching: test_reader_as_single_string: matches: matches expectation: "cool" 9 | ok 8 - Matching: test_reader_using_array: matches: matches expectation: ["foo bar","15","moo cow"] 10 | ok 9 - Matching: test_reader_using_int_matchers: matches: matches expectation: {"and":[{"le":250},{"ge":20}]} 11 | 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"}]}]} 12 | ok 11 - Matching: test_string_float: matches: matches expectation: {"and":[128.3,{"le":129},{"gt":120.2}]} 13 | ok 12 - Matching: test_string_numeric: matches: matches expectation: {"and":[128,128,{"le":128},{"gt":120}]} 14 | -------------------------------------------------------------------------------- /testdata/out_matching_transformers_failing.2.nagios: -------------------------------------------------------------------------------- 1 | GOSS CRITICAL - Count: 18, Failed: 18, Skipped: 0, Duration: 2 | -------------------------------------------------------------------------------- /testdata/passing.goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | command: 3 | hello world: 4 | exit-status: 0 5 | exec: "echo hello world" 6 | stdout: 7 | - hello world 8 | stderr: [] 9 | timeout: 10000 10 | -------------------------------------------------------------------------------- /util/build.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | var Version string 4 | -------------------------------------------------------------------------------- /util/command.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | 6 | //"fmt" 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | type Command struct { 12 | name string 13 | Cmd *exec.Cmd 14 | Stdout, Stderr bytes.Buffer 15 | Err error 16 | Status int 17 | } 18 | 19 | func NewCommand(name string, arg ...string) *Command { 20 | //fmt.Println(arg) 21 | command := new(Command) 22 | command.name = name 23 | command.Cmd = exec.Command(name, arg...) 24 | 25 | return command 26 | } 27 | 28 | func (c *Command) Run() error { 29 | c.Cmd.Stdout = &c.Stdout 30 | c.Cmd.Stderr = &c.Stderr 31 | 32 | if _, err := exec.LookPath(c.name); err != nil { 33 | c.Err = err 34 | return c.Err 35 | } 36 | 37 | if err := c.Cmd.Start(); err != nil { 38 | c.Err = err 39 | return c.Err 40 | } 41 | 42 | if err := c.Cmd.Wait(); err != nil { 43 | c.Err = err 44 | if exiterr, ok := err.(*exec.ExitError); ok { 45 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 46 | c.Status = status.ExitStatus() 47 | } 48 | } 49 | } else { 50 | c.Status = 0 51 | } 52 | return c.Err 53 | } 54 | -------------------------------------------------------------------------------- /util/command_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package util 5 | 6 | import ( 7 | "strings" 8 | 9 | //"fmt" 10 | "os/exec" 11 | "syscall" 12 | ) 13 | 14 | func NewCommandForWindowsCmd(name string, arg ...string) *Command { 15 | //fmt.Println(arg) 16 | command := new(Command) 17 | command.name = name 18 | 19 | // cmd.exe has a unique unquoting algorithm 20 | // provide the full command line in SysProcAttr.CmdLine, leaving Args empty. 21 | // more information: https://golang.org/pkg/os/exec/#Command 22 | command.Cmd = exec.Command(name) 23 | command.Cmd.SysProcAttr = &syscall.SysProcAttr{ 24 | HideWindow: false, 25 | CmdLine: strings.Join(arg, " "), 26 | CreationFlags: 0, 27 | } 28 | 29 | return command 30 | } 31 | -------------------------------------------------------------------------------- /util/config_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWithVarsBytes(t *testing.T) { 8 | vs := `{"hello":"world"}` 9 | c, err := NewConfig(WithVarsBytes([]byte(vs))) 10 | if err != nil { 11 | t.Fatal(err.Error()) 12 | } 13 | 14 | if c.VarsInline != vs { 15 | t.Fatalf("expected %q got %q", vs, c.VarsInline) 16 | } 17 | } 18 | 19 | func TestWithVarsString(t *testing.T) { 20 | vs := `{"hello":"world"}` 21 | c, err := NewConfig(WithVarsString(vs)) 22 | if err != nil { 23 | t.Fatal(err.Error()) 24 | } 25 | 26 | if c.VarsInline != vs { 27 | t.Fatalf("expected %q got %q", vs, c.VarsInline) 28 | } 29 | } 30 | 31 | func TestWithVarsFile(t *testing.T) { 32 | c, err := NewConfig(WithVarsFile("/nonexisting")) 33 | if err != nil { 34 | t.Fatal(err.Error()) 35 | } 36 | 37 | if c.Vars != "/nonexisting" { 38 | t.Fatalf("expected '/nonexisting' got %q", c.Vars) 39 | } 40 | } 41 | 42 | func TestWithVarsData(t *testing.T) { 43 | c, err := NewConfig(WithVarsData(map[string]string{"hello": "world"})) 44 | if err != nil { 45 | t.Fatal(err.Error()) 46 | } 47 | 48 | if c.VarsInline != `{"hello":"world"}` { 49 | t.Fatalf("expected %q got %q", `{"hello":"world"}`, c.VarsInline) 50 | } 51 | } 52 | --------------------------------------------------------------------------------