├── .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 |
--------------------------------------------------------------------------------