├── .bazelrc ├── .gcloudignore ├── .github ├── dependabot.yml └── workflows │ ├── bazel.yaml │ ├── ci.bazelrc │ ├── integration-tests.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── BUILD.bazel ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING ├── Dockerfile ├── LICENSE ├── MODULE.bazel ├── MODULE.bazel.lock ├── Makefile ├── README.md ├── RELEASING.md ├── WORKSPACE.bazel ├── bazel ├── BUILD.bazel ├── container_structure_test.bzl ├── docs │ ├── BUILD.bazel │ └── defs.md ├── test │ ├── .bazelrc │ ├── .bazelversion │ ├── BUILD.bazel │ ├── MODULE.bazel │ ├── README.md │ ├── WORKSPACE.bazel │ ├── WORKSPACE.bzlmod │ ├── tarball.tar │ └── test.yaml ├── toolchain.bzl └── toolchains_repo.bzl ├── cmd └── container-structure-test │ ├── app │ ├── cmd │ │ ├── cmd.go │ │ ├── test.go │ │ ├── test │ │ │ └── util.go │ │ ├── test_test.go │ │ └── version.go │ ├── container-structure-test.go │ └── flags │ │ └── template.go │ └── container-structure-test.go ├── defs.bzl ├── deploy ├── Dockerfile ├── Dockerfile.build ├── Dockerfile_debug ├── cloudbuild.yaml ├── config.json ├── release.sh └── release_cloudbuild.yaml ├── go.mod ├── go.sum ├── hack ├── .gofmt.sh ├── checksums.sh ├── run.sh └── tests.bzl ├── internal └── pkgutil │ ├── README.md │ ├── fs_utils.go │ ├── image_utils.go │ ├── tar_utils.go │ └── transport_builder.go ├── pkg ├── color │ ├── formatter.go │ └── formatter_test.go ├── config │ └── options.go ├── drivers │ ├── docker_driver.go │ ├── driver.go │ ├── host_driver.go │ └── tar_driver.go ├── output │ ├── output.go │ └── output_test.go ├── types │ ├── types.go │ ├── unversioned │ │ └── types.go │ ├── v1 │ │ ├── command.go │ │ ├── file_content.go │ │ ├── file_existence.go │ │ ├── licenses.go │ │ └── structure.go │ └── v2 │ │ ├── command.go │ │ ├── file_content.go │ │ ├── file_existence.go │ │ ├── licenses.go │ │ ├── metadata.go │ │ └── structure.go ├── utils │ └── utils.go └── version │ └── version.go ├── repositories.bzl ├── tests ├── .gitignore ├── Dockerfile ├── Dockerfile.cap ├── Dockerfile.metadata ├── Dockerfile.unprivileged ├── amd64 │ ├── ubuntu_22_04_containeropts_env_test.yaml │ ├── ubuntu_22_04_containeropts_envfile_test.yaml │ ├── ubuntu_22_04_containeropts_test.yaml │ ├── ubuntu_22_04_containeropts_user_test.yaml │ ├── ubuntu_22_04_failure_test.yaml │ ├── ubuntu_22_04_metadata_test.yaml │ └── ubuntu_22_04_test.yaml ├── arm64 │ ├── ubuntu_22_04_containeropts_env_test.yaml │ ├── ubuntu_22_04_containeropts_envfile_test.yaml │ ├── ubuntu_22_04_containeropts_test.yaml │ ├── ubuntu_22_04_containeropts_user_test.yaml │ ├── ubuntu_22_04_failure_test.yaml │ ├── ubuntu_22_04_metadata_test.yaml │ └── ubuntu_22_04_test.yaml ├── envfile ├── ppc64le │ ├── ubuntu_22_04_containeropts_env_test.yaml │ ├── ubuntu_22_04_containeropts_envfile_test.yaml │ ├── ubuntu_22_04_containeropts_test.yaml │ ├── ubuntu_22_04_containeropts_user_test.yaml │ ├── ubuntu_22_04_failure_test.yaml │ ├── ubuntu_22_04_metadata_test.yaml │ └── ubuntu_22_04_test.yaml ├── s390x │ ├── ubuntu_22_04_containeropts_env_test.yaml │ ├── ubuntu_22_04_containeropts_envfile_test.yaml │ ├── ubuntu_22_04_containeropts_test.yaml │ ├── ubuntu_22_04_containeropts_user_test.yaml │ ├── ubuntu_22_04_failure_test.yaml │ ├── ubuntu_22_04_metadata_test.yaml │ └── ubuntu_22_04_test.yaml └── structure_test_tests.sh └── testutil └── util.go /.bazelrc: -------------------------------------------------------------------------------- 1 | common --enable_bzlmod 2 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | vendor/github.com/docker/docker/hack 2 | vendor/github.com/docker/docker/project 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: gomod 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | open-pull-requests-limit: 10 13 | - package-ecosystem: github-actions 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | open-pull-requests-limit: 10 18 | -------------------------------------------------------------------------------- /.github/workflows/bazel.yaml: -------------------------------------------------------------------------------- 1 | name: Bazel 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | test: 13 | uses: bazel-contrib/.github/.github/workflows/bazel.yaml@v6 14 | with: 15 | folders: '["bazel/test"]' 16 | exclude_windows: true 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.bazelrc: -------------------------------------------------------------------------------- 1 | # This file contains Bazel settings to apply on CI only. 2 | # It is referenced with a --bazelrc option in the call to bazel in ci.yaml 3 | 4 | # Debug where options came from 5 | build --announce_rc 6 | # This directory is configured in GitHub actions to be persisted between runs. 7 | build --disk_cache=~/.cache/bazel 8 | build --repository_cache=~/.cache/bazel-repo 9 | # Don't rely on test logs being easily accessible from the test runner, 10 | # though it makes the log noisier. 11 | test --test_output=errors 12 | # Allows tests to run bazelisk-in-bazel, since this is the cache folder used 13 | test --test_env=XDG_CACHE_HOME 14 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: PR unit tests 2 | 3 | # Triggers the workflow on push or pull request events 4 | on: [push, pull_request] 5 | 6 | permissions: read-all 7 | 8 | concurrency: 9 | # On master/release, we don't want any jobs cancelled 10 | # On PR branches, we cancel the job if new commits are pushed 11 | # More info: https://stackoverflow.com/a/70972844/1261287 12 | group: ${{ github.ref }} 13 | cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} 14 | 15 | jobs: 16 | build: 17 | name: PR unit tests 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest, macos-12] 21 | fail-fast: false 22 | runs-on: ${{ matrix.os }} 23 | 24 | steps: 25 | - name: start docker 26 | if: ${{ matrix.os == 'macos-12' }} 27 | run: | 28 | brew install docker 29 | colima start 30 | echo "DOCKER_HOST=unix:///Users/runner/.colima/default/docker.sock" >> $GITHUB_ENV 31 | 32 | - name: Set up Go 33 | uses: actions/setup-go@v5 34 | with: 35 | go-version: ^1.22 36 | id: go 37 | 38 | - uses: imjasonh/setup-crane@v0.4 39 | 40 | - name: Check out code into the Go module directory 41 | uses: actions/checkout@v4 42 | with: 43 | fetch-depth: 0 44 | 45 | - name: Run tests 46 | run: | 47 | make test 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | packages: write 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: 'go.mod' 22 | 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v6 25 | with: 26 | distribution: goreleaser 27 | version: latest 28 | args: release --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | structure_test 2 | structure-test 3 | structure_tests.test 4 | .vscode 5 | out/ 6 | bazel-* 7 | dist/ 8 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: container-structure-test 2 | 3 | builds: 4 | - env: 5 | - CGO_ENABLED=0 6 | 7 | main: ./cmd/container-structure-test 8 | 9 | binary: container-structure-test-{{.Os}}-{{.Arch}} 10 | 11 | ldflags: 12 | - -X github.com/GoogleContainerTools/container-structure-test/pkg/version.version={{.Version}} 13 | - -X github.com/GoogleContainerTools/container-structure-test/pkg/version.buildDate={{.Date}} 14 | - -X github.com/GoogleContainerTools/container-structure-test/pkg/version.builtBy=goreleaser 15 | - -X github.com/GoogleContainerTools/container-structure-test/pkg/version.commit={{.Commit}} 16 | 17 | targets: 18 | - darwin_amd64 19 | - darwin_arm64 20 | - linux_amd64 21 | - linux_arm64 22 | - linux_s390x 23 | - linux_ppc64le 24 | - windows_amd64 25 | 26 | no_unique_dist_dir: true 27 | 28 | checksum: 29 | name_template: "checksums.txt" 30 | 31 | archives: 32 | - format: binary 33 | name_template: "{{ .Binary }}" 34 | 35 | kos: 36 | - repository: ghcr.io/googlecontainertools/container-structure-test 37 | base_image: ubuntu:22.04 38 | tags: 39 | - '{{.Version}}' 40 | - latest 41 | bare: true 42 | preserve_import_paths: false 43 | platforms: 44 | - linux/amd64 45 | - linux/arm64 46 | ldflags: 47 | - -X github.com/GoogleContainerTools/container-structure-test/pkg/version.version=12 48 | - -X github.com/GoogleContainerTools/container-structure-test/pkg/version.buildDate={{.Date}} 49 | - -X github.com/GoogleContainerTools/container-structure-test/pkg/version.builtBy=goreleaser 50 | - -X github.com/GoogleContainerTools/container-structure-test/pkg/version.commit={{.Commit}} 51 | 52 | release: 53 | footer: | 54 | ## Container Images 55 | 56 | `ghcr.io/googlecontainertools/container-structure-test:{{.Version}}` 57 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_skylib//:bzl_library.bzl", "bzl_library") 2 | 3 | # For stardoc to reference the file directly 4 | exports_files(["defs.bzl"]) 5 | 6 | bzl_library( 7 | name = "defs", 8 | srcs = ["defs.bzl"], 9 | deps = ["//bazel:container_structure_test"], 10 | visibility = ["//visibility:public"], 11 | ) 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.13.0 Release - 11/03/2022 2 | 3 | Highlights: 4 | * Make EnvVars and UnboundEnvVars consistent for metadataTest (#314) 5 | * Support --image-from-oci-layout (#306) 6 | 7 | Big thanks to everyone who contributed to this release: 8 | * bananaappletw 9 | * thesayyn 10 | 11 | ## Distribution 12 | 13 | container-structure-test is distributed in binary form for Linux (arm64, amd64, s390x, ppc64le), OS X, and Windows systems for the v1.13.0 release, as well as a container image for running tests in Google Cloud Builder. 14 | 15 | Binaries are available on Google Cloud Storage. The direct GCS links are: 16 | [Linux/amd64](https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-linux-amd64) 17 | [Linux/arm64](https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-linux-arm64) 18 | [Linux/s390x](https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-linux-s390x) 19 | [Linux/ppc64le](https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-linux-ppc64le) 20 | [Darwin/amd64](https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-darwin-amd64) 21 | [Windows](https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-windows-amd64.exe) 22 | 23 | The container image can be found at `gcr.io/gcp-runtimes/container-structure-test:v1.13.0`. 24 | 25 | ## Installation 26 | 27 | ### OSX 28 | ```shell 29 | curl -LO https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-darwin-amd64 && mv container-structure-test-darwin-amd64 container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 30 | ``` 31 | Feel free to leave off the `sudo mv container-structure-test /usr/local/bin` if you would like to add container-structure-test to your path manually. 32 | 33 | ### Windows 34 | https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-windows-amd64.exe 35 | 36 | ### Linux 37 | amd64: 38 | ```shell 39 | curl -LO https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-linux-amd64 && mv container-structure-test-linux-amd64 container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 40 | ``` 41 | arm64: 42 | ```shell 43 | curl -LO https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-linux-arm64 && mv container-structure-test-linux-arm64 container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 44 | ``` 45 | s390x: 46 | ```shell 47 | curl -LO https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-linux-s390x && mv container-structure-test-linux-s390x container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 48 | ``` 49 | ppc64le: 50 | ```shell 51 | curl -LO https://storage.googleapis.com/container-structure-test/v1.13.0/container-structure-test-linux-ppc64le && mv container-structure-test-linux-ppc64le container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 52 | ``` 53 | Feel free to leave off the `sudo mv container-structure-test /usr/local/bin` if you would like to add container-structure-test to your path manually. 54 | 55 | ## Usage 56 | Documentation is available [here](https://github.com/GoogleCloudPlatform/container-structure-test/blob/master/README.md) 57 | 58 | # v1.11.0 Release - 11/09/2021 59 | 60 | Highlights: 61 | * Use os.Lstat over os.Stat (#292) 62 | * Add support for the "user" metadata. Related to #80. (#274) 63 | * Move to Go 1.17 to support newer versions of macOS 64 | 65 | Big thanks to everyone who contributed to this release: 66 | * charlyx 67 | * dduportal 68 | * midnightconman 69 | 70 | ## Distribution 71 | 72 | container-structure-test is distributed in binary form for Linux (arm64, amd64, s390x, ppc64le), OS X, and Windows systems for the v1.11.0 release, as well as a container image for running tests in Google Cloud Builder. 73 | 74 | Binaries are available on Google Cloud Storage. The direct GCS links are: 75 | [Linux/amd64](https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-linux-amd64) 76 | [Linux/arm64](https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-linux-arm64) 77 | [Linux/s390x](https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-linux-s390x) 78 | [Linux/ppc64le](https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-linux-ppc64le) 79 | [Darwin/amd64](https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-darwin-amd64) 80 | [Windows](https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-windows-amd64.exe) 81 | 82 | The container image can be found at `gcr.io/gcp-runtimes/container-structure-test:v1.11.0`. 83 | 84 | ## Installation 85 | 86 | ### OSX 87 | ```shell 88 | curl -LO https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-darwin-amd64 && mv container-structure-test-darwin-amd64 container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 89 | ``` 90 | Feel free to leave off the `sudo mv container-structure-test /usr/local/bin` if you would like to add container-structure-test to your path manually. 91 | 92 | ### Windows 93 | https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-windows-amd64.exe 94 | 95 | ### Linux 96 | amd64: 97 | ```shell 98 | curl -LO https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-linux-amd64 && mv container-structure-test-linux-amd64 container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 99 | ``` 100 | arm64: 101 | ```shell 102 | curl -LO https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-linux-arm64 && mv container-structure-test-linux-arm64 container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 103 | ``` 104 | s390x: 105 | ```shell 106 | curl -LO https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-linux-s390x && mv container-structure-test-linux-s390x container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 107 | ``` 108 | ppc64le: 109 | ```shell 110 | curl -LO https://storage.googleapis.com/container-structure-test/v1.11.0/container-structure-test-linux-ppc64le && mv container-structure-test-linux-ppc64le container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 111 | ``` 112 | Feel free to leave off the `sudo mv container-structure-test /usr/local/bin` if you would like to add container-structure-test to your path manually. 113 | 114 | ## Usage 115 | Documentation is available [here](https://github.com/GoogleCloudPlatform/container-structure-test/blob/master/README.md) 116 | 117 | v1.10.0 Release - 01/07/2021 118 | 119 | Highlights: 120 | * :sparkles: Add new output format JUnit [#254](https://github.com/GoogleContainerTools/container-structure-test/pull/254) 121 | * Produce linux/s390x and linux/ppc64le binaries to use in container_test [#269](https://github.com/GoogleContainerTools/container-structure-test/pull/269) 122 | 123 | Big thanks to everyone who contributed to this release: 124 | * barthy1 125 | * charlyx 126 | 127 | ## Distribution 128 | 129 | container-structure-test is distributed in binary form for Linux (arm64 and amd64) and OS X systems for the v1.10.0 release, as well as a container image for running tests in Google Cloud Builder. 130 | 131 | Binaries are available on Google Cloud Storage. The direct GCS links are: 132 | [Darwin/amd64](https://storage.googleapis.com/container-structure-test/v1.10.0/container-structure-test-darwin-amd64) 133 | [Linux/amd64](https://storage.googleapis.com/container-structure-test/v1.10.0/container-structure-test-linux-amd64) 134 | [Linux/arm64](https://storage.googleapis.com/container-structure-test/v1.10.0/container-structure-test-linux-arm64) 135 | 136 | The container image can be found at `gcr.io/gcp-runtimes/container-structure-test:v1.10.0`. 137 | 138 | ## Installation 139 | 140 | ### OSX 141 | ```shell 142 | curl -LO https://storage.googleapis.com/container-structure-test/v1.10.0/container-structure-test-darwin-amd64 && mv container-structure-test-darwin-amd64 container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 143 | ``` 144 | Feel free to leave off the `sudo mv container-structure-test /usr/local/bin` if you would like to add container-structure-test to your path manually. 145 | 146 | ### Linux 147 | amd64: 148 | ```shell 149 | curl -LO https://storage.googleapis.com/container-structure-test/v1.10.0/container-structure-test-linux-amd64 && mv container-structure-test-linux-amd64 container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 150 | ``` 151 | arm64: 152 | ```shell 153 | curl -LO https://storage.googleapis.com/container-structure-test/v1.10.0/container-structure-test-linux-arm64 && mv container-structure-test-linux-arm64 container-structure-test && chmod +x container-structure-test && sudo mv container-structure-test /usr/local/bin/ 154 | ``` 155 | Feel free to leave off the `sudo mv container-structure-test /usr/local/bin` if you would like to add container-structure-test to your path manually. 156 | 157 | ## Usage 158 | Documentation is available [here](https://github.com/GoogleCloudPlatform/container-structure-test/blob/master/README.md) 159 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners file. 2 | # This file controls which users are tagged by default for pull request reviews. 3 | 4 | * @nkubala @tstromberg 5 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your patches! Before we can take them, we have to jump a couple of legal hurdles. 6 | 7 | Please fill out either the individual or corporate Contributor License Agreement (CLA). 8 | 9 | * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](http://code.google.com/legal/individual-cla-v1.0.html). 10 | * If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html). 11 | 12 | Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests. 13 | 14 | ## Contributing A Patch 15 | 16 | 1. Submit an issue describing your proposed change to the repo in question. 17 | 1. The repo owner will respond to your issue promptly. 18 | 1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). 19 | 1. Fork the desired repo, develop and test your code changes. 20 | 1. Submit a pull request. 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | ADD out/container-structure-test /container-structure-test 3 | ENTRYPOINT ["/container-structure-test"] 4 | -------------------------------------------------------------------------------- /MODULE.bazel: -------------------------------------------------------------------------------- 1 | "Bazel module definition, see https://bazel.build/external/overview#bzlmod" 2 | module( 3 | name = "container_structure_test", 4 | compatibility_level = 1, 5 | # Replaced dynamically when published 6 | version = "0.0.0", 7 | ) 8 | 9 | # To run jq 10 | bazel_dep(name = "aspect_bazel_lib", version = "1.28.0") 11 | bazel_dep(name = "bazel_skylib", version = "1.6.1") 12 | bazel_dep(name = "platforms", version = "0.0.9") 13 | 14 | ext = use_extension("//:repositories.bzl", "extension") 15 | use_repo( 16 | ext, 17 | "structure_test_toolchains", 18 | # For testing only 19 | "structure_test_st_darwin_amd64", 20 | "structure_test_st_darwin_arm64", 21 | "structure_test_st_linux_arm64", 22 | "structure_test_st_linux_s390x", 23 | "structure_test_st_linux_amd64", 24 | "structure_test_st_windows_amd64", 25 | ) 26 | 27 | register_toolchains("@structure_test_toolchains//:all") 28 | 29 | # 0.5.4 is the first version with bzlmod support 30 | bazel_dep(name = "stardoc", version = "0.5.4", repo_name = "io_bazel_stardoc", dev_dependency = True) 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All rights reserved. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Bump these on release 16 | VERSION_MAJOR ?= 1 17 | VERSION_MINOR ?= 19 18 | VERSION_BUILD ?= 0 19 | 20 | VERSION ?= v$(VERSION_MAJOR).$(VERSION_MINOR).$(VERSION_BUILD) 21 | 22 | GOOS ?= $(shell go env GOOS) 23 | GOARCH ?= $(shell go env GOARCH) 24 | BUILD_DIR ?= ./out 25 | ORG := github.com/GoogleContainerTools 26 | PROJECT := container-structure-test 27 | REPOPATH ?= $(ORG)/$(PROJECT) 28 | RELEASE_BUCKET ?= $(PROJECT) 29 | DOCKER ?=docker 30 | 31 | VERSION_PACKAGE := $(REPOPATH)/pkg/version 32 | 33 | # If the architecture is not amd64, only create the linux binary 34 | ifeq ($(GOARCH), amd64) 35 | SUPPORTED_PLATFORMS := linux-$(GOARCH) darwin-$(GOARCH) windows-$(GOARCH).exe 36 | else 37 | ifeq ($(GOARCH), arm64) 38 | SUPPORTED_PLATFORMS := linux-$(GOARCH) darwin-$(GOARCH) 39 | else 40 | SUPPORTED_PLATFORMS := linux-$(GOARCH) 41 | endif 42 | endif 43 | 44 | GO_LDFLAGS :=" 45 | GO_LDFLAGS += -X $(VERSION_PACKAGE).version=$(VERSION) 46 | GO_LDFLAGS += -X $(VERSION_PACKAGE).buildDate=$(shell date +'%Y-%m-%dT%H:%M:%SZ') 47 | GO_LDFLAGS +=" 48 | 49 | BUILD_PACKAGE = $(REPOPATH)/cmd/container-structure-test 50 | GO_FILES := $(shell find . -type f -name '*.go' -not -path "./vendor/*") 51 | 52 | $(BUILD_DIR)/$(PROJECT): $(BUILD_DIR)/$(PROJECT)-$(GOOS)-$(GOARCH) 53 | cp $(BUILD_DIR)/$(PROJECT)-$(GOOS)-$(GOARCH) $@ 54 | 55 | $(BUILD_DIR)/$(PROJECT)-%-$(GOARCH): $(GO_FILES) $(BUILD_DIR) 56 | GOOS=$* GOARCH=$(GOARCH) CGO_ENABLED=0 go build -ldflags $(GO_LDFLAGS) -o $@ $(BUILD_PACKAGE) 57 | 58 | %.sha256: % 59 | shasum -a 256 $< &> $@ 60 | 61 | %.exe: % 62 | cp $< $@ 63 | 64 | .PRECIOUS: $(foreach platform, $(SUPPORTED_PLATFORMS), $(BUILD_DIR)/$(PROJECT)-$(platform)) 65 | 66 | .PHONY: cross 67 | cross: $(foreach platform, $(SUPPORTED_PLATFORMS), $(BUILD_DIR)/$(PROJECT)-$(platform).sha256) 68 | 69 | .PHONY: $(BUILD_DIR)/VERSION 70 | $(BUILD_DIR)/VERSION: $(BUILD_DIR) 71 | @ echo $(VERSION) > $@ 72 | 73 | $(BUILD_DIR): 74 | mkdir -p $(BUILD_DIR) 75 | 76 | .PHONY: release 77 | release: cross 78 | gsutil cp $(BUILD_DIR)/$(PROJECT)-* gs://$(RELEASE_BUCKET)/$(VERSION)/ 79 | 80 | .PHONY: clean 81 | clean: 82 | rm -rf $(BUILD_DIR) 83 | 84 | .PHONY: test 85 | test: $(BUILD_DIR)/$(PROJECT) 86 | ./tests/structure_test_tests.sh 87 | 88 | image: 89 | $(DOCKER) build -t gcr.io/gcp-runtimes/container-structure-test:latest . 90 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | Barebones releasing instructions 4 | 5 | - Update Makefile `VERSION_xxx` and submit changes 6 | - Tag the new release (at the above commit) 7 | - Tagging triggers a goreleaser build on github actions 8 | - Artifacts are automatically added to the github release 9 | - Container images are published to ghcr.io/googlecontainertools/container-structure-test (:latest, :) 10 | -------------------------------------------------------------------------------- /WORKSPACE.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-structure-test/56c7201716d770c0f820a9c19207ba2ea77c34f8/WORKSPACE.bazel -------------------------------------------------------------------------------- /bazel/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_skylib//:bzl_library.bzl", "bzl_library") 2 | 3 | package(default_visibility = ["//visibility:public"]) 4 | 5 | toolchain_type( 6 | name = "structure_test_toolchain_type", 7 | ) 8 | 9 | bzl_library( 10 | name = "container_structure_test", 11 | srcs = ["container_structure_test.bzl"], 12 | deps = [ 13 | "@aspect_bazel_lib//lib:paths", 14 | "@aspect_bazel_lib//lib:windows_utils", 15 | ], 16 | ) 17 | -------------------------------------------------------------------------------- /bazel/container_structure_test.bzl: -------------------------------------------------------------------------------- 1 | "Implementation details for container_structure_test rule." 2 | 3 | load("@aspect_bazel_lib//lib:paths.bzl", "BASH_RLOCATION_FUNCTION", "to_rlocation_path") 4 | load("@aspect_bazel_lib//lib:windows_utils.bzl", "create_windows_native_launcher_script") 5 | 6 | _attrs = { 7 | "image": attr.label( 8 | allow_single_file = True, 9 | doc = "Label of an oci_image or oci_tarball target.", 10 | ), 11 | "configs": attr.label_list(allow_files = True, mandatory = True), 12 | "driver": attr.string( 13 | default = "docker", 14 | # https://github.com/GoogleContainerTools/container-structure-test/blob/5e347b66fcd06325e3caac75ef7dc999f1a9b614/pkg/drivers/driver.go#L26-L28 15 | values = ["docker", "tar", "host"], 16 | doc = "See https://github.com/GoogleContainerTools/container-structure-test#running-file-tests-without-docker", 17 | ), 18 | "platform": attr.string( 19 | default = "linux/amd64", 20 | doc = "Set platform if host is multi-platform capable (default \"linux/amd64\")", 21 | ), 22 | "_runfiles": attr.label(default = "@bazel_tools//tools/bash/runfiles"), 23 | "_windows_constraint": attr.label(default = "@platforms//os:windows"), 24 | } 25 | 26 | CMD_HEAD = [ 27 | "#!/usr/bin/env bash", 28 | "# This script generated by container_structure_test.bzl", 29 | BASH_RLOCATION_FUNCTION, 30 | ] 31 | 32 | CMD = """\ 33 | readonly st=$(rlocation {st_path}) 34 | readonly jq=$(rlocation {jq_path}) 35 | readonly image=$(rlocation {image_path}) 36 | 37 | # When the image points to a folder, we can read the index.json file inside 38 | if [[ -d "$image" ]]; then 39 | readonly DIGEST=$("$jq" -r '.manifests[0].digest | sub(":"; "-")' "$image/index.json") 40 | exec "$st" test --driver {driver} {fixed_args} --default-image-tag "cst.oci.local/$DIGEST:$DIGEST" $@ 41 | else 42 | exec "$st" test --driver {driver} {fixed_args} $@ 43 | fi 44 | """ 45 | 46 | def _structure_test_impl(ctx): 47 | fixed_args = [] 48 | test_bin = ctx.toolchains["@container_structure_test//bazel:structure_test_toolchain_type"].st_info.binary 49 | jq_bin = ctx.toolchains["@aspect_bazel_lib//lib:jq_toolchain_type"].jqinfo.bin 50 | 51 | image_path = to_rlocation_path(ctx, ctx.file.image) 52 | 53 | # Prefer to use a tarball if we are given one, as it works with more 'driver' types. 54 | if image_path.endswith(".tar"): 55 | fixed_args.extend(["--image", "$(rlocation %s)" % image_path]) 56 | else: 57 | # https://github.com/GoogleContainerTools/container-structure-test/blob/5e347b66fcd06325e3caac75ef7dc999f1a9b614/cmd/container-structure-test/app/cmd/test.go#L110 58 | if ctx.attr.driver != "docker": 59 | fail("when the 'driver' attribute is not 'docker', then the image must be a .tar file") 60 | fixed_args.extend(["--ignore-ref-annotation", "--image-from-oci-layout", "$(rlocation %s)" % image_path]) 61 | 62 | for arg in ctx.files.configs: 63 | fixed_args.extend(["--config", "$(rlocation %s)" % to_rlocation_path(ctx, arg)]) 64 | 65 | if ctx.attr.platform: 66 | fixed_args.extend(["--platform", ctx.attr.platform]) 67 | 68 | bash_launcher = ctx.actions.declare_file("%s.sh" % ctx.label.name) 69 | ctx.actions.write( 70 | bash_launcher, 71 | content = "\n".join(CMD_HEAD) + CMD.format( 72 | st_path = to_rlocation_path(ctx, test_bin), 73 | jq_path = to_rlocation_path(ctx, jq_bin), 74 | driver = ctx.attr.driver, 75 | image_path = image_path, 76 | fixed_args = " ".join(fixed_args), 77 | ), 78 | is_executable = True, 79 | ) 80 | 81 | is_windows = ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]) 82 | launcher = create_windows_native_launcher_script(ctx, bash_launcher) if is_windows else bash_launcher 83 | runfiles = ctx.runfiles( 84 | files = ctx.files.image + ctx.files.configs + [ 85 | bash_launcher, 86 | test_bin, 87 | jq_bin, 88 | ], 89 | ).merge(ctx.attr._runfiles.default_runfiles) 90 | 91 | return DefaultInfo(runfiles = runfiles, executable = launcher) 92 | 93 | lib = struct( 94 | attrs = _attrs, 95 | implementation = _structure_test_impl, 96 | ) 97 | -------------------------------------------------------------------------------- /bazel/docs/BUILD.bazel: -------------------------------------------------------------------------------- 1 | # This load statement must be in the docs/ package rather than anything users depend on 2 | # so that the dependency on stardoc doesn't leak to them. 3 | load("@aspect_bazel_lib//lib:docs.bzl", "stardoc_with_diff_test", "update_docs") 4 | 5 | stardoc_with_diff_test( 6 | name = "defs", 7 | bzl_library_target = "//:defs", 8 | ) 9 | 10 | update_docs(name = "update") 11 | -------------------------------------------------------------------------------- /bazel/docs/defs.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exposes container-structure-test as a Bazel rule 4 | 5 | 6 | 7 | ## container_structure_test 8 | 9 |
10 | container_structure_test(name, configs, driver, image)
11 | 
12 | 13 | Tests a Docker- or OCI-format image. 14 | 15 | By default, it relies on the container runtime already installed and running on the target. 16 | 17 | By default, container-structure-test uses the socket available at `/var/run/docker.sock`. 18 | If the installation creates the socket in a different path, use 19 | `--test_env=DOCKER_HOST='unix://<path_to_sock>'`. 20 | 21 | If the installation uses a remote Docker daemon and is protected by TLS, the following may be needed as well 22 | `--test_env=DOCKER_TLS_VERIFY=1` 23 | `--test_env=DOCKER_CERT_PATH=<path_to_certs>`. 24 | 25 | To avoid putting this into the commandline or to instruct bazel to read it from terminal environment, 26 | simply add `test --test_env=DOCKER_HOST` into the `.bazelrc` file. 27 | 28 | Alternatively, use the `driver = "tar"` attribute to avoid the need for a container runtime, see 29 | https://github.com/GoogleContainerTools/container-structure-test#running-file-tests-without-docker 30 | 31 | 32 | **ATTRIBUTES** 33 | 34 | 35 | | Name | Description | Type | Mandatory | Default | 36 | | :------------- | :------------- | :------------- | :------------- | :------------- | 37 | | name | A unique name for this target. | Name | required | | 38 | | configs | - | List of labels | required | | 39 | | driver | See https://github.com/GoogleContainerTools/container-structure-test#running-file-tests-without-docker | String | optional | "docker" | 40 | | image | Label of an oci_image or oci_tarball target. | Label | optional | None | 41 | 42 | 43 | -------------------------------------------------------------------------------- /bazel/test/.bazelrc: -------------------------------------------------------------------------------- 1 | # Bazel options go here 2 | -------------------------------------------------------------------------------- /bazel/test/.bazelversion: -------------------------------------------------------------------------------- 1 | 6.1.1 2 | -------------------------------------------------------------------------------- /bazel/test/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@container_structure_test//:defs.bzl", "container_structure_test") 2 | 3 | container_structure_test( 4 | name = "test", 5 | configs = ["test.yaml"], 6 | driver = "tar", 7 | image = "tarball.tar", 8 | ) 9 | -------------------------------------------------------------------------------- /bazel/test/MODULE.bazel: -------------------------------------------------------------------------------- 1 | "Bazel dependencies under --enable_bzlmod" 2 | module( 3 | name = "smoke", 4 | compatibility_level = 1, 5 | version = "0.0.0", 6 | ) 7 | 8 | bazel_dep(name = "container_structure_test", version = "0.0.0") 9 | 10 | local_path_override( 11 | module_name = "container_structure_test", 12 | path = "../..", 13 | ) 14 | -------------------------------------------------------------------------------- /bazel/test/README.md: -------------------------------------------------------------------------------- 1 | # Bazel smoke test 2 | 3 | Verifies that the container_structure_test bazel rule exposed by this project works properly. 4 | 5 | ```sh 6 | cd bazel/test 7 | bazel test ... 8 | bazel test --enable_bzlmod ... 9 | ``` -------------------------------------------------------------------------------- /bazel/test/WORKSPACE.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 2 | 3 | http_archive( 4 | name = "aspect_bazel_lib", 5 | sha256 = "97fa63d95cc9af006c4c7b2123ddd2a91fb8d273012f17648e6423bae2c69470", 6 | strip_prefix = "bazel-lib-1.30.2", 7 | url = "https://github.com/aspect-build/bazel-lib/releases/download/v1.30.2/bazel-lib-v1.30.2.tar.gz", 8 | ) 9 | 10 | http_archive( 11 | name = "bazel_skylib", 12 | sha256 = "b8a1527901774180afc798aeb28c4634bdccf19c4d98e7bdd1ce79d1fe9aaad7", 13 | urls = [ 14 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.4.1/bazel-skylib-1.4.1.tar.gz", 15 | "https://github.com/bazelbuild/bazel-skylib/releases/download/1.4.1/bazel-skylib-1.4.1.tar.gz", 16 | ], 17 | ) 18 | 19 | local_repository( 20 | name = "container_structure_test", 21 | path = "../..", 22 | ) 23 | 24 | load("@container_structure_test//:repositories.bzl", "container_structure_test_register_toolchain") 25 | 26 | container_structure_test_register_toolchain(name = "cst") 27 | 28 | -------------------------------------------------------------------------------- /bazel/test/WORKSPACE.bzlmod: -------------------------------------------------------------------------------- 1 | # Replaces WORKSPACE.bazel under --enable_bzlmod 2 | -------------------------------------------------------------------------------- /bazel/test/tarball.tar: -------------------------------------------------------------------------------- 1 | blobs/0000755000000000000000000000000000000000000007006 5ustar blobs/sha256/0000755000000000000000000000000000000000000010016 5ustar blobs/sha256/1e7fda128dc65355b2e32d958d10988baf1a711155b0381d2f1d960d6ed855410000644000000000000000000000030500000000000020375 0ustar {"architecture":"arm64","created":"0001-01-01T00:00:00Z","os":"linux","rootfs":{"type":"layers","diff_ids":[]},"config":{"Cmd":["--arg1","--arg2"],"Entrypoint":["/custom_bin"],"Env":["ENV=/test"]}}manifest.json0000755000000000000000000000022000000000000010403 0ustar [ 2 | { 3 | "Config": "blobs/sha256/1e7fda128dc65355b2e32d958d10988baf1a711155b0381d2f1d960d6ed85541", 4 | "RepoTags": [], 5 | "Layers": [] 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /bazel/test/test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | 3 | metadataTest: 4 | envVars: 5 | - key: "ENV" 6 | value: "/test" 7 | entrypoint: ["/custom_bin"] 8 | cmd: ["--arg1", "--arg2"] 9 | -------------------------------------------------------------------------------- /bazel/toolchain.bzl: -------------------------------------------------------------------------------- 1 | """This module implements the toolchain rule to locate the binary.""" 2 | 3 | StructureTestInfo = provider( 4 | doc = "Information about how to invoke the container-structure-test executable.", 5 | fields = { 6 | "binary": "Executable container-structure-test binary", 7 | }, 8 | ) 9 | 10 | def _structure_test_toolchain_impl(ctx): 11 | binary = ctx.executable.structure_test 12 | 13 | template_variables = platform_common.TemplateVariableInfo({ 14 | "STRUCTURE_TEST_BIN": binary.path, 15 | }) 16 | default = DefaultInfo( 17 | files = depset([binary]), 18 | runfiles = ctx.runfiles(files = [binary]), 19 | ) 20 | st_info = StructureTestInfo(binary = binary) 21 | 22 | toolchain_info = platform_common.ToolchainInfo( 23 | st_info = st_info, 24 | template_variables = template_variables, 25 | default = default, 26 | ) 27 | return [ 28 | default, 29 | toolchain_info, 30 | template_variables, 31 | ] 32 | 33 | structure_test_toolchain = rule( 34 | implementation = _structure_test_toolchain_impl, 35 | attrs = { 36 | "structure_test": attr.label( 37 | doc = "A hermetically downloaded structure_test executable for the target platform.", 38 | mandatory = True, 39 | executable = True, 40 | cfg = "exec", 41 | allow_single_file = True, 42 | ), 43 | }, 44 | doc = "Defines a structure_test toolchain. See: https://docs.bazel.build/versions/main/toolchains.html#defining-toolchains.", 45 | ) 46 | -------------------------------------------------------------------------------- /bazel/toolchains_repo.bzl: -------------------------------------------------------------------------------- 1 | """Create a repository to hold the toolchains 2 | 3 | This follows guidance here: 4 | https://docs.bazel.build/versions/main/skylark/deploying.html#registering-toolchains 5 | " 6 | Note that in order to resolve toolchains in the analysis phase 7 | Bazel needs to analyze all toolchain targets that are registered. 8 | Bazel will not need to analyze all targets referenced by toolchain.toolchain attribute. 9 | If in order to register toolchains you need to perform complex computation in the repository, 10 | consider splitting the repository with toolchain targets 11 | from the repository with _toolchain targets. 12 | Former will be always fetched, 13 | and the latter will only be fetched when user actually needs to build code. 14 | " 15 | The "complex computation" in our case is simply downloading large artifacts. 16 | This guidance tells us how to avoid that: we put the toolchain targets in the alias repository 17 | with only the toolchain attribute pointing into the platform-specific repositories. 18 | """ 19 | 20 | # Add more platforms as needed to mirror all the binaries 21 | # published by the upstream project. 22 | PLATFORMS = { 23 | "darwin_amd64": struct( 24 | compatible_with = [ 25 | "@platforms//os:macos", 26 | "@platforms//cpu:x86_64", 27 | ], 28 | ), 29 | "darwin_arm64": struct( 30 | compatible_with = [ 31 | "@platforms//os:macos", 32 | "@platforms//cpu:aarch64", 33 | ], 34 | ), 35 | "linux_arm64": struct( 36 | compatible_with = [ 37 | "@platforms//os:linux", 38 | "@platforms//cpu:aarch64", 39 | ], 40 | ), 41 | "linux_s390x": struct( 42 | compatible_with = [ 43 | "@platforms//os:linux", 44 | "@platforms//cpu:s390x", 45 | ], 46 | ), 47 | "linux_amd64": struct( 48 | compatible_with = [ 49 | "@platforms//os:linux", 50 | "@platforms//cpu:x86_64", 51 | ], 52 | ), 53 | "windows_amd64": struct( 54 | compatible_with = [ 55 | "@platforms//os:windows", 56 | "@platforms//cpu:x86_64", 57 | ], 58 | ), 59 | } 60 | 61 | TOOLCHAIN_TMPL = """\ 62 | toolchain( 63 | name = "{platform}_toolchain", 64 | exec_compatible_with = {compatible_with}, 65 | toolchain = "{toolchain}", 66 | toolchain_type = "{toolchain_type}", 67 | ) 68 | """ 69 | 70 | BUILD_HEADER_TMPL = """\ 71 | # Generated by toolchains_repo.bzl 72 | # 73 | # These can be registered in the workspace file or passed to --extra_toolchains flag. 74 | # By default all of these toolchains are registered by the oci_register_toolchains macro 75 | # so you don't normally need to interact with these targets. 76 | 77 | """ 78 | 79 | def _toolchains_repo_impl(repository_ctx): 80 | build_content = BUILD_HEADER_TMPL 81 | 82 | for [platform, meta] in PLATFORMS.items(): 83 | build_content += TOOLCHAIN_TMPL.format( 84 | platform = platform, 85 | name = repository_ctx.attr.name, 86 | compatible_with = meta.compatible_with, 87 | toolchain_type = repository_ctx.attr.toolchain_type, 88 | toolchain = repository_ctx.attr.toolchain.format(platform = platform), 89 | ) 90 | 91 | repository_ctx.file("BUILD.bazel", build_content) 92 | 93 | toolchains_repo = repository_rule( 94 | _toolchains_repo_impl, 95 | doc = "Creates a repository with toolchain definitions for all known platforms which can be registered or selected.", 96 | attrs = { 97 | "toolchain": attr.string(doc = "Label of the toolchain with {platform} left as placeholder. example; @container_crane_{platform}//:crane_toolchain"), 98 | "toolchain_type": attr.string(doc = "Label of the toolchain_type. example; //oci:crane_toolchain_type"), 99 | }, 100 | ) 101 | -------------------------------------------------------------------------------- /cmd/container-structure-test/app/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "io" 19 | 20 | "github.com/GoogleContainerTools/container-structure-test/pkg/version" 21 | 22 | "github.com/pkg/errors" 23 | "github.com/sirupsen/logrus" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | var ( 28 | v string 29 | testReport string 30 | ) 31 | 32 | var rootCmd = &cobra.Command{ 33 | Use: "container-structure-test", 34 | Short: "container-structure-test provides a framework to test the structure of a container image", 35 | Long: `container-structure-test provides a powerful framework to validate 36 | the structure of a container image. 37 | These tests can be used to check the output of commands in an image, 38 | as well as verify metadata and contents of the filesystem.`, 39 | } 40 | 41 | func NewRootCommand(out, err io.Writer) *cobra.Command { 42 | rootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { 43 | if err := SetUpLogs(err, v); err != nil { 44 | return err 45 | } 46 | 47 | rootCmd.SilenceUsage = true 48 | logrus.Infof("container-structure-test %+v", version.GetVersion()) 49 | return nil 50 | } 51 | 52 | rootCmd.SilenceErrors = true 53 | rootCmd.AddCommand(NewCmdVersion(out)) 54 | rootCmd.AddCommand(NewCmdTest(out)) 55 | 56 | rootCmd.PersistentFlags().StringVarP(&v, "verbosity", "v", logrus.WarnLevel.String(), "Log level (debug, info, warn, error, fatal, panic)") 57 | 58 | return rootCmd 59 | } 60 | 61 | func SetUpLogs(out io.Writer, level string) error { 62 | logrus.SetOutput(out) 63 | lvl, err := logrus.ParseLevel(v) 64 | if err != nil { 65 | return errors.Wrap(err, "parsing log level") 66 | } 67 | logrus.SetLevel(lvl) 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /cmd/container-structure-test/app/cmd/test/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package test 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "io" 21 | "os" 22 | "strings" 23 | "time" 24 | 25 | "github.com/GoogleContainerTools/container-structure-test/pkg/config" 26 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 27 | "github.com/GoogleContainerTools/container-structure-test/pkg/output" 28 | "github.com/GoogleContainerTools/container-structure-test/pkg/types" 29 | "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 30 | 31 | "github.com/pkg/errors" 32 | "gopkg.in/yaml.v2" 33 | ) 34 | 35 | func ValidateArgs(opts *config.StructureTestOptions) error { 36 | if opts.Driver == drivers.Host { 37 | if opts.Metadata == "" { 38 | return fmt.Errorf("Please provide path to image metadata file") 39 | } 40 | if opts.ImagePath != "" { 41 | return fmt.Errorf("Cannot provide both image path and metadata file") 42 | } 43 | } else { 44 | if opts.ImagePath == "" && opts.ImageFromLayout == "" { 45 | return fmt.Errorf("Please supply path to image or oci image layout to test against") 46 | } 47 | if opts.Metadata != "" { 48 | return fmt.Errorf("Cannot provide both image path and metadata file") 49 | } 50 | } 51 | if len(opts.ConfigFiles) == 0 { 52 | return fmt.Errorf("Please provide at least one test config file") 53 | } 54 | return nil 55 | } 56 | 57 | func Parse(fp string, args *drivers.DriverConfig, driverImpl func(drivers.DriverConfig) (drivers.Driver, error)) (types.StructureTest, error) { 58 | testContents, err := os.ReadFile(fp) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | // We first have to unmarshal to determine the schema version, then we unmarshal again 64 | // to do the full parse. 65 | var unmarshal types.Unmarshaller 66 | var strictUnmarshal types.Unmarshaller 67 | var versionHolder types.SchemaVersion 68 | 69 | switch { 70 | case strings.HasSuffix(fp, ".json"): 71 | unmarshal = json.Unmarshal 72 | strictUnmarshal = json.Unmarshal 73 | case strings.HasSuffix(fp, ".yaml"): 74 | unmarshal = yaml.Unmarshal 75 | strictUnmarshal = yaml.UnmarshalStrict 76 | case strings.HasSuffix(fp, ".yml"): 77 | unmarshal = yaml.Unmarshal 78 | strictUnmarshal = yaml.UnmarshalStrict 79 | default: 80 | return nil, errors.New("Please provide valid JSON or YAML config file") 81 | } 82 | 83 | if err := unmarshal(testContents, &versionHolder); err != nil { 84 | return nil, err 85 | } 86 | 87 | version := versionHolder.SchemaVersion 88 | if version == "" { 89 | return nil, errors.New("Please provide schema version") 90 | } 91 | 92 | var st types.StructureTest 93 | if schemaVersion, ok := types.SchemaVersions[version]; ok { 94 | st = schemaVersion() 95 | } else { 96 | return nil, errors.New("Unsupported schema version: " + version) 97 | } 98 | 99 | if err = strictUnmarshal(testContents, st); err != nil { 100 | return nil, errors.New("error unmarshalling config: " + err.Error()) 101 | } 102 | 103 | tests, _ := st.(types.StructureTest) //type assertion 104 | tests.SetDriverImpl(driverImpl, *args) 105 | return tests, nil 106 | } 107 | 108 | func ProcessResults(out io.Writer, format unversioned.OutputValue, c chan interface{}) error { 109 | totalPass := 0 110 | totalFail := 0 111 | totalDuration := time.Duration(0) 112 | errStrings := make([]string, 0) 113 | results, err := channelToSlice(c) 114 | if err != nil { 115 | return errors.Wrap(err, "reading results from channel") 116 | } 117 | for _, r := range results { 118 | if format == unversioned.Text { 119 | // output individual results if we're not in json mode 120 | output.OutputResult(out, r) 121 | } 122 | if r.IsPass() { 123 | totalPass++ 124 | } else { 125 | totalFail++ 126 | } 127 | totalDuration += r.Duration 128 | } 129 | if totalPass+totalFail == 0 || totalFail > 0 { 130 | errStrings = append(errStrings, "FAIL") 131 | } 132 | if len(errStrings) > 0 { 133 | err = fmt.Errorf(strings.Join(errStrings, "\n")) 134 | } 135 | 136 | summary := unversioned.SummaryObject{ 137 | Total: totalFail + totalPass, 138 | Pass: totalPass, 139 | Fail: totalFail, 140 | Duration: totalDuration, 141 | } 142 | if format == unversioned.Json || format == unversioned.Junit { 143 | // only output results here if we're in json mode 144 | summary.Results = results 145 | } 146 | output.FinalResults(out, format, summary) 147 | 148 | return err 149 | } 150 | 151 | func channelToSlice(c chan interface{}) ([]*unversioned.TestResult, error) { 152 | results := []*unversioned.TestResult{} 153 | for elem := range c { 154 | elem, ok := elem.(*unversioned.TestResult) 155 | if !ok { 156 | return nil, fmt.Errorf("unexpected value found in channel: %v", elem) 157 | } 158 | results = append(results, elem) 159 | } 160 | return results, nil 161 | } 162 | -------------------------------------------------------------------------------- /cmd/container-structure-test/app/cmd/test_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import "testing" 18 | 19 | func TestSplitImagePath(t *testing.T) { 20 | tables := []struct { 21 | path string 22 | name string 23 | tag string 24 | len int 25 | }{ 26 | {"image", "image", "", 1}, 27 | {"image:tag", "image", "tag", 2}, 28 | {"path/image", "path/image", "", 1}, 29 | {"path/image:tag", "path/image", "tag", 2}, 30 | {"my.registry:50000/path/image", "my.registry:50000/path/image", "", 1}, 31 | {"my.registry:50000/path/image:tag", "my.registry:50000/path/image", "tag", 2}, 32 | {"gcr.io/dga-demo/skaffold-example@sha256:44092b2ea3da5b9adc3c51c2ff6b399ae487094183a3746dbb8918d450d52ac5", "gcr.io/dga-demo/skaffold-example", "sha256:44092b2ea3da5b9adc3c51c2ff6b399ae487094183a3746dbb8918d450d52ac5", 2}, 33 | {"gcr.io/dga-demo/skaffold-example:96be410b-dirty@sha256:44092b2ea3da5b9adc3c51c2ff6b399ae487094183a3746dbb8918d450d52ac5", "gcr.io/dga-demo/skaffold-example:96be410b-dirty", "sha256:44092b2ea3da5b9adc3c51c2ff6b399ae487094183a3746dbb8918d450d52ac5", 2}, 34 | } 35 | 36 | for _, table := range tables { 37 | parts := splitImagePath(table.path) 38 | if parts[0] != table.name && parts[1] != table.tag && len(parts) != table.len { 39 | t.Errorf("Splitting image path (%v) was incorrect, got: %v:%v, expected: %v:%v", table.path, parts[0], parts[1], table.name, table.tag) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cmd/container-structure-test/app/cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "io" 19 | 20 | "github.com/GoogleContainerTools/container-structure-test/cmd/container-structure-test/app/flags" 21 | "github.com/GoogleContainerTools/container-structure-test/pkg/version" 22 | "github.com/pkg/errors" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | var versionFlag = flags.NewTemplateFlag("{{.Version}}\n", version.Info{}) 27 | 28 | func NewCmdVersion(out io.Writer) *cobra.Command { 29 | cmd := &cobra.Command{ 30 | Use: "version", 31 | Short: "Print the version information", 32 | RunE: func(cmd *cobra.Command, args []string) error { 33 | return RunVersion(out, cmd) 34 | }, 35 | } 36 | 37 | cmd.Flags().VarP(versionFlag, "output", "o", versionFlag.Usage()) 38 | return cmd 39 | } 40 | 41 | func RunVersion(out io.Writer, cmd *cobra.Command) error { 42 | if err := versionFlag.Template().Execute(out, version.GetVersion()); err != nil { 43 | return errors.Wrap(err, "executing template") 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /cmd/container-structure-test/app/container-structure-test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package app 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/GoogleContainerTools/container-structure-test/cmd/container-structure-test/app/cmd" 21 | ) 22 | 23 | func Run() error { 24 | c := cmd.NewRootCommand(os.Stdout, os.Stderr) 25 | return c.Execute() 26 | } 27 | -------------------------------------------------------------------------------- /cmd/container-structure-test/app/flags/template.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package flags 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "fmt" 21 | "reflect" 22 | "strings" 23 | "text/template" 24 | 25 | "github.com/pkg/errors" 26 | ) 27 | 28 | type TemplateFlag struct { 29 | rawTemplate string 30 | template *template.Template 31 | context interface{} 32 | } 33 | 34 | func (t *TemplateFlag) String() string { 35 | return t.rawTemplate 36 | } 37 | 38 | func (t *TemplateFlag) Usage() string { 39 | defaultUsage := "Format output with go-template." 40 | if t.context != nil { 41 | goType := reflect.TypeOf(t.context) 42 | url := fmt.Sprintf("https://godoc.org/%s#%s", goType.PkgPath(), goType.Name()) 43 | defaultUsage += fmt.Sprintf(" For full struct documentation, see %s", url) 44 | } 45 | return defaultUsage 46 | } 47 | 48 | func (t *TemplateFlag) Set(value string) error { 49 | tmpl, err := parseTemplate(value) 50 | if err != nil { 51 | return errors.Wrap(err, "setting template flag") 52 | } 53 | t.rawTemplate = value 54 | t.template = tmpl 55 | return nil 56 | } 57 | 58 | func (t *TemplateFlag) Type() string { 59 | return fmt.Sprintf("%T", t) 60 | } 61 | 62 | func (t *TemplateFlag) Template() *template.Template { 63 | return t.template 64 | } 65 | 66 | func NewTemplateFlag(value string, context interface{}) *TemplateFlag { 67 | return &TemplateFlag{ 68 | template: template.Must(parseTemplate(value)), 69 | rawTemplate: value, 70 | context: context, 71 | } 72 | } 73 | 74 | func parseTemplate(value string) (*template.Template, error) { 75 | var funcs = template.FuncMap{ 76 | "json": func(v interface{}) string { 77 | buf := &bytes.Buffer{} 78 | enc := json.NewEncoder(buf) 79 | enc.SetEscapeHTML(false) 80 | enc.Encode(v) 81 | return strings.TrimSpace(buf.String()) 82 | }, 83 | "join": strings.Join, 84 | "title": strings.Title, 85 | "lower": strings.ToLower, 86 | "upper": strings.ToUpper, 87 | } 88 | 89 | return template.New("flagtemplate").Funcs(funcs).Parse(value) 90 | } 91 | -------------------------------------------------------------------------------- /cmd/container-structure-test/container-structure-test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "github.com/sirupsen/logrus" 19 | 20 | "github.com/GoogleContainerTools/container-structure-test/cmd/container-structure-test/app" 21 | ) 22 | 23 | func main() { 24 | if err := app.Run(); err != nil { 25 | logrus.Fatal(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /defs.bzl: -------------------------------------------------------------------------------- 1 | "Exposes container-structure-test as a Bazel rule" 2 | 3 | load("//bazel:container_structure_test.bzl", "lib") 4 | 5 | container_structure_test = rule( 6 | implementation = lib.implementation, 7 | attrs = lib.attrs, 8 | doc = """\ 9 | Tests a Docker- or OCI-format image. 10 | 11 | By default, it relies on the container runtime already installed and running on the target. 12 | 13 | By default, container-structure-test uses the socket available at `/var/run/docker.sock`. 14 | If the installation creates the socket in a different path, use 15 | `--test_env=DOCKER_HOST='unix://'`. 16 | 17 | If the installation uses a remote Docker daemon and is protected by TLS, the following may be needed as well 18 | `--test_env=DOCKER_TLS_VERIFY=1` 19 | `--test_env=DOCKER_CERT_PATH=`. 20 | 21 | To avoid putting this into the commandline or to instruct bazel to read it from terminal environment, 22 | simply add `test --test_env=DOCKER_HOST` into the `.bazelrc` file. 23 | 24 | Alternatively, use the `driver = "tar"` attribute to avoid the need for a container runtime, see 25 | https://github.com/GoogleContainerTools/container-structure-test#running-file-tests-without-docker 26 | """, 27 | test = True, 28 | toolchains = [ 29 | "@aspect_bazel_lib//lib:jq_toolchain_type", 30 | "@bazel_tools//tools/sh:toolchain_type", 31 | "@container_structure_test//bazel:structure_test_toolchain_type", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 2 | ADD https://github.com/GoogleCloudPlatform/docker-credential-gcr/releases/download/v2.1.6/docker-credential-gcr_linux_amd64-2.1.6.tar.gz /usr/local/bin 3 | RUN tar -C /usr/local/bin/ -xvzf /usr/local/bin/docker-credential-gcr_linux_amd64-2.1.6.tar.gz 4 | 5 | FROM gcr.io/distroless/base:latest 6 | COPY --from=0 /usr/local/bin/docker-credential-gcr /docker-credential-gcr 7 | ADD out/container-structure-test-linux-amd64 /container-structure-test 8 | ENTRYPOINT ["/container-structure-test"] 9 | -------------------------------------------------------------------------------- /deploy/Dockerfile.build: -------------------------------------------------------------------------------- 1 | # Dockerfile used to build a build step that builds container-structure-test in CI. 2 | FROM golang:1.22 3 | RUN apt-get update && apt-get install make 4 | RUN mkdir -p /go/src/github.com/GoogleContainerTools 5 | RUN ln -s /workspace /go/src/github.com/GoogleContainerTools/container-structure-test 6 | WORKDIR /go/src/github.com/GoogleContainerTools/container-structure-test 7 | -------------------------------------------------------------------------------- /deploy/Dockerfile_debug: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 2 | ADD https://github.com/GoogleCloudPlatform/docker-credential-gcr/releases/download/v2.1.6/docker-credential-gcr_linux_amd64-2.1.6.tar.gz /usr/local/bin/ 3 | RUN tar -C /usr/local/bin/ -xvzf /usr/local/bin/docker-credential-gcr_linux_amd64-2.1.6.tar.gz 4 | 5 | FROM gcr.io/distroless/base:debug 6 | COPY --from=0 /usr/local/bin/docker-credential-gcr /docker-credential-gcr 7 | ADD out/container-structure-test-linux-amd64 /container-structure-test 8 | ENTRYPOINT ["/container-structure-test"] 9 | -------------------------------------------------------------------------------- /deploy/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # This build is run on every merge to master. 2 | steps: 3 | # Build the container that will do the go build. 4 | - name: 'gcr.io/cloud-builders/docker' 5 | args: ['build', '-t', 'builder', '-f', 'deploy/Dockerfile.build', '.'] 6 | 7 | # Do the go build for amd64 8 | - name: 'builder' 9 | args: ['make', 'cross'] 10 | env: ['GOARCH=amd64'] 11 | 12 | # Do the go build for arm64 13 | - name: 'builder' 14 | args: ['make', 'cross'] 15 | env: ['GOARCH=arm64'] 16 | 17 | # Do the go build for s390x 18 | - name: 'builder' 19 | args: ['make', 'cross'] 20 | env: ['GOARCH=s390x'] 21 | 22 | # Do the go build for ppc64le 23 | - name: 'builder' 24 | args: ['make', 'cross'] 25 | env: ['GOARCH=ppc64le'] 26 | 27 | 28 | # Upload to GCS 29 | - name: 'gcr.io/cloud-builders/gsutil' 30 | args: ['cp', '-r', 'out/*', 'gs://container-structure-test/builds/$COMMIT_SHA/'] 31 | 32 | # Build the image 33 | - name: 'gcr.io/cloud-builders/docker' 34 | args: ['build', '-t', 'gcr.io/gcp-runtimes/container-structure-test:$COMMIT_SHA', 35 | '-f', 'deploy/Dockerfile', '.'] 36 | images: 37 | - 'gcr.io/gcp-runtimes/container-structure-test:$COMMIT_SHA' 38 | -------------------------------------------------------------------------------- /deploy/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auths": { 3 | }, 4 | "credHelpers": { 5 | "appengine.gcr.io": "gcr", 6 | "asia.gcr.io": "gcr", 7 | "eu.gcr.io": "gcr", 8 | "gcr.io": "gcr", 9 | "gcr.kubernetes.io": "gcr", 10 | "us.gcr.io": "gcr" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /deploy/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2017 Google Inc. All rights reserved. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | usage() { echo "Usage: ./release.sh [commit_sha]"; exit 1; } 18 | 19 | set -e 20 | 21 | export COMMIT=$1 22 | 23 | if [ -z "$COMMIT" ]; then 24 | usage 25 | fi 26 | 27 | gsutil cp gs://container-structure-test/builds/$COMMIT/container-structure-test gs://container-structure-test/latest/ 28 | -------------------------------------------------------------------------------- /deploy/release_cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # This cloudbuild is run on the creation of new tags, which should signify releases. 2 | 3 | steps: 4 | # Build the container that will do the go build 5 | - name: 'gcr.io/cloud-builders/docker' 6 | args: ['build', '-t', 'builder', '-f', 'deploy/Dockerfile.build', '.'] 7 | 8 | # Do the go build for amd64 9 | - name: 'builder' 10 | args: ['make', 'cross'] 11 | env: ['GOARCH=amd64'] 12 | 13 | # Do the go build for arm64 14 | - name: 'builder' 15 | args: ['make', 'cross'] 16 | env: ['GOARCH=arm64'] 17 | 18 | # Do the go build for s390x 19 | - name: 'builder' 20 | args: ['make', 'cross'] 21 | env: ['GOARCH=s390x'] 22 | 23 | # Do the go build for s390x 24 | - name: 'builder' 25 | args: ['make', 'cross'] 26 | env: ['GOARCH=ppc64le'] 27 | 28 | # Upload to GCS 29 | - name: 'gcr.io/cloud-builders/gsutil' 30 | args: ['cp', '-r', 'out/*', 'gs://container-structure-test/$TAG_NAME/'] 31 | 32 | - name: 'gcr.io/cloud-builders/gsutil' 33 | args: ['cp', '-r', 'gs://container-structure-test/$TAG_NAME/*', 'gs://container-structure-test/latest/'] 34 | 35 | # Build the image 36 | - name: 'gcr.io/cloud-builders/docker' 37 | args: ['build', '-t', 'gcr.io/gcp-runtimes/container-structure-test:$TAG_NAME', 38 | '-f', 'deploy/Dockerfile', '.'] 39 | 40 | # Tag the image as latest 41 | - name: 'gcr.io/cloud-builders/docker' 42 | args: ['tag', 'gcr.io/gcp-runtimes/container-structure-test:$TAG_NAME', 43 | 'gcr.io/gcp-runtimes/container-structure-test:latest'] 44 | 45 | # Build the debug image 46 | - name: 'gcr.io/cloud-builders/docker' 47 | args: ['build', '-t', 'gcr.io/gcp-runtimes/container-structure-test:debug', 48 | '-f', 'deploy/Dockerfile_debug', '.'] 49 | 50 | images: 51 | - 'gcr.io/gcp-runtimes/container-structure-test:$TAG_NAME' 52 | - 'gcr.io/gcp-runtimes/container-structure-test:latest' 53 | - 'gcr.io/gcp-runtimes/container-structure-test:debug' 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleContainerTools/container-structure-test 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/fsouza/go-dockerclient v1.11.2 7 | github.com/google/go-cmp v0.6.0 8 | github.com/google/go-containerregistry v0.20.1 9 | github.com/joho/godotenv v1.5.1 10 | github.com/moby/sys/sequential v0.6.0 11 | github.com/opencontainers/image-spec v1.1.0 12 | github.com/pkg/errors v0.9.1 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/spf13/cobra v1.8.0 15 | golang.org/x/crypto v0.25.0 16 | gopkg.in/yaml.v2 v2.4.0 17 | ) 18 | 19 | exclude github.com/docker/docker v24.0.6+incompatible // indirect 20 | 21 | require ( 22 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 23 | github.com/Microsoft/go-winio v0.6.2 // indirect 24 | github.com/containerd/containerd v1.7.13 // indirect 25 | github.com/containerd/log v0.1.0 // indirect 26 | github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect 27 | github.com/distribution/reference v0.5.0 // indirect 28 | github.com/docker/cli v25.0.3+incompatible // indirect 29 | github.com/docker/distribution v2.8.3+incompatible // indirect 30 | github.com/docker/docker v27.1.1+incompatible // indirect 31 | github.com/docker/docker-credential-helpers v0.8.1 // indirect 32 | github.com/docker/go-connections v0.5.0 // indirect 33 | github.com/docker/go-units v0.5.0 // indirect 34 | github.com/felixge/httpsnoop v1.0.4 // indirect 35 | github.com/go-logr/logr v1.4.1 // indirect 36 | github.com/go-logr/stdr v1.2.2 // indirect 37 | github.com/gogo/protobuf v1.3.2 // indirect 38 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 39 | github.com/klauspost/compress v1.17.7 // indirect 40 | github.com/kr/pretty v0.2.1 // indirect 41 | github.com/kr/text v0.2.0 // indirect 42 | github.com/mitchellh/go-homedir v1.1.0 // indirect 43 | github.com/moby/docker-image-spec v1.3.1 // indirect 44 | github.com/moby/patternmatcher v0.6.0 // indirect 45 | github.com/moby/sys/user v0.1.0 // indirect 46 | github.com/moby/term v0.5.0 // indirect 47 | github.com/morikuni/aec v1.0.0 // indirect 48 | github.com/opencontainers/go-digest v1.0.0 // indirect 49 | github.com/spf13/pflag v1.0.5 // indirect 50 | github.com/vbatts/tar-split v0.11.5 // indirect 51 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 52 | go.opentelemetry.io/otel v1.24.0 // indirect 53 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 54 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 55 | golang.org/x/sync v0.6.0 // indirect 56 | golang.org/x/sys v0.22.0 // indirect 57 | golang.org/x/term v0.22.0 // indirect 58 | ) 59 | -------------------------------------------------------------------------------- /hack/.gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Google Inc. All rights reserved. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | files=$(find . -name "*.go" | grep -v vendor/ | xargs gofmt -l -s) 20 | if [[ $files ]]; then 21 | echo "Gofmt errors in files:" 22 | echo "$files" 23 | diff=$(find . -name "*.go" | grep -v vendor/ | xargs gofmt -d -s) 24 | echo "$diff" 25 | exit 1 26 | fi 27 | -------------------------------------------------------------------------------- /hack/checksums.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2024 Google Inc. All rights reserved. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eu 18 | 19 | 20 | LATEST=$(curl --silent -L https://api.github.com/repos/GoogleContainerTools/container-structure-test/releases/latest | jq -r .tag_name) 21 | 22 | TAG="${TAG:-$LATEST}" 23 | 24 | 25 | echo "Using tag: $LATEST" 26 | 27 | checksums="$(curl --silent -L https://github.com/GoogleContainerTools/container-structure-test/releases/download/$TAG/checksums.txt)" 28 | 29 | 30 | echo "Paste this into repositories.bzl" 31 | echo "" 32 | echo "" 33 | echo "" 34 | 35 | echo "_VERSION=\"$TAG\"" 36 | echo "_HASHES = {" 37 | while IFS= read -r line; do 38 | read -r sha256 filename <<< "$line" 39 | integrity="sha256-$(echo $sha256 | xxd -r -p | base64)" 40 | filename=${filename#container-structure-test-} 41 | echo " \"$filename\": \"$integrity\"," 42 | done <<< "$checksums" 43 | echo "}" 44 | 45 | echo "" 46 | -------------------------------------------------------------------------------- /hack/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2017 Google Inc. All rights reserved. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eu 18 | 19 | usage() { 20 | echo "Usage: $0 [-i ] [-c ] [-v] [-e ]" 21 | exit 1 22 | } 23 | 24 | export DOCKER_API_VERSION="1.21" 25 | 26 | VERBOSE=0 27 | CMD_STRING="/test/structure_test" 28 | ENTRYPOINT="/bin/sh" 29 | 30 | while test $# -gt 0; do 31 | case "$1" in 32 | --image|-i) 33 | shift 34 | if test $# -gt 0; then 35 | export IMAGE_NAME=$1 36 | else 37 | usage 38 | fi 39 | shift 40 | ;; 41 | --verbose|-v) 42 | export VERBOSE=1 43 | shift 44 | ;; 45 | --entrypoint|-e) 46 | shift 47 | if test $# -gt 0; then 48 | export ENTRYPOINT=$1 49 | else 50 | usage 51 | fi 52 | shift 53 | ;; 54 | --config|-c) 55 | shift 56 | if test $# -eq 0; then 57 | usage 58 | else 59 | CMD_STRING=$CMD_STRING" --config $1" 60 | fi 61 | shift 62 | ;; 63 | *) 64 | usage 65 | ;; 66 | esac 67 | done 68 | 69 | if [ $VERBOSE -eq 1 ]; then 70 | CMD_STRING=$CMD_STRING" -test.v" 71 | fi 72 | 73 | if [ -z "$IMAGE_NAME" ]; then 74 | usage 75 | fi 76 | 77 | # Get id of container we're currently in. This method is 78 | # system-specific. We could look at $HOSTNAME instead, but $HOSTNAME 79 | # is truncated at 12 characters so collisions are possible, and it 80 | # will fail if this container is started with something like: 81 | # docker run --name=foo --hostname=bar 82 | THIS_CONTAINER=$(basename "$(head -1 /proc/self/cgroup)") 83 | if [ -z "$THIS_CONTAINER" ]; then 84 | echo "Failed to read container id from /proc" 85 | exit 1 86 | fi 87 | 88 | docker run --privileged=true --volumes-from="${THIS_CONTAINER}" --entrypoint="$ENTRYPOINT" "$IMAGE_NAME" -c "$CMD_STRING" 89 | -------------------------------------------------------------------------------- /hack/tests.bzl: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All rights reserved. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Rule for running structure tests.""" 16 | 17 | load( 18 | "@io_bazel_rules_docker//docker:docker.bzl", 19 | "docker_build", 20 | ) 21 | 22 | def _impl(ctx): 23 | 24 | if (not (ctx.attr.image_tar or ctx.executable.image) or 25 | (ctx.attr.image_tar and ctx.executable.image)): 26 | fail('Please specify one of \'image\' or \'image_tar\'') 27 | st_binary = ctx.executable._structure_test.short_path 28 | config_location = ctx.file.config.short_path 29 | if ctx.executable.image: 30 | load_statement = ctx.executable.image.short_path 31 | image = ctx.attr.image 32 | runfiles = [ctx.executable.image] + image.files.to_list() + image.data_runfiles.files.to_list() 33 | else: # image_tar was set. 34 | load_statement = 'docker load -i %s' % ctx.file.image_tar.short_path 35 | image = ctx.attr.image_tar 36 | runfiles = [ctx.file.image_tar] 37 | 38 | image_name = "bazel/%s:%s" % (image.label.package, image.label.name) 39 | # Generate a shell script to execute structure_tests with the correct flags. 40 | test_contents = """\ 41 | #!/bin/bash 42 | set -ex 43 | # Execute the image load statement. 44 | {0} 45 | 46 | # Run the tests. 47 | {1} \ 48 | -image {2} \ 49 | $(pwd)/{3} 50 | """.format(load_statement, st_binary, image_name, config_location) 51 | ctx.file_action( 52 | output=ctx.outputs.executable, 53 | content=test_contents 54 | ) 55 | 56 | return struct(runfiles=ctx.runfiles(files = [ 57 | ctx.executable._structure_test, 58 | ctx.file.config] + 59 | runfiles 60 | ), 61 | ) 62 | 63 | structure_test = rule( 64 | attrs = { 65 | "_structure_test": attr.label( 66 | default = Label("//structure_tests:go_default_test"), 67 | cfg = "target", 68 | allow_files = True, 69 | executable = True, 70 | ), 71 | "image": attr.label( 72 | executable = True, 73 | cfg = "target", 74 | ), 75 | "image_tar": attr.label( 76 | allow_files = [".tar"], 77 | single_file = True, 78 | ), 79 | "config": attr.label( 80 | mandatory = True, 81 | allow_files = True, 82 | single_file = True, 83 | ), 84 | }, 85 | executable = True, 86 | test = True, 87 | implementation = _impl, 88 | ) 89 | 90 | def structure_test_with_files(name, image, config, files): 91 | """A macro for including extra files inside an image before testing it.""" 92 | child_image_name = "%s.child_image" % name 93 | docker_build( 94 | name = child_image_name, 95 | base = image, 96 | files = files, 97 | ) 98 | 99 | structure_test( 100 | name = name, 101 | image = child_image_name, 102 | config = config, 103 | ) 104 | -------------------------------------------------------------------------------- /internal/pkgutil/README.md: -------------------------------------------------------------------------------- 1 | This package was copied over from [container-diff](https://github.com/GoogleContainerTools/container-diff) to remove our dependency on it. That project is too hard to maintain. 2 | 3 | project: https://github.com/GoogleContainerTools/container-diff 4 | 5 | commit: ae4befd09f92caf735cdd63794ae2fa9f2efc5e3 6 | 7 | path: ./pkg/util -------------------------------------------------------------------------------- /internal/pkgutil/fs_utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google, Inc. All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package pkgutil 18 | 19 | import ( 20 | "bytes" 21 | "io" 22 | "io/ioutil" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | 27 | "github.com/sirupsen/logrus" 28 | ) 29 | 30 | // Directory stores a representation of a file directory. 31 | type Directory struct { 32 | Root string 33 | Content []string 34 | } 35 | 36 | type DirectoryEntry struct { 37 | Name string 38 | Size int64 39 | } 40 | 41 | func GetSize(path string) int64 { 42 | stat, err := os.Lstat(path) 43 | if err != nil { 44 | logrus.Errorf("Could not obtain size for %s: %s", path, err) 45 | return -1 46 | } 47 | if stat.IsDir() { 48 | size, err := getDirectorySize(path) 49 | if err != nil { 50 | logrus.Errorf("Could not obtain directory size for %s: %s", path, err) 51 | } 52 | return size 53 | } 54 | return stat.Size() 55 | } 56 | 57 | // GetFileContents returns the contents of a file at the specified path 58 | func GetFileContents(path string) (*string, error) { 59 | if _, err := os.Lstat(path); os.IsNotExist(err) { 60 | return nil, err 61 | } 62 | 63 | contents, err := ioutil.ReadFile(path) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | strContents := string(contents) 69 | //If file is empty, return nil 70 | if strContents == "" { 71 | return nil, nil 72 | } 73 | return &strContents, nil 74 | } 75 | 76 | func getDirectorySize(path string) (int64, error) { 77 | var size int64 78 | err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { 79 | if !info.IsDir() { 80 | size += info.Size() 81 | } 82 | return err 83 | }) 84 | return size, err 85 | } 86 | 87 | // GetDirectoryContents converts the directory starting at the provided path into a Directory struct. 88 | func GetDirectory(path string, deep bool) (Directory, error) { 89 | var directory Directory 90 | directory.Root = path 91 | var err error 92 | if deep { 93 | walkFn := func(currPath string, info os.FileInfo, err error) error { 94 | newContent := strings.TrimPrefix(currPath, directory.Root) 95 | if newContent != "" { 96 | directory.Content = append(directory.Content, newContent) 97 | } 98 | return nil 99 | } 100 | 101 | err = filepath.Walk(path, walkFn) 102 | } else { 103 | contents, err := ioutil.ReadDir(path) 104 | if err != nil { 105 | return directory, err 106 | } 107 | 108 | for _, file := range contents { 109 | fileName := "/" + file.Name() 110 | directory.Content = append(directory.Content, fileName) 111 | } 112 | } 113 | return directory, err 114 | } 115 | 116 | func GetDirectoryEntries(d Directory) []DirectoryEntry { 117 | return CreateDirectoryEntries(d.Root, d.Content) 118 | } 119 | 120 | func CreateDirectoryEntries(root string, entryNames []string) (entries []DirectoryEntry) { 121 | for _, name := range entryNames { 122 | entryPath := filepath.Join(root, name) 123 | size := GetSize(entryPath) 124 | 125 | entry := DirectoryEntry{ 126 | Name: name, 127 | Size: size, 128 | } 129 | entries = append(entries, entry) 130 | } 131 | return entries 132 | } 133 | 134 | func CheckSameSymlink(f1name, f2name string) (bool, error) { 135 | link1, err := os.Readlink(f1name) 136 | if err != nil { 137 | return false, err 138 | } 139 | link2, err := os.Readlink(f2name) 140 | if err != nil { 141 | return false, err 142 | } 143 | return (link1 == link2), nil 144 | } 145 | 146 | func CheckSameFile(f1name, f2name string) (bool, error) { 147 | // Check first if files differ in size and immediately return 148 | f1stat, err := os.Lstat(f1name) 149 | if err != nil { 150 | return false, err 151 | } 152 | f2stat, err := os.Lstat(f2name) 153 | if err != nil { 154 | return false, err 155 | } 156 | 157 | if f1stat.Size() != f2stat.Size() { 158 | return false, nil 159 | } 160 | 161 | // Next, check file contents 162 | f1, err := ioutil.ReadFile(f1name) 163 | if err != nil { 164 | return false, err 165 | } 166 | f2, err := ioutil.ReadFile(f2name) 167 | if err != nil { 168 | return false, err 169 | } 170 | 171 | if !bytes.Equal(f1, f2) { 172 | return false, nil 173 | } 174 | return true, nil 175 | } 176 | 177 | // HasFilepathPrefix checks if the given file path begins with prefix 178 | func HasFilepathPrefix(path, prefix string) bool { 179 | path = filepath.Clean(path) 180 | prefix = filepath.Clean(prefix) 181 | pathArray := strings.Split(path, "/") 182 | prefixArray := strings.Split(prefix, "/") 183 | 184 | if len(pathArray) < len(prefixArray) { 185 | return false 186 | } 187 | for index := range prefixArray { 188 | if prefixArray[index] == pathArray[index] { 189 | continue 190 | } 191 | return false 192 | } 193 | return true 194 | } 195 | 196 | // given a path to a directory, check if it has any contents 197 | func DirIsEmpty(path string) (bool, error) { 198 | f, err := os.Open(path) 199 | if err != nil { 200 | return false, err 201 | } 202 | defer f.Close() 203 | 204 | _, err = f.Readdir(1) 205 | if err == io.EOF { 206 | return true, nil 207 | } 208 | return false, err 209 | } 210 | 211 | // CleanFilePath removes characters from a given path that cannot be used 212 | // in paths by the underlying platform (e.g. Windows) 213 | func CleanFilePath(dirtyPath string) string { 214 | var windowsReplacements = []string{"<", "_", ">", "_", ":", "_", "?", "_", "*", "_", "?", "_", "|", "_"} 215 | replacer := strings.NewReplacer(windowsReplacements...) 216 | return filepath.Clean(replacer.Replace(dirtyPath)) 217 | } 218 | -------------------------------------------------------------------------------- /internal/pkgutil/tar_utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google, Inc. All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package pkgutil 18 | 19 | import ( 20 | "archive/tar" 21 | "fmt" 22 | "io" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | 27 | "sync" 28 | "sync/atomic" 29 | 30 | "github.com/pkg/errors" 31 | "github.com/sirupsen/logrus" 32 | ) 33 | 34 | type OriginalPerm struct { 35 | path string 36 | perm os.FileMode 37 | } 38 | 39 | func unpackTar(tr *tar.Reader, path string, whitelist []string) error { 40 | // Thread safe Map of target:linkname 41 | var hardlinks sync.Map 42 | 43 | originalPerms := make([]OriginalPerm, 0) 44 | for { 45 | header, err := tr.Next() 46 | if err == io.EOF { 47 | // end of tar archive 48 | break 49 | } 50 | if err != nil { 51 | return errors.Wrap(err, "Error getting next tar header") 52 | } 53 | target := filepath.Clean(filepath.Join(path, header.Name)) 54 | // Make sure the target isn't part of the whitelist 55 | if checkWhitelist(target, whitelist) { 56 | continue 57 | } 58 | mode := header.FileInfo().Mode() 59 | switch header.Typeflag { 60 | 61 | // if its a dir and it doesn't exist create it 62 | case tar.TypeDir: 63 | if _, err := os.Stat(target); os.IsNotExist(err) { 64 | if mode.Perm()&(1<<(uint(7))) == 0 { 65 | logrus.Debugf("Write permission bit not set on %s by default; setting manually", target) 66 | originalMode := mode 67 | mode = mode | (1 << uint(7)) 68 | // keep track of original file permission to reset later 69 | originalPerms = append(originalPerms, OriginalPerm{ 70 | path: target, 71 | perm: originalMode, 72 | }) 73 | } 74 | logrus.Debugf("Creating directory %s with permissions %v", target, mode) 75 | if err := os.MkdirAll(target, mode); err != nil { 76 | return err 77 | } 78 | // In some cases, MkdirAll doesn't change the permissions, so run Chmod 79 | if err := os.Chmod(target, mode); err != nil { 80 | return err 81 | } 82 | } 83 | 84 | // if it's a file create it 85 | case tar.TypeReg: 86 | // It's possible for a file to be included before the directory it's in is created. 87 | baseDir := filepath.Dir(target) 88 | if _, err := os.Stat(baseDir); os.IsNotExist(err) { 89 | logrus.Debugf("baseDir %s for file %s does not exist. Creating", baseDir, target) 90 | if err := os.MkdirAll(baseDir, 0755); err != nil { 91 | return err 92 | } 93 | } 94 | // It's possible we end up creating files that can't be overwritten based on their permissions. 95 | // Explicitly delete an existing file before continuing. 96 | if _, err := os.Stat(target); !os.IsNotExist(err) { 97 | logrus.Debugf("Removing %s for overwrite", target) 98 | if err := os.Remove(target); err != nil { 99 | logrus.Errorf("error removing file %s", target) 100 | return err 101 | } 102 | } 103 | 104 | logrus.Debugf("Creating file %s with permissions %v", target, mode) 105 | currFile, err := os.Create(target) 106 | if err != nil { 107 | logrus.Errorf("Error creating file %s %s", target, err) 108 | return err 109 | } 110 | // manually set permissions on file, since the default umask (022) will interfere 111 | if err = os.Chmod(target, mode); err != nil { 112 | logrus.Errorf("Error updating file permissions on %s", target) 113 | return err 114 | } 115 | _, err = io.Copy(currFile, tr) 116 | if err != nil { 117 | return err 118 | } 119 | currFile.Close() 120 | case tar.TypeSymlink: 121 | // It's possible we end up creating files that can't be overwritten based on their permissions. 122 | // Explicitly delete an existing file before continuing. 123 | if _, err := os.Stat(target); !os.IsNotExist(err) { 124 | logrus.Debugf("Removing %s to create symlink", target) 125 | if err := os.RemoveAll(target); err != nil { 126 | logrus.Debugf("Unable to remove %s: %s", target, err) 127 | } 128 | } 129 | 130 | if err = os.Symlink(header.Linkname, target); err != nil { 131 | logrus.Errorf("Failed to create symlink between %s and %s: %s", header.Linkname, target, err) 132 | } 133 | case tar.TypeLink: 134 | linkname := filepath.Clean(filepath.Join(path, header.Linkname)) 135 | // Check if the linkname already exists 136 | if _, err := os.Stat(linkname); !os.IsNotExist(err) { 137 | // If it exists, create the hard link 138 | resolveHardlink(linkname, target) 139 | } else { 140 | hardlinks.Store(target, linkname) 141 | } 142 | } 143 | } 144 | var resolveError atomic.Value 145 | hardlinks.Range(func(key, value interface{}) bool { 146 | target := key.(string) 147 | linkname := value.(string) 148 | logrus.Info("Resolving hard links") 149 | if _, err := os.Stat(linkname); !os.IsNotExist(err) { 150 | // If it exists, create the hard link 151 | if err := resolveHardlink(linkname, target); err != nil { 152 | resolveError.Store(errors.Wrap(err, fmt.Sprintf("Unable to create hard link from %s to %s", linkname, target))) 153 | return false 154 | } 155 | } 156 | return true 157 | }) 158 | if resolveError.Load() != nil { 159 | return resolveError.Load().(error) 160 | } 161 | 162 | // reset all original file 163 | for _, perm := range originalPerms { 164 | if err := os.Chmod(perm.path, perm.perm); err != nil { 165 | return err 166 | } 167 | } 168 | return nil 169 | } 170 | 171 | func resolveHardlink(linkname, target string) error { 172 | if err := os.Link(linkname, target); err != nil { 173 | return err 174 | } 175 | logrus.Debugf("Created hard link from %s to %s", linkname, target) 176 | return nil 177 | } 178 | 179 | func checkWhitelist(target string, whitelist []string) bool { 180 | for _, w := range whitelist { 181 | if HasFilepathPrefix(target, w) { 182 | logrus.Debugf("Not extracting %s, as it has prefix %s which is whitelisted", target, w) 183 | return true 184 | } 185 | } 186 | return false 187 | } 188 | 189 | func IsTar(path string) bool { 190 | return filepath.Ext(path) == ".tar" || 191 | filepath.Ext(path) == ".tar.gz" || 192 | filepath.Ext(path) == ".tgz" 193 | } 194 | 195 | func CheckTar(image string) bool { 196 | if strings.TrimSuffix(image, ".tar") == image { 197 | return false 198 | } 199 | if _, err := os.Stat(image); err != nil { 200 | logrus.Errorf("%s does not exist", image) 201 | return false 202 | } 203 | return true 204 | } 205 | -------------------------------------------------------------------------------- /internal/pkgutil/transport_builder.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Google, Inc. All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package pkgutil 18 | 19 | import ( 20 | "crypto/tls" 21 | "crypto/x509" 22 | . "github.com/google/go-containerregistry/pkg/name" 23 | "github.com/sirupsen/logrus" 24 | "io/ioutil" 25 | "net/http" 26 | ) 27 | 28 | var tlsConfiguration = struct { 29 | certifiedRegistries map[string]string 30 | skipTLSVerifyRegistries map[string]struct{} 31 | }{ 32 | certifiedRegistries: make(map[string]string), 33 | skipTLSVerifyRegistries: make(map[string]struct{}), 34 | } 35 | 36 | func ConfigureTLS(skipTsVerifyRegistries []string, registriesToCertificates map[string]string) { 37 | tlsConfiguration.skipTLSVerifyRegistries = make(map[string]struct{}) 38 | for _, registry := range skipTsVerifyRegistries { 39 | tlsConfiguration.skipTLSVerifyRegistries[registry] = struct{}{} 40 | } 41 | tlsConfiguration.certifiedRegistries = make(map[string]string) 42 | for registry := range registriesToCertificates { 43 | tlsConfiguration.certifiedRegistries[registry] = registriesToCertificates[registry] 44 | } 45 | } 46 | 47 | func BuildTransport(registry Registry) http.RoundTripper { 48 | var tr http.RoundTripper = http.DefaultTransport.(*http.Transport).Clone() 49 | 50 | if _, present := tlsConfiguration.skipTLSVerifyRegistries[registry.RegistryStr()]; present { 51 | tr.(*http.Transport).TLSClientConfig = &tls.Config{ 52 | InsecureSkipVerify: true, 53 | } 54 | } else if certificatePath := tlsConfiguration.certifiedRegistries[registry.RegistryStr()]; certificatePath != "" { 55 | systemCertPool := defaultX509Handler() 56 | if err := appendCertificate(systemCertPool, certificatePath); err != nil { 57 | logrus.WithError(err).Warnf("Failed to load certificate %s for %s\n", certificatePath, registry.RegistryStr()) 58 | } else { 59 | tr.(*http.Transport).TLSClientConfig = &tls.Config{ 60 | RootCAs: systemCertPool, 61 | } 62 | } 63 | } 64 | return tr 65 | } 66 | 67 | func appendCertificate(pool *x509.CertPool, path string) error { 68 | pem, err := ioutil.ReadFile(path) 69 | if err != nil { 70 | return err 71 | } 72 | pool.AppendCertsFromPEM(pem) 73 | return nil 74 | } 75 | 76 | func defaultX509Handler() *x509.CertPool { 77 | systemCertPool, err := x509.SystemCertPool() 78 | if err != nil { 79 | logrus.Warn("Failed to load system cert pool. Loading empty one instead.") 80 | systemCertPool = x509.NewCertPool() 81 | } 82 | return systemCertPool 83 | } 84 | -------------------------------------------------------------------------------- /pkg/color/formatter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google Inc. All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package color 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | "os" 23 | "strings" 24 | 25 | "golang.org/x/crypto/ssh/terminal" 26 | ) 27 | 28 | var ( 29 | // ColoredOutput will check if color escape codes should be printed. This can be changed 30 | // for testing to an arbitrary method. 31 | ColoredOutput = coloredOutput 32 | // IsTerminal will check if the specified output stream is a terminal. This can be changed 33 | // for testing to an arbitrary method. 34 | IsTerminal = isTerminal 35 | // NoColor allow to force to not output colors. 36 | NoColor = false 37 | ) 38 | 39 | // Color can be used to format text using ANSI escape codes so it can be printed to 40 | // the terminal in color. 41 | type Color int 42 | 43 | // Define some Color instances that can format text to be displayed to the terminal in color, using ANSI escape codes. 44 | var ( 45 | LightRed = Color(91) 46 | LightGreen = Color(92) 47 | LightYellow = Color(93) 48 | LightBlue = Color(94) 49 | LightPurple = Color(95) 50 | Red = Color(31) 51 | Green = Color(32) 52 | Yellow = Color(33) 53 | Blue = Color(34) 54 | Purple = Color(35) 55 | Cyan = Color(36) 56 | White = Color(37) 57 | // None uses ANSI escape codes to reset all formatting. 58 | None = Color(0) 59 | 60 | // Default default output color for output from container-structure-test to the user 61 | Default = None 62 | ) 63 | 64 | // Fprint wraps the operands in c's ANSI escape codes, and outputs the result to 65 | // out. If out is not a terminal, the escape codes will not be added. 66 | // It returns the number of bytes written and any errors encountered. 67 | func (c Color) Fprint(out io.Writer, a ...interface{}) (n int, err error) { 68 | if ColoredOutput(out) { 69 | return fmt.Fprintf(out, "\033[%dm%s\033[0m", c, fmt.Sprint(a...)) 70 | } 71 | return fmt.Fprint(out, a...) 72 | } 73 | 74 | // Fprintln wraps the operands in c's ANSI escape codes, and outputs the result to 75 | // out, followed by a newline. If out is not a terminal, the escape codes will not be added. 76 | // It returns the number of bytes written and any errors encountered. 77 | func (c Color) Fprintln(out io.Writer, a ...interface{}) (n int, err error) { 78 | if ColoredOutput(out) { 79 | return fmt.Fprintf(out, "\033[%dm%s\033[0m\n", c, strings.TrimSuffix(fmt.Sprintln(a...), "\n")) 80 | } 81 | return fmt.Fprintln(out, a...) 82 | } 83 | 84 | // Fprintf applies formats according to the format specifier (and the optional interfaces provided), 85 | // wraps the result in c's ANSI escape codes, and outputs the result to 86 | // out, followed by a newline. If out is not a terminal, the escape codes will not be added. 87 | // It returns the number of bytes written and any errors encountered. 88 | func (c Color) Fprintf(out io.Writer, format string, a ...interface{}) (n int, err error) { 89 | if ColoredOutput(out) { 90 | return fmt.Fprintf(out, "\033[%dm%s\033[0m", c, fmt.Sprintf(format, a...)) 91 | } 92 | return fmt.Fprintf(out, format, a...) 93 | } 94 | 95 | // ColoredWriteCloser forces printing with colors to an io.WriteCloser. 96 | type ColoredWriteCloser struct { 97 | io.WriteCloser 98 | } 99 | 100 | // ColoredWriter forces printing with colors to an io.Writer. 101 | type ColoredWriter struct { 102 | io.Writer 103 | } 104 | 105 | // OverwriteDefault overwrites default color 106 | func OverwriteDefault(color Color) { 107 | Default = color 108 | } 109 | 110 | func coloredOutput(w io.Writer) bool { 111 | if NoColor { 112 | return false 113 | } 114 | return IsTerminal(w) 115 | } 116 | 117 | // This implementation comes from logrus (https://github.com/sirupsen/logrus/blob/master/terminal_check_notappengine.go), 118 | // unfortunately logrus doesn't expose a public interface we can use to call it. 119 | func isTerminal(w io.Writer) bool { 120 | if _, ok := w.(ColoredWriteCloser); ok { 121 | return true 122 | } 123 | if _, ok := w.(ColoredWriter); ok { 124 | return true 125 | } 126 | 127 | switch v := w.(type) { 128 | case *os.File: 129 | return terminal.IsTerminal(int(v.Fd())) 130 | default: 131 | return false 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /pkg/color/formatter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google Inc. All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package color 18 | 19 | import ( 20 | "bytes" 21 | "io" 22 | "testing" 23 | 24 | "github.com/GoogleContainerTools/container-structure-test/testutil" 25 | ) 26 | 27 | func compareText(t *testing.T, expected, actual string, expectedN int, actualN int, err error) { 28 | t.Helper() 29 | if err != nil { 30 | t.Errorf("did not expect error when formatting text but got %s", err) 31 | } 32 | if actualN != expectedN { 33 | t.Errorf("expected formatter to have written %d bytes but wrote %d", expectedN, actualN) 34 | } 35 | if actual != expected { 36 | t.Errorf("formatting not applied to text. Expected \"%s\" but got \"%s\"", expected, actual) 37 | } 38 | } 39 | 40 | func TestFprint(t *testing.T) { 41 | defer func(f func(io.Writer) bool) { ColoredOutput = f }(ColoredOutput) 42 | ColoredOutput = func(io.Writer) bool { return true } 43 | 44 | var b bytes.Buffer 45 | n, err := Green.Fprint(&b, "It's not easy being") 46 | expected := "\033[32mIt's not easy being\033[0m" 47 | compareText(t, expected, b.String(), 28, n, err) 48 | } 49 | 50 | func TestFprintln(t *testing.T) { 51 | defer func(f func(io.Writer) bool) { ColoredOutput = f }(ColoredOutput) 52 | ColoredOutput = func(io.Writer) bool { return true } 53 | 54 | var b bytes.Buffer 55 | n, err := Green.Fprintln(&b, "2", "less", "chars!") 56 | expected := "\033[32m2 less chars!\033[0m\n" 57 | compareText(t, expected, b.String(), 23, n, err) 58 | } 59 | 60 | func TestFprintf(t *testing.T) { 61 | defer func(f func(io.Writer) bool) { ColoredOutput = f }(ColoredOutput) 62 | ColoredOutput = func(io.Writer) bool { return true } 63 | 64 | var b bytes.Buffer 65 | n, err := Green.Fprintf(&b, "It's been %d %s", 1, "week") 66 | expected := "\033[32mIt's been 1 week\033[0m" 67 | compareText(t, expected, b.String(), 25, n, err) 68 | } 69 | 70 | func TestFprintNoTTY(t *testing.T) { 71 | var b bytes.Buffer 72 | expected := "It's not easy being" 73 | n, err := Green.Fprint(&b, expected) 74 | compareText(t, expected, b.String(), 19, n, err) 75 | } 76 | 77 | func TestFprintlnNoTTY(t *testing.T) { 78 | var b bytes.Buffer 79 | n, err := Green.Fprintln(&b, "2", "less", "chars!") 80 | expected := "2 less chars!\n" 81 | compareText(t, expected, b.String(), 14, n, err) 82 | } 83 | 84 | func TestFprintfNoTTY(t *testing.T) { 85 | var b bytes.Buffer 86 | n, err := Green.Fprintf(&b, "It's been %d %s", 1, "week") 87 | expected := "It's been 1 week" 88 | compareText(t, expected, b.String(), 16, n, err) 89 | } 90 | 91 | func TestFprintTTYNoColor(t *testing.T) { 92 | defer func(f func(io.Writer) bool) { IsTerminal = f }(IsTerminal) 93 | IsTerminal = func(io.Writer) bool { return true } 94 | defer func() { NoColor = false }() 95 | NoColor = true 96 | 97 | var b bytes.Buffer 98 | expected := "It's not easy being" 99 | n, err := Green.Fprint(&b, expected) 100 | compareText(t, expected, b.String(), 19, n, err) 101 | } 102 | 103 | func TestFprintlnTTYNoColor(t *testing.T) { 104 | defer func(f func(io.Writer) bool) { IsTerminal = f }(IsTerminal) 105 | IsTerminal = func(io.Writer) bool { return true } 106 | defer func() { NoColor = false }() 107 | NoColor = true 108 | 109 | var b bytes.Buffer 110 | n, err := Green.Fprintln(&b, "2", "less", "chars!") 111 | expected := "2 less chars!\n" 112 | compareText(t, expected, b.String(), 14, n, err) 113 | } 114 | 115 | func TestFprintfTTYNoColor(t *testing.T) { 116 | defer func(f func(io.Writer) bool) { IsTerminal = f }(IsTerminal) 117 | IsTerminal = func(io.Writer) bool { return true } 118 | defer func() { NoColor = false }() 119 | NoColor = true 120 | 121 | var b bytes.Buffer 122 | n, err := Green.Fprintf(&b, "It's been %d %s", 1, "week") 123 | expected := "It's been 1 week" 124 | compareText(t, expected, b.String(), 16, n, err) 125 | } 126 | 127 | func TestOverwriteDefault(t *testing.T) { 128 | testutil.CheckDeepEqual(t, None, Default) 129 | OverwriteDefault(Red) 130 | testutil.CheckDeepEqual(t, Red, Default) 131 | } 132 | -------------------------------------------------------------------------------- /pkg/config/options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 18 | 19 | type StructureTestOptions struct { 20 | ImagePath string 21 | ImageFromLayout string 22 | DefaultImageTag string 23 | IgnoreRefAnnotation bool 24 | Driver string 25 | Runtime string 26 | Platform string 27 | Metadata string 28 | TestReport string 29 | ConfigFiles []string 30 | 31 | JSON bool 32 | Output unversioned.OutputValue 33 | Pull bool 34 | Save bool 35 | Quiet bool 36 | Force bool 37 | NoColor bool 38 | } 39 | -------------------------------------------------------------------------------- /pkg/drivers/driver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package drivers 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strings" 21 | 22 | "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 23 | ) 24 | 25 | const ( 26 | Docker = "docker" 27 | Tar = "tar" 28 | Host = "host" 29 | ) 30 | 31 | type DriverConfig struct { 32 | Image string // used by Docker/Tar drivers 33 | Save bool // used by Docker/Tar drivers 34 | Metadata string // used by Host driver 35 | Runtime string // used by Docker driver 36 | Platform string // used by Docker driver 37 | RunOpts unversioned.ContainerRunOptions // used by Docker driver 38 | } 39 | 40 | type Driver interface { 41 | Setup(envVars []unversioned.EnvVar, fullCommands [][]string) error 42 | 43 | // Teardown is optional and is only used in the host driver 44 | Teardown(fullCommands [][]string) error 45 | 46 | SetEnv(envVars []unversioned.EnvVar) error 47 | 48 | // given an array of command parts, construct a full command and execute it against the 49 | // current environment. a list of environment variables can be passed to be set in the 50 | // environment before the command is executed. additionally, a boolean flag is passed 51 | // to specify whether or not we care about the output of the command. 52 | ProcessCommand(envVars []unversioned.EnvVar, fullCommand []string) (string, string, int, error) 53 | 54 | StatFile(path string) (os.FileInfo, error) 55 | 56 | ReadFile(path string) ([]byte, error) 57 | 58 | ReadDir(path string) ([]os.FileInfo, error) 59 | 60 | GetConfig() (unversioned.Config, error) 61 | 62 | Destroy() 63 | } 64 | 65 | func InitDriverImpl(driver string) func(DriverConfig) (Driver, error) { 66 | switch driver { 67 | // future drivers will be added here 68 | case Docker: 69 | return NewDockerDriver 70 | case Tar: 71 | return NewTarDriver 72 | case Host: 73 | return NewHostDriver 74 | default: 75 | return nil 76 | } 77 | } 78 | 79 | func convertSliceToMap(slice []string) map[string]string { 80 | // convert slice to map for processing 81 | res := make(map[string]string) 82 | for _, slicePair := range slice { 83 | pair := strings.SplitN(slicePair, "=", 2) 84 | res[pair[0]] = pair[1] 85 | } 86 | return res 87 | } 88 | 89 | func convertMapToSlice(m map[string]string) []string { 90 | res := []string{} 91 | for k, v := range m { 92 | res = append(res, fmt.Sprintf("%s=%s", k, v)) 93 | } 94 | return res 95 | } 96 | -------------------------------------------------------------------------------- /pkg/drivers/host_driver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package drivers 16 | 17 | import ( 18 | "encoding/json" 19 | "io/fs" 20 | "os" 21 | "os/exec" 22 | "strings" 23 | "syscall" 24 | 25 | "github.com/pkg/errors" 26 | "github.com/sirupsen/logrus" 27 | 28 | "bytes" 29 | 30 | "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 31 | v1 "github.com/google/go-containerregistry/pkg/v1" 32 | ) 33 | 34 | type HostDriver struct { 35 | ConfigPath string // path to image metadata config on host fs 36 | GlobalVars []unversioned.EnvVar 37 | } 38 | 39 | func NewHostDriver(args DriverConfig) (Driver, error) { 40 | return &HostDriver{ 41 | ConfigPath: args.Metadata, 42 | }, nil 43 | } 44 | 45 | func (d *HostDriver) Destroy() { 46 | // since we're running on the host, don't do anything 47 | } 48 | 49 | func (d *HostDriver) Setup(envVars []unversioned.EnvVar, fullCommands [][]string) error { 50 | // since we're running on the host, we'll provide an optional teardown field for 51 | // each test that will allow users to undo the setup they did. 52 | // keep track of the original env vars so we can reset later. 53 | d.GlobalVars = SetEnvVars(envVars) 54 | for _, cmd := range fullCommands { 55 | _, _, _, err := d.ProcessCommand(nil, cmd) 56 | if err != nil { 57 | return err 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | func (d *HostDriver) Teardown(fullCommands [][]string) error { 64 | // since we're running on the host, we'll provide an optional teardown field for each test that 65 | // will allow users to undo the setup they did. 66 | ResetEnvVars(d.GlobalVars) 67 | for _, cmd := range fullCommands { 68 | _, _, _, err := d.ProcessCommand(nil, cmd) 69 | if err != nil { 70 | return err 71 | } 72 | } 73 | return nil 74 | } 75 | 76 | func (d *HostDriver) SetEnv(envVars []unversioned.EnvVar) error { 77 | for _, envVar := range envVars { 78 | if err := os.Setenv(envVar.Key, os.ExpandEnv(envVar.Value)); err != nil { 79 | return err 80 | } 81 | } 82 | return nil 83 | } 84 | 85 | // given a list of environment variable key/value pairs, set these in the current environment. 86 | // also, keep track of the previous values of these vars to reset after test execution. 87 | func SetEnvVars(envVars []unversioned.EnvVar) []unversioned.EnvVar { 88 | var originalVars []unversioned.EnvVar 89 | for _, envVar := range envVars { 90 | originalVars = append(originalVars, unversioned.EnvVar{envVar.Key, os.Getenv(envVar.Key), envVar.IsRegex}) 91 | if err := os.Setenv(envVar.Key, os.ExpandEnv(envVar.Value)); err != nil { 92 | logrus.Errorf("Error setting env var: %s", err) 93 | } 94 | } 95 | return originalVars 96 | } 97 | 98 | func ResetEnvVars(envVars []unversioned.EnvVar) { 99 | for _, envVar := range envVars { 100 | var err error 101 | if envVar.Value == "" { 102 | // if the previous value was empty string, the variable did not 103 | // exist in the environment; unset it 104 | err = os.Unsetenv(envVar.Key) 105 | } else { 106 | // otherwise, set it back to its previous value 107 | err = os.Setenv(envVar.Key, envVar.Value) 108 | } 109 | if err != nil { 110 | logrus.Errorf("error resetting env var: %s", err) 111 | } 112 | } 113 | } 114 | 115 | func (d *HostDriver) ProcessCommand(envVars []unversioned.EnvVar, fullCommand []string) (string, string, int, error) { 116 | originalVars := SetEnvVars(envVars) 117 | defer ResetEnvVars(originalVars) 118 | cmd := exec.Command(fullCommand[0], fullCommand[1:]...) 119 | var stdout, stderr bytes.Buffer 120 | cmd.Stdout = &stdout 121 | cmd.Stderr = &stderr 122 | 123 | exitCode := 0 124 | 125 | if err := cmd.Start(); err != nil { 126 | logrus.Fatalf("error starting command: %v", err) 127 | } 128 | 129 | if err := cmd.Wait(); err != nil { 130 | if exiterr, ok := err.(*exec.ExitError); ok { 131 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 132 | exitCode = status.ExitStatus() 133 | } 134 | } else { 135 | return "", "", -1, errors.Wrap(err, "Error when retrieving exit code") 136 | } 137 | } 138 | logrus.Debugf("command output: %s", stdout.String()) 139 | return stdout.String(), stderr.String(), exitCode, nil 140 | } 141 | 142 | func (d *HostDriver) StatFile(path string) (os.FileInfo, error) { 143 | return os.Lstat(path) 144 | } 145 | 146 | func (d *HostDriver) ReadFile(path string) ([]byte, error) { 147 | return os.ReadFile(path) 148 | } 149 | 150 | func (d *HostDriver) ReadDir(path string) ([]os.FileInfo, error) { 151 | entries, err := os.ReadDir(path) 152 | if err != nil { 153 | return nil, err 154 | } 155 | infos := make([]fs.FileInfo, 0, len(entries)) 156 | for _, entry := range entries { 157 | info, err := entry.Info() 158 | if err != nil { 159 | return nil, err 160 | } 161 | infos = append(infos, info) 162 | } 163 | return infos, nil 164 | } 165 | 166 | func (d *HostDriver) GetConfig() (unversioned.Config, error) { 167 | file, err := os.ReadFile(d.ConfigPath) 168 | if err != nil { 169 | return unversioned.Config{}, errors.Wrap(err, "Error retrieving config") 170 | } 171 | 172 | var metadata v1.ConfigFile 173 | 174 | json.Unmarshal(file, &metadata) 175 | config := metadata.Config 176 | 177 | // docker provides these as maps (since they can be mapped in docker run commands) 178 | // since this will never be the case when built through a dockerfile, we convert to list of strings 179 | volumes := []string{} 180 | for v := range config.Volumes { 181 | volumes = append(volumes, v) 182 | } 183 | 184 | ports := []string{} 185 | for p := range config.ExposedPorts { 186 | // docker always appends the protocol to the port, so this is safe 187 | ports = append(ports, strings.Split(p, "/")[0]) 188 | } 189 | 190 | return unversioned.Config{ 191 | Env: convertSliceToMap(config.Env), 192 | Entrypoint: config.Entrypoint, 193 | Cmd: config.Cmd, 194 | Volumes: volumes, 195 | Workdir: config.WorkingDir, 196 | ExposedPorts: ports, 197 | Labels: config.Labels, 198 | User: config.User, 199 | }, nil 200 | } 201 | -------------------------------------------------------------------------------- /pkg/drivers/tar_driver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package drivers 16 | 17 | import ( 18 | "io/fs" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | 23 | "github.com/pkg/errors" 24 | "github.com/sirupsen/logrus" 25 | 26 | "github.com/GoogleContainerTools/container-structure-test/internal/pkgutil" 27 | "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 28 | v1 "github.com/google/go-containerregistry/pkg/v1" 29 | "github.com/google/go-containerregistry/pkg/v1/mutate" 30 | ) 31 | 32 | type TarDriver struct { 33 | Image pkgutil.Image 34 | Save bool 35 | } 36 | 37 | func NewTarDriver(args DriverConfig) (Driver, error) { 38 | if pkgutil.IsTar(args.Image) { 39 | // tar provided, so don't provide any prefix. container-diff can figure this out. 40 | image, err := pkgutil.GetImageForName(args.Image) 41 | if err != nil { 42 | return nil, errors.Wrap(err, "processing tar image reference") 43 | } 44 | return &TarDriver{ 45 | Image: image, 46 | Save: args.Save, 47 | }, nil 48 | } 49 | // try the local docker daemon first 50 | image, err := pkgutil.GetImageForName("daemon://" + args.Image) 51 | if err == nil { 52 | logrus.Debugf("image found in local docker daemon") 53 | return &TarDriver{ 54 | Image: image, 55 | Save: args.Save, 56 | }, nil 57 | } 58 | 59 | // image not found in local daemon, so try remote. 60 | logrus.Infof("unable to retrieve image locally: %s", err) 61 | image, err = pkgutil.GetImageForName("remote://" + args.Image) 62 | if err != nil { 63 | return nil, errors.Wrap(err, "retrieving image") 64 | } 65 | return &TarDriver{ 66 | Image: image, 67 | Save: args.Save, 68 | }, nil 69 | } 70 | 71 | func (d *TarDriver) Destroy() { 72 | if !d.Save { 73 | pkgutil.CleanupImage(d.Image) 74 | } 75 | } 76 | 77 | func (d *TarDriver) SetEnv(envVars []unversioned.EnvVar) error { 78 | configFile, err := d.Image.Image.ConfigFile() 79 | if err != nil { 80 | return errors.Wrap(err, "retrieving image config") 81 | } 82 | config := configFile.Config 83 | env := convertSliceToMap(config.Env) 84 | for _, envVar := range envVars { 85 | env[envVar.Key] = envVar.Value 86 | } 87 | newConfig := v1.Config{ 88 | AttachStderr: config.AttachStderr, 89 | AttachStdin: config.AttachStdin, 90 | AttachStdout: config.AttachStdout, 91 | Cmd: config.Cmd, 92 | Domainname: config.Domainname, 93 | Entrypoint: config.Entrypoint, 94 | Env: convertMapToSlice(env), 95 | Hostname: config.Hostname, 96 | Image: config.Image, 97 | Labels: config.Labels, 98 | OnBuild: config.OnBuild, 99 | OpenStdin: config.OpenStdin, 100 | StdinOnce: config.StdinOnce, 101 | Tty: config.Tty, 102 | User: config.User, 103 | Volumes: config.Volumes, 104 | WorkingDir: config.WorkingDir, 105 | ExposedPorts: config.ExposedPorts, 106 | ArgsEscaped: config.ArgsEscaped, 107 | NetworkDisabled: config.NetworkDisabled, 108 | MacAddress: config.MacAddress, 109 | StopSignal: config.StopSignal, 110 | Shell: config.Shell, 111 | } 112 | newImg, err := mutate.Config(d.Image.Image, newConfig) 113 | if err != nil { 114 | return errors.Wrap(err, "setting new config on image") 115 | } 116 | newImage := pkgutil.Image{ 117 | Image: newImg, 118 | Source: d.Image.Source, 119 | FSPath: d.Image.FSPath, 120 | Digest: d.Image.Digest, 121 | Layers: d.Image.Layers, 122 | } 123 | d.Image = newImage 124 | return nil 125 | } 126 | 127 | func (d *TarDriver) Setup(_ []unversioned.EnvVar, _ [][]string) error { 128 | // this driver is unable to process commands, inform user and fail. 129 | return errors.New("Tar driver is unable to process commands, please use a different driver") 130 | } 131 | 132 | func (d *TarDriver) Teardown(_ [][]string) error { 133 | return errors.New("Tar driver is unable to process commands, please use a different driver") 134 | } 135 | 136 | func (d *TarDriver) ProcessCommand(_ []unversioned.EnvVar, _ []string) (string, string, int, error) { 137 | // this driver is unable to process commands, inform user and fail. 138 | return "", "", -1, errors.New("Tar driver is unable to process commands, please use a different driver") 139 | } 140 | 141 | func (d *TarDriver) StatFile(path string) (os.FileInfo, error) { 142 | return os.Lstat(filepath.Join(d.Image.FSPath, path)) 143 | } 144 | 145 | func (d *TarDriver) ReadFile(path string) ([]byte, error) { 146 | return os.ReadFile(filepath.Join(d.Image.FSPath, path)) 147 | } 148 | 149 | func (d *TarDriver) ReadDir(path string) ([]os.FileInfo, error) { 150 | entries, err := os.ReadDir(filepath.Join(d.Image.FSPath, path)) 151 | if err != nil { 152 | return nil, err 153 | } 154 | infos := make([]fs.FileInfo, 0, len(entries)) 155 | for _, entry := range entries { 156 | info, err := entry.Info() 157 | if err != nil { 158 | return nil, err 159 | } 160 | infos = append(infos, info) 161 | } 162 | return infos, nil 163 | } 164 | 165 | func (d *TarDriver) GetConfig() (unversioned.Config, error) { 166 | configFile, err := d.Image.Image.ConfigFile() 167 | if err != nil { 168 | return unversioned.Config{}, errors.Wrap(err, "retrieving config file") 169 | } 170 | config := configFile.Config 171 | 172 | // docker provides these as maps (since they can be mapped in docker run commands) 173 | // since this will never be the case when built through a dockerfile, we convert to list of strings 174 | volumes := []string{} 175 | for v := range config.Volumes { 176 | volumes = append(volumes, v) 177 | } 178 | 179 | ports := []string{} 180 | for p := range config.ExposedPorts { 181 | // docker always appends the protocol to the port, so this is safe 182 | ports = append(ports, strings.Split(p, "/")[0]) 183 | } 184 | 185 | return unversioned.Config{ 186 | Env: convertSliceToMap(config.Env), 187 | Entrypoint: config.Entrypoint, 188 | Cmd: config.Cmd, 189 | Volumes: volumes, 190 | Workdir: config.WorkingDir, 191 | ExposedPorts: ports, 192 | Labels: config.Labels, 193 | User: config.User, 194 | }, nil 195 | } 196 | -------------------------------------------------------------------------------- /pkg/output/output.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package output 16 | 17 | import ( 18 | "encoding/json" 19 | "encoding/xml" 20 | "fmt" 21 | "io" 22 | "path/filepath" 23 | "strings" 24 | "time" 25 | 26 | "github.com/pkg/errors" 27 | 28 | color "github.com/GoogleContainerTools/container-structure-test/pkg/color" 29 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 30 | ) 31 | 32 | var bannerLength = 27 // default banner length 33 | 34 | func OutputResult(out io.Writer, result *types.TestResult) { 35 | color.Default.Fprintf(out, "=== RUN: %s\n", result.Name) 36 | if result.Pass { 37 | color.Green.Fprintln(out, "--- PASS") 38 | } else { 39 | color.Red.Fprintln(out, "--- FAIL") 40 | } 41 | color.Default.Fprintf(out, "duration: %s\n", result.Duration.String()) 42 | if result.Stdout != "" { 43 | color.Blue.Fprintf(out, "stdout: %s\n", result.Stdout) 44 | } 45 | if result.Stderr != "" { 46 | color.Blue.Fprintf(out, "stderr: %s\n", result.Stderr) 47 | } 48 | for _, s := range result.Errors { 49 | color.Yellow.Fprintf(out, "Error: %s\n", s) 50 | } 51 | } 52 | 53 | func Banner(out io.Writer, filename string) { 54 | fileStr := fmt.Sprintf("====== Test file: %s ======", filepath.Base(filename)) 55 | bannerLength = len(fileStr) 56 | color.Purple.Fprintln(out, "\n"+strings.Repeat("=", bannerLength)) 57 | color.Purple.Fprintln(out, fileStr) 58 | color.Purple.Fprintln(out, strings.Repeat("=", bannerLength)) 59 | } 60 | 61 | func FinalResults(out io.Writer, format types.OutputValue, result types.SummaryObject) error { 62 | if format == types.Json { 63 | res, err := json.Marshal(result) 64 | if err != nil { 65 | return errors.Wrap(err, "marshalling json") 66 | } 67 | res = append(res, []byte("\n")...) 68 | _, err = out.Write(res) 69 | return err 70 | } 71 | 72 | if format == types.Junit { 73 | junit_cases := []*types.JUnitTestCase{} 74 | for elem := range result.Results { 75 | r := result.Results[elem] 76 | junit_cases = append(junit_cases, &types.JUnitTestCase{ 77 | Name: r.Name, 78 | Errors: r.Errors, 79 | Duration: r.Duration.Seconds(), 80 | Stdout: r.Stdout, 81 | Stderr: r.Stderr, 82 | }) 83 | } 84 | junit_result := struct { 85 | XMLName xml.Name `xml:"testsuites"` 86 | Pass int `xml:"-"` 87 | Fail int `xml:"failures,attr"` 88 | Total int `xml:"tests,attr"` 89 | Duration float64 `xml:"time,attr"` 90 | TestSuite types.JUnitTestSuite `xml:"testsuite"` 91 | }{ 92 | XMLName: result.XMLName, 93 | Pass: result.Pass, 94 | Fail: result.Fail, 95 | Total: result.Total, 96 | Duration: time.Duration.Seconds(result.Duration), // JUnit expects durations as float of seconds 97 | TestSuite: types.JUnitTestSuite{ 98 | Name: "container-structure-test.test", 99 | Results: junit_cases, 100 | }, 101 | } 102 | res := []byte(strings.ReplaceAll(xml.Header, "\n", "")) 103 | marshalled, err := xml.Marshal(junit_result) 104 | if err != nil { 105 | return errors.Wrap(err, "marshalling xml") 106 | } 107 | res = append(res, marshalled...) 108 | res = append(res, []byte("\n")...) 109 | _, err = out.Write(res) 110 | return err 111 | } 112 | 113 | if bannerLength%2 == 0 { 114 | bannerLength++ 115 | } 116 | if result.Total == 0 { 117 | color.Red.Fprintln(out, "No tests run! Check config file format.") 118 | return nil 119 | } 120 | color.Default.Fprintln(out, "") 121 | color.Default.Fprintln(out, strings.Repeat("=", bannerLength)) 122 | color.Default.Fprintf(out, "%s RESULTS %s\n", strings.Repeat("=", (bannerLength-9)/2), strings.Repeat("=", (bannerLength-9)/2)) 123 | color.Default.Fprintln(out, strings.Repeat("=", bannerLength)) 124 | color.LightGreen.Fprintf(out, "Passes: %d\n", result.Pass) 125 | color.LightRed.Fprintf(out, "Failures: %d\n", result.Fail) 126 | color.Default.Fprintf(out, "Duration: %s\n", result.Duration.String()) 127 | color.Cyan.Fprintf(out, "Total tests: %d\n", result.Total) 128 | color.Default.Fprintln(out, "") 129 | if result.Fail == 0 { 130 | color.Green.Fprintln(out, "PASS") 131 | } else { 132 | color.Red.Fprintln(out, "FAIL") 133 | } 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /pkg/output/output_test.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 10 | ) 11 | 12 | func TestFinalResults(t *testing.T) { 13 | t.Parallel() 14 | 15 | result := unversioned.SummaryObject{ 16 | Pass: 1, 17 | Fail: 1, 18 | Total: 2, 19 | Duration: time.Duration(2), 20 | Results: []*unversioned.TestResult{ 21 | { 22 | Name: "my first test", 23 | Pass: true, 24 | Stdout: "it works!", 25 | Stderr: "", 26 | Duration: time.Duration(1), 27 | }, 28 | { 29 | Name: "my fail", 30 | Pass: false, 31 | Stdout: "", 32 | Stderr: "this failed", 33 | Errors: []string{"this failed because of that"}, 34 | Duration: time.Duration(1), 35 | }, 36 | }, 37 | } 38 | 39 | var finalResultsTests = []struct { 40 | actual *bytes.Buffer 41 | format unversioned.OutputValue 42 | expected string 43 | }{ 44 | { 45 | actual: bytes.NewBuffer([]byte{}), 46 | format: unversioned.Junit, 47 | expected: `it works!this failed because of thatthis failed`, 48 | }, 49 | { 50 | actual: bytes.NewBuffer([]byte{}), 51 | format: unversioned.Json, 52 | expected: `{"Pass":1,"Fail":1,"Total":2,"Duration":2,"Results":[{"Name":"my first test","Pass":true,"Stdout":"it works!","Duration":1},{"Name":"my fail","Pass":false,"Stderr":"this failed","Errors":["this failed because of that"],"Duration":1}]}`, 53 | }, 54 | } 55 | 56 | for _, test := range finalResultsTests { 57 | test := test 58 | 59 | t.Run(test.format.String(), func(t *testing.T) { 60 | t.Parallel() 61 | 62 | FinalResults(test.actual, test.format, result) 63 | 64 | if strings.TrimSpace(test.actual.String()) != test.expected { 65 | t.Errorf("expected %s but got %s", test.expected, test.actual) 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/types/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package types 16 | 17 | import ( 18 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 19 | "github.com/GoogleContainerTools/container-structure-test/pkg/types/v1" 20 | "github.com/GoogleContainerTools/container-structure-test/pkg/types/v2" 21 | ) 22 | 23 | type StructureTest interface { 24 | SetDriverImpl(func(drivers.DriverConfig) (drivers.Driver, error), drivers.DriverConfig) 25 | NewDriver() (drivers.Driver, error) 26 | RunAll(chan interface{}, string) 27 | } 28 | 29 | var SchemaVersions map[string]func() StructureTest = map[string]func() StructureTest{ 30 | "1.0.0": func() StructureTest { return new(v1.StructureTest) }, 31 | "2.0.0": func() StructureTest { return new(v2.StructureTest) }, 32 | } 33 | 34 | type SchemaVersion struct { 35 | SchemaVersion string `yaml:"schemaVersion"` 36 | } 37 | 38 | type Unmarshaller func([]byte, interface{}) error 39 | -------------------------------------------------------------------------------- /pkg/types/unversioned/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unversioned 16 | 17 | import ( 18 | "encoding/xml" 19 | "fmt" 20 | "strings" 21 | "time" 22 | ) 23 | 24 | type EnvVar struct { 25 | Key string 26 | Value string 27 | IsRegex bool `yaml:"isRegex"` 28 | } 29 | 30 | type Label struct { 31 | Key string 32 | Value string 33 | IsRegex bool `yaml:"isRegex"` 34 | } 35 | 36 | type Config struct { 37 | Env map[string]string 38 | Entrypoint []string 39 | Cmd []string 40 | Volumes []string 41 | Workdir string 42 | ExposedPorts []string 43 | Labels map[string]string 44 | User string 45 | } 46 | 47 | type ContainerRunOptions struct { 48 | User string 49 | Privileged bool 50 | TTY bool `yaml:"allocateTty"` 51 | EnvVars []string `yaml:"envVars"` 52 | EnvFile string `yaml:"envFile"` 53 | Capabilities []string 54 | BindMounts []string `yaml:"bindMounts"` 55 | } 56 | 57 | func (opts *ContainerRunOptions) IsSet() bool { 58 | return len(opts.User) != 0 || 59 | opts.Privileged || 60 | opts.TTY || 61 | len(opts.EnvFile) > 0 || 62 | (opts.EnvVars != nil && len(opts.EnvVars) > 0) || 63 | (opts.Capabilities != nil && len(opts.Capabilities) > 0) || 64 | (opts.BindMounts != nil && len(opts.BindMounts) > 0) 65 | } 66 | 67 | type TestResult struct { 68 | Name string `xml:"name,attr"` 69 | Pass bool `xml:"-"` 70 | Stdout string `json:",omitempty" xml:"-"` 71 | Stderr string `json:",omitempty" xml:"-"` 72 | Errors []string `json:",omitempty" xml:"failure"` 73 | Duration time.Duration `xml:"time,attr"` 74 | } 75 | 76 | func (t *TestResult) String() string { 77 | strRepr := fmt.Sprintf("\nTest Name:%s", t.Name) 78 | testStatus := "Fail" 79 | if t.IsPass() { 80 | testStatus = "Pass" 81 | } 82 | strRepr += fmt.Sprintf("\nTest Status:%s", testStatus) 83 | if t.Stdout != "" { 84 | strRepr += fmt.Sprintf("\nStdout:%s", t.Stdout) 85 | } 86 | if t.Stderr != "" { 87 | strRepr += fmt.Sprintf("\nStderr:%s", t.Stderr) 88 | } 89 | strRepr += fmt.Sprintf("\nErrors:%s\n", strings.Join(t.Errors, ",")) 90 | strRepr += fmt.Sprintf("\nDuration:%s\n", t.Duration.String()) 91 | return strRepr 92 | } 93 | 94 | func (t *TestResult) Error(s string) { 95 | t.Errors = append(t.Errors, s) 96 | } 97 | 98 | func (t *TestResult) Errorf(s string, args ...interface{}) { 99 | t.Errors = append(t.Errors, fmt.Sprintf(s, args...)) 100 | } 101 | 102 | func (t *TestResult) Fail() { 103 | t.Pass = false 104 | } 105 | 106 | func (t *TestResult) IsPass() bool { 107 | return t.Pass 108 | } 109 | 110 | type SummaryObject struct { 111 | XMLName xml.Name `json:"-" xml:"testsuites"` 112 | Pass int `xml:"-"` 113 | Fail int `xml:"failures,attr"` 114 | Total int `xml:"tests,attr"` 115 | Duration time.Duration `xml:"time,attr"` 116 | Results []*TestResult `json:",omitempty" xml:"testsuite>testcase"` 117 | } 118 | 119 | type JUnitTestSuite struct { 120 | Name string `xml:"name,attr"` 121 | Results []*JUnitTestCase `xml:"testcase"` 122 | } 123 | 124 | type JUnitTestCase struct { 125 | Name string `xml:"name,attr"` 126 | Errors []string `xml:"failure"` 127 | Duration float64 `xml:"time,attr"` 128 | Stdout string `xml:"system-out"` 129 | Stderr string `xml:"system-err"` 130 | } 131 | 132 | type OutputValue int 133 | 134 | const ( 135 | Text OutputValue = iota 136 | Json 137 | Junit 138 | ) 139 | 140 | func (o OutputValue) String() string { 141 | return [...]string{"text", "json", "junit"}[o] 142 | } 143 | 144 | func (o OutputValue) Type() string { 145 | return "string" 146 | } 147 | 148 | func (o *OutputValue) Set(value string) error { 149 | switch value { 150 | case "text": 151 | *o = Text 152 | case "json": 153 | *o = Json 154 | case "junit": 155 | *o = Junit 156 | default: 157 | return fmt.Errorf("unsupported format %s: please select from `text`, `json`, or `junit`", value) 158 | } 159 | 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /pkg/types/v1/command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/sirupsen/logrus" 22 | 23 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 24 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 25 | "github.com/GoogleContainerTools/container-structure-test/pkg/utils" 26 | ) 27 | 28 | type CommandTest struct { 29 | Name string `yaml:"name"` 30 | Setup [][]string `yaml:"setup"` 31 | Teardown [][]string `yaml:"teardown"` 32 | EnvVars []types.EnvVar `yaml:"envVars"` 33 | ExitCode int `yaml:"exitCode"` 34 | Command []string `yaml:"command"` 35 | ExpectedOutput []string `yaml:"expectedOutput"` 36 | ExcludedOutput []string `yaml:"excludedOutput"` 37 | ExpectedError []string `yaml:"expectedError"` 38 | ExcludedError []string `yaml:"excludedError" ` // excluded error from running command 39 | } 40 | 41 | func (ct *CommandTest) Validate() error { 42 | if ct.Name == "" { 43 | return fmt.Errorf("Please provide a valid name for every test") 44 | } 45 | if ct.Command == nil || len(ct.Command) == 0 { 46 | return fmt.Errorf("Please provide a valid command to run for test %s", ct.Name) 47 | } 48 | if ct.Setup != nil { 49 | for _, c := range ct.Setup { 50 | if len(c) == 0 { 51 | return fmt.Errorf("Error in setup command configuration encountered; please check formatting and remove all empty setup commands") 52 | } 53 | } 54 | } 55 | if ct.Teardown != nil { 56 | for _, c := range ct.Teardown { 57 | if len(c) == 0 { 58 | return fmt.Errorf("Error in teardown command configuration encountered; please check formatting and remove all empty teardown commands") 59 | } 60 | } 61 | } 62 | if ct.EnvVars != nil { 63 | for _, envVar := range ct.EnvVars { 64 | if envVar.Key == "" || envVar.Value == "" { 65 | return fmt.Errorf("Please provide non-empty keys and values for all specified env vars") 66 | } 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | func (ct *CommandTest) Run(driver drivers.Driver) *types.TestResult { 73 | logrus.Debug(ct.LogName()) 74 | config, err := driver.GetConfig() 75 | if err != nil { 76 | logrus.Errorf("error retrieving image config: %s", err.Error()) 77 | } 78 | start := time.Now() 79 | stdout, stderr, exitcode, err := driver.ProcessCommand(ct.EnvVars, utils.SubstituteEnvVars(ct.Command, config.Env)) 80 | end := time.Now() 81 | duration := end.Sub(start) 82 | result := &types.TestResult{ 83 | Name: ct.LogName(), 84 | Pass: true, 85 | Errors: make([]string, 0), 86 | Stderr: stderr, 87 | Stdout: stdout, 88 | Duration: duration, 89 | } 90 | if err != nil { 91 | result.Fail() 92 | result.Error(err.Error()) 93 | return result 94 | } 95 | 96 | ct.CheckOutput(result, stdout, stderr, exitcode) 97 | return result 98 | } 99 | 100 | func (ct *CommandTest) LogName() string { 101 | return fmt.Sprintf("Command Test: %s", ct.Name) 102 | } 103 | 104 | func (ct *CommandTest) CheckOutput(result *types.TestResult, stdout string, stderr string, exitCode int) { 105 | for _, errStr := range ct.ExpectedError { 106 | if !utils.CompileAndRunRegex(errStr, stderr, true) { 107 | result.Errorf("Expected string '%s' not found in error", errStr) 108 | result.Fail() 109 | } 110 | } 111 | for _, errStr := range ct.ExcludedError { 112 | if !utils.CompileAndRunRegex(errStr, stderr, false) { 113 | result.Errorf("Excluded string '%s' found in error", errStr) 114 | result.Fail() 115 | } 116 | } 117 | for _, outStr := range ct.ExpectedOutput { 118 | if !utils.CompileAndRunRegex(outStr, stdout, true) { 119 | result.Errorf("Expected string '%s' not found in output", outStr) 120 | result.Fail() 121 | } 122 | } 123 | for _, outStr := range ct.ExcludedOutput { 124 | if !utils.CompileAndRunRegex(outStr, stdout, false) { 125 | result.Errorf("Excluded string '%s' found in output", outStr) 126 | result.Fail() 127 | } 128 | } 129 | if ct.ExitCode != exitCode { 130 | result.Errorf("Test '%s' exited with incorrect error code. Expected: %d, Actual: %d", ct.Name, ct.ExitCode, exitCode) 131 | result.Fail() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /pkg/types/v1/file_content.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 21 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 22 | "github.com/GoogleContainerTools/container-structure-test/pkg/utils" 23 | ) 24 | 25 | type FileContentTest struct { 26 | Name string `yaml:"name"` // name of test 27 | Path string `yaml:"path"` // file to check existence of 28 | ExpectedContents []string `yaml:"expectedContents"` // list of expected contents of file 29 | ExcludedContents []string `yaml:"excludedContents"` // list of excluded contents of file 30 | } 31 | 32 | func (ft FileContentTest) Validate() error { 33 | if ft.Name == "" { 34 | return fmt.Errorf("Please provide a valid name for every test") 35 | } 36 | if ft.Path == "" { 37 | return fmt.Errorf("Please provide a valid file path for test %s", ft.Name) 38 | } 39 | return nil 40 | } 41 | 42 | func (ft FileContentTest) LogName() string { 43 | return fmt.Sprintf("File Content Test: %s", ft.Name) 44 | } 45 | 46 | func (ft FileContentTest) Run(driver drivers.Driver) *types.TestResult { 47 | result := &types.TestResult{ 48 | Name: ft.LogName(), 49 | Pass: true, 50 | Errors: make([]string, 0), 51 | } 52 | actualContents, err := driver.ReadFile(ft.Path) 53 | if err != nil { 54 | result.Errorf("Failed to open %s. Error: %s", ft.Path, err) 55 | result.Fail() 56 | return result 57 | } 58 | 59 | contents := string(actualContents) 60 | 61 | for _, s := range ft.ExpectedContents { 62 | if !utils.CompileAndRunRegex(s, contents, true) { 63 | result.Errorf("Expected string '%s' not found in file content string '%s'", s, contents) 64 | result.Fail() 65 | } 66 | } 67 | for _, s := range ft.ExcludedContents { 68 | if !utils.CompileAndRunRegex(s, contents, false) { 69 | result.Errorf("Expected string '%s' not found in file content string '%s'", s, contents) 70 | result.Fail() 71 | } 72 | } 73 | return result 74 | } 75 | -------------------------------------------------------------------------------- /pkg/types/v1/file_existence.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/pkg/errors" 22 | "github.com/sirupsen/logrus" 23 | 24 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 25 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 26 | ) 27 | 28 | type FileExistenceTest struct { 29 | Name string `yaml:"name"` // name of test 30 | Path string `yaml:"path"` // file to check existence of 31 | ShouldExist bool `yaml:"shouldExist"` // whether or not the file should exist 32 | Permissions string `yaml:"permissions"` // expected Unix permission string of the file, e.g. drwxrwxrwx 33 | } 34 | 35 | func (fe FileExistenceTest) MarshalYAML() (interface{}, error) { 36 | return FileExistenceTest{ShouldExist: true}, nil 37 | } 38 | 39 | func (fe *FileExistenceTest) UnmarshalYAML(unmarshal func(interface{}) error) error { 40 | // Create a type alias and call unmarshal on this type to unmarshal the yaml text into 41 | // struct, since calling unmarshal on FileExistenceTest will result in an infinite loop. 42 | type FileExistenceTestHolder FileExistenceTest 43 | holder := FileExistenceTestHolder{ 44 | ShouldExist: true, 45 | } 46 | err := unmarshal(&holder) 47 | if err != nil { 48 | return err 49 | } 50 | *fe = FileExistenceTest(holder) 51 | return nil 52 | } 53 | 54 | func (ft FileExistenceTest) Validate() error { 55 | if ft.Name == "" { 56 | return fmt.Errorf("Please provide a valid name for every test") 57 | } 58 | if ft.Path == "" { 59 | fmt.Errorf("Please provide a valid file path for test %s", ft.Name) 60 | } 61 | return nil 62 | } 63 | 64 | func (ft FileExistenceTest) LogName() string { 65 | return fmt.Sprintf("File Existence Test: %s", ft.Name) 66 | } 67 | 68 | func (ft FileExistenceTest) Run(driver drivers.Driver) *types.TestResult { 69 | result := &types.TestResult{ 70 | Name: ft.LogName(), 71 | Pass: true, 72 | Errors: make([]string, 0), 73 | } 74 | logrus.Info(ft.LogName()) 75 | var info os.FileInfo 76 | info, err := driver.StatFile(ft.Path) 77 | if info == nil && ft.ShouldExist { 78 | result.Errorf(errors.Wrap(err, "Error examining file in container").Error()) 79 | result.Fail() 80 | return result 81 | } 82 | if ft.ShouldExist && err != nil { 83 | result.Errorf("File %s should exist but does not, got error: %s", ft.Path, err) 84 | result.Fail() 85 | } else if !ft.ShouldExist && err == nil { 86 | result.Errorf("File %s should not exist but does", ft.Path) 87 | result.Fail() 88 | } 89 | 90 | // Next assertions don't make sense if the file doesn't exist. 91 | if !ft.ShouldExist { 92 | return result 93 | } 94 | 95 | if ft.Permissions != "" && info != nil { 96 | perms := info.Mode() 97 | if perms.String() != ft.Permissions { 98 | result.Errorf("%s has incorrect permissions. Expected: %s, Actual: %s", ft.Path, ft.Permissions, perms.String()) 99 | result.Fail() 100 | } 101 | } 102 | return result 103 | } 104 | -------------------------------------------------------------------------------- /pkg/types/v1/licenses.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1 16 | 17 | import ( 18 | "fmt" 19 | "path" 20 | "strings" 21 | 22 | "github.com/sirupsen/logrus" 23 | 24 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 25 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 26 | "github.com/GoogleContainerTools/container-structure-test/pkg/utils" 27 | ) 28 | 29 | type LicenseTest struct { 30 | Debian bool `yaml:"debian"` 31 | Files []string `yaml:"files"` 32 | } 33 | 34 | var ( 35 | // Whitelist is the list of packages that we want to automatically pass this 36 | // check even if it would normally fail for one reason or another. 37 | whitelist = []string{"libgnutls30"} 38 | 39 | // Blacklist is the set of words that, if contained in a license file, should cause a failure. 40 | // This will most likely just be names of unsupported licenses. 41 | blacklist = []string{"AGPL", "WTFPL"} 42 | ) 43 | 44 | func checkFile(licenseFile string, driver drivers.Driver) error { 45 | // Read through the copyright file and make sure don't have an unauthorized license 46 | license, err := driver.ReadFile(licenseFile) 47 | if err != nil { 48 | return fmt.Errorf("Error reading license file for %s: %s", licenseFile, err.Error()) 49 | } 50 | contents := strings.ToUpper(string(license)) 51 | for _, b := range blacklist { 52 | if strings.Contains(contents, b) { 53 | return fmt.Errorf("Invalid license for %s, license contains %s", licenseFile, b) 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | func (lt LicenseTest) Run(driver drivers.Driver) *types.TestResult { 60 | result := &types.TestResult{ 61 | Name: lt.LogName(), 62 | Pass: true, 63 | Errors: make([]string, 0), 64 | } 65 | logrus.Info(lt.LogName()) 66 | if lt.Debian { 67 | root := utils.DebianRoot 68 | packages, err := driver.ReadDir(root) 69 | if err != nil { 70 | result.Errorf("Error reading directory: %s", err) 71 | result.Fail() 72 | return result 73 | } 74 | for _, p := range packages { 75 | if !p.IsDir() { 76 | continue 77 | } 78 | logrus.Infof(p.Name()) 79 | // Skip over packages in the whitelist 80 | whitelisted := false 81 | for _, w := range whitelist { 82 | if w == p.Name() { 83 | whitelisted = true 84 | break 85 | } 86 | } 87 | if whitelisted { 88 | continue 89 | } 90 | 91 | // If package doesn't have copyright file, log an error. 92 | licenseFile := path.Join(root, p.Name(), utils.LicenseFile) 93 | _, err := driver.StatFile(licenseFile) 94 | if err != nil { 95 | result.Errorf("Error reading license file for %s: %s", p.Name(), err.Error()) 96 | result.Pass = false 97 | } 98 | 99 | if err = checkFile(licenseFile, driver); err != nil { 100 | result.Error(err.Error()) 101 | result.Pass = false 102 | } 103 | } 104 | } 105 | 106 | for _, file := range lt.Files { 107 | if err := checkFile(file, driver); err != nil { 108 | result.Error(err.Error()) 109 | result.Pass = false 110 | } 111 | } 112 | return result 113 | } 114 | 115 | func (lt LicenseTest) LogName() string { 116 | return "License Test" 117 | } 118 | -------------------------------------------------------------------------------- /pkg/types/v1/structure.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1 16 | 17 | import ( 18 | "github.com/sirupsen/logrus" 19 | 20 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 21 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 22 | ) 23 | 24 | type StructureTest struct { 25 | DriverImpl func(drivers.DriverConfig) (drivers.Driver, error) 26 | DriverArgs drivers.DriverConfig 27 | SchemaVersion string `yaml:"schemaVersion"` 28 | GlobalEnvVars []types.EnvVar `yaml:"globalEnvVars"` 29 | CommandTests []CommandTest `yaml:"commandTests"` 30 | FileExistenceTests []FileExistenceTest `yaml:"fileExistenceTests"` 31 | FileContentTests []FileContentTest `yaml:"fileContentTests"` 32 | LicenseTests []LicenseTest `yaml:"licenseTests"` 33 | } 34 | 35 | func (st *StructureTest) NewDriver() (drivers.Driver, error) { 36 | return st.DriverImpl(st.DriverArgs) 37 | } 38 | 39 | func (st *StructureTest) SetDriverImpl(f func(drivers.DriverConfig) (drivers.Driver, error), args drivers.DriverConfig) { 40 | st.DriverImpl = f 41 | st.DriverArgs = args 42 | } 43 | 44 | func (st *StructureTest) RunAll(channel chan interface{}, file string) { 45 | // Wait till the file is Processed so we can display the results per file. 46 | fileProcessed := make(chan bool, 1) 47 | go st.runAll(channel, fileProcessed) 48 | <-fileProcessed 49 | } 50 | 51 | func (st *StructureTest) runAll(channel chan interface{}, fileProcessed chan bool) { 52 | st.RunCommandTests(channel) 53 | st.RunFileContentTests(channel) 54 | st.RunFileExistenceTests(channel) 55 | st.RunLicenseTests(channel) 56 | fileProcessed <- true 57 | } 58 | 59 | func (st *StructureTest) RunCommandTests(channel chan interface{}) { 60 | for _, test := range st.CommandTests { 61 | if err := test.Validate(); err != nil { 62 | logrus.Error(err.Error()) 63 | continue 64 | } 65 | driver, err := st.NewDriver() 66 | if err != nil { 67 | logrus.Fatal(err.Error()) 68 | } 69 | vars := append(st.GlobalEnvVars, test.EnvVars...) 70 | if err = driver.Setup(vars, test.Setup); err != nil { 71 | logrus.Error(err.Error()) 72 | driver.Destroy() 73 | continue 74 | } 75 | defer func() { 76 | if err := driver.Teardown(test.Teardown); err != nil { 77 | logrus.Error(err.Error()) 78 | } 79 | driver.Destroy() 80 | }() 81 | channel <- test.Run(driver) 82 | } 83 | } 84 | 85 | func (st *StructureTest) RunFileExistenceTests(channel chan interface{}) { 86 | for _, test := range st.FileExistenceTests { 87 | if err := test.Validate(); err != nil { 88 | logrus.Error(err.Error()) 89 | continue 90 | } 91 | driver, err := st.NewDriver() 92 | if err != nil { 93 | logrus.Fatal(err.Error()) 94 | } 95 | channel <- test.Run(driver) 96 | driver.Destroy() 97 | } 98 | 99 | } 100 | func (st *StructureTest) RunFileContentTests(channel chan interface{}) { 101 | for _, test := range st.FileContentTests { 102 | if err := test.Validate(); err != nil { 103 | logrus.Error(err.Error()) 104 | continue 105 | } 106 | driver, err := st.NewDriver() 107 | if err != nil { 108 | logrus.Fatal(err.Error()) 109 | } 110 | channel <- test.Run(driver) 111 | driver.Destroy() 112 | } 113 | } 114 | 115 | func (st *StructureTest) RunLicenseTests(channel chan interface{}) { 116 | for _, test := range st.LicenseTests { 117 | driver, err := st.NewDriver() 118 | if err != nil { 119 | logrus.Fatal(err.Error()) 120 | } 121 | channel <- test.Run(driver) 122 | driver.Destroy() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /pkg/types/v2/command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v2 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/sirupsen/logrus" 22 | 23 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 24 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 25 | "github.com/GoogleContainerTools/container-structure-test/pkg/utils" 26 | ) 27 | 28 | type CommandTest struct { 29 | Name string `yaml:"name"` 30 | Setup [][]string `yaml:"setup"` 31 | Teardown [][]string `yaml:"teardown"` 32 | EnvVars []types.EnvVar `yaml:"envVars"` 33 | ExitCode int `yaml:"exitCode"` 34 | Command string `yaml:"command"` 35 | Args []string `yaml:"args"` 36 | ExpectedOutput []string `yaml:"expectedOutput"` 37 | ExcludedOutput []string `yaml:"excludedOutput"` 38 | ExpectedError []string `yaml:"expectedError"` 39 | ExcludedError []string `yaml:"excludedError"` // excluded error from running command 40 | } 41 | 42 | func (ct *CommandTest) Validate(channel chan interface{}) bool { 43 | res := &types.TestResult{} 44 | if ct.Name == "" { 45 | res.Error("Please provide a valid name for every test") 46 | } 47 | res.Name = ct.Name 48 | if ct.Command == "" { 49 | res.Errorf("Please provide a valid command to run for test %s", ct.Name) 50 | } 51 | if ct.Setup != nil { 52 | for _, c := range ct.Setup { 53 | if len(c) == 0 { 54 | res.Error("Error in setup command configuration encountered; please check formatting and remove all empty setup commands") 55 | } 56 | } 57 | } 58 | if ct.Teardown != nil { 59 | for _, c := range ct.Teardown { 60 | if len(c) == 0 { 61 | res.Error("Error in teardown command configuration encountered; please check formatting and remove all empty teardown commands") 62 | } 63 | } 64 | } 65 | if ct.EnvVars != nil { 66 | for _, envVar := range ct.EnvVars { 67 | if envVar.Key == "" || envVar.Value == "" { 68 | res.Error("Please provide non-empty keys and values for all specified env vars") 69 | } 70 | } 71 | } 72 | if len(res.Errors) > 0 { 73 | channel <- res 74 | return false 75 | } 76 | return true 77 | } 78 | 79 | func (ct *CommandTest) LogName() string { 80 | return fmt.Sprintf("Command Test: %s", ct.Name) 81 | } 82 | 83 | func (ct *CommandTest) Run(driver drivers.Driver) *types.TestResult { 84 | logrus.Debug(ct.LogName()) 85 | config, err := driver.GetConfig() 86 | if err != nil { 87 | logrus.Errorf("error retrieving image config: %s", err.Error()) 88 | } 89 | fullCommand := utils.SubstituteEnvVars(append([]string{ct.Command}, ct.Args...), config.Env) 90 | start := time.Now() 91 | stdout, stderr, exitcode, err := driver.ProcessCommand(ct.EnvVars, fullCommand) 92 | end := time.Now() 93 | duration := end.Sub(start) 94 | result := &types.TestResult{ 95 | Name: ct.LogName(), 96 | Pass: true, 97 | Errors: make([]string, 0), 98 | Stderr: stderr, 99 | Stdout: stdout, 100 | Duration: duration, 101 | } 102 | if err != nil { 103 | result.Fail() 104 | result.Error(err.Error()) 105 | return result 106 | } 107 | 108 | ct.CheckOutput(result, stdout, stderr, exitcode) 109 | return result 110 | } 111 | 112 | func (ct *CommandTest) CheckOutput(result *types.TestResult, stdout string, stderr string, exitCode int) { 113 | for _, errStr := range ct.ExpectedError { 114 | if !utils.CompileAndRunRegex(errStr, stderr, true) { 115 | result.Errorf("Expected string '%s' not found in error '%s'", errStr, stderr) 116 | result.Fail() 117 | } 118 | } 119 | for _, errStr := range ct.ExcludedError { 120 | if !utils.CompileAndRunRegex(errStr, stderr, false) { 121 | result.Errorf("Excluded string '%s' found in error '%s'", errStr, stderr) 122 | result.Fail() 123 | } 124 | } 125 | for _, outStr := range ct.ExpectedOutput { 126 | if !utils.CompileAndRunRegex(outStr, stdout, true) { 127 | result.Errorf("Expected string '%s' not found in output '%s'", outStr, stdout) 128 | result.Fail() 129 | } 130 | } 131 | for _, outStr := range ct.ExcludedOutput { 132 | if !utils.CompileAndRunRegex(outStr, stdout, false) { 133 | result.Errorf("Excluded string '%s' found in output '%s'", outStr, stdout) 134 | result.Fail() 135 | } 136 | } 137 | if ct.ExitCode != exitCode { 138 | result.Errorf("Test '%s' exited with incorrect error code. Expected: %d, Actual: %d", ct.Name, ct.ExitCode, exitCode) 139 | result.Fail() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /pkg/types/v2/file_content.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v2 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/sirupsen/logrus" 21 | 22 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 23 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 24 | "github.com/GoogleContainerTools/container-structure-test/pkg/utils" 25 | ) 26 | 27 | type FileContentTest struct { 28 | Name string `yaml:"name"` // name of test 29 | Path string `yaml:"path"` // file to check existence of 30 | ExpectedContents []string `yaml:"expectedContents"` // list of expected contents of file 31 | ExcludedContents []string `yaml:"excludedContents"` // list of excluded contents of file 32 | } 33 | 34 | func (ft FileContentTest) Validate(channel chan interface{}) bool { 35 | res := &types.TestResult{} 36 | if ft.Name == "" { 37 | res.Error("Please provide a valid name for every test") 38 | } 39 | res.Name = ft.Name 40 | if ft.Path == "" { 41 | res.Errorf("Please provide a valid file path for test %s", ft.Name) 42 | } 43 | if len(res.Errors) > 0 { 44 | channel <- res 45 | return false 46 | } 47 | return true 48 | } 49 | 50 | func (ft FileContentTest) LogName() string { 51 | return fmt.Sprintf("File Content Test: %s", ft.Name) 52 | } 53 | 54 | func (ft FileContentTest) Run(driver drivers.Driver) *types.TestResult { 55 | result := &types.TestResult{ 56 | Name: ft.LogName(), 57 | Pass: true, 58 | Errors: make([]string, 0), 59 | } 60 | logrus.Info(ft.LogName()) 61 | actualContents, err := driver.ReadFile(ft.Path) 62 | if err != nil { 63 | result.Errorf("Failed to open %s. Error: %s", ft.Path, err) 64 | result.Fail() 65 | return result 66 | } 67 | 68 | contents := string(actualContents) 69 | 70 | for _, s := range ft.ExpectedContents { 71 | if !utils.CompileAndRunRegex(s, contents, true) { 72 | result.Errorf("Expected string '%s' not found in file content string '%s'", s, contents) 73 | result.Fail() 74 | } 75 | } 76 | for _, s := range ft.ExcludedContents { 77 | if !utils.CompileAndRunRegex(s, contents, false) { 78 | result.Errorf("Excluded string '%s' found in file content string '%s'", s, contents) 79 | result.Fail() 80 | } 81 | } 82 | return result 83 | } 84 | -------------------------------------------------------------------------------- /pkg/types/v2/file_existence.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v2 16 | 17 | import ( 18 | "archive/tar" 19 | "fmt" 20 | "os" 21 | 22 | "github.com/pkg/errors" 23 | "github.com/sirupsen/logrus" 24 | 25 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 26 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 27 | "github.com/GoogleContainerTools/container-structure-test/pkg/utils" 28 | ) 29 | 30 | var defaultOwnership = -1 31 | 32 | type FileExistenceTest struct { 33 | Name string `yaml:"name"` // name of test 34 | Path string `yaml:"path"` // file to check existence of 35 | ShouldExist bool `yaml:"shouldExist"` // whether or not the file should exist 36 | Permissions string `yaml:"permissions"` // expected Unix permission string of the file, e.g. drwxrwxrwx 37 | Uid int `yaml:"uid"` // ID of the owner of the file 38 | Gid int `yaml:"gid"` // ID of the group of the file 39 | IsExecutableBy string `yaml:"isExecutableBy"` // name of group that file should be executable by 40 | } 41 | 42 | func (fe FileExistenceTest) MarshalYAML() (interface{}, error) { 43 | return FileExistenceTest{ShouldExist: true}, nil 44 | } 45 | 46 | func (fe *FileExistenceTest) UnmarshalYAML(unmarshal func(interface{}) error) error { 47 | // Create a type alias and call unmarshal on this type to unmarshal the yaml text into 48 | // struct, since calling unmarshal on FileExistenceTest will result in an infinite loop. 49 | type FileExistenceTestHolder FileExistenceTest 50 | holder := FileExistenceTestHolder{ 51 | ShouldExist: true, 52 | Uid: defaultOwnership, 53 | Gid: defaultOwnership, 54 | } 55 | err := unmarshal(&holder) 56 | if err != nil { 57 | return err 58 | } 59 | *fe = FileExistenceTest(holder) 60 | return nil 61 | } 62 | 63 | func (ft FileExistenceTest) Validate(channel chan interface{}) bool { 64 | res := &types.TestResult{} 65 | if ft.Name == "" { 66 | res.Errorf("Please provide a valid name for every test") 67 | } 68 | res.Name = ft.Name 69 | if ft.Path == "" { 70 | res.Errorf("Please provide a valid file path for test %s", ft.Name) 71 | } 72 | if len(res.Errors) > 0 { 73 | channel <- res 74 | return false 75 | } 76 | return true 77 | } 78 | 79 | func (ft FileExistenceTest) LogName() string { 80 | return fmt.Sprintf("File Existence Test: %s", ft.Name) 81 | } 82 | 83 | func (ft FileExistenceTest) Run(driver drivers.Driver) *types.TestResult { 84 | result := &types.TestResult{ 85 | Name: ft.LogName(), 86 | Pass: true, 87 | Errors: make([]string, 0), 88 | } 89 | logrus.Info(ft.LogName()) 90 | var info os.FileInfo 91 | config, err := driver.GetConfig() 92 | if err != nil { 93 | logrus.Errorf("error retrieving image config: %s", err.Error()) 94 | } 95 | info, err = driver.StatFile(utils.SubstituteEnvVar(ft.Path, config.Env)) 96 | if info == nil && ft.ShouldExist { 97 | result.Errorf(errors.Wrap(err, "Error examining file in container").Error()) 98 | result.Fail() 99 | return result 100 | } 101 | if ft.ShouldExist && err != nil { 102 | result.Errorf("File %s should exist but does not, got error: %s", ft.Path, err) 103 | result.Fail() 104 | } else if !ft.ShouldExist && err == nil { 105 | result.Errorf("File %s should not exist but does", ft.Path) 106 | result.Fail() 107 | } 108 | 109 | // Next assertions don't make sense if the file doesn't exist. 110 | if !ft.ShouldExist { 111 | return result 112 | } 113 | 114 | if ft.Permissions != "" && info != nil { 115 | perms := info.Mode() 116 | if perms.String() != ft.Permissions { 117 | result.Errorf("%s has incorrect permissions. Expected: %s, Actual: %s", ft.Path, ft.Permissions, perms.String()) 118 | result.Fail() 119 | } 120 | } 121 | if ft.IsExecutableBy != "" { 122 | perms := info.Mode() 123 | switch ft.IsExecutableBy { 124 | case "any": 125 | if perms&0111 == 0 { 126 | result.Errorf("%s has incorrect executable bit. Expected to be executable by any, Actual: %s", ft.Path, perms.String()) 127 | result.Fail() 128 | } 129 | case "owner": 130 | if perms&0100 == 0 { 131 | result.Errorf("%s has incorrect executable bit. Expected to be executable by owner, Actual: %s", ft.Path, perms.String()) 132 | result.Fail() 133 | } 134 | case "group": 135 | if perms&0010 == 0 { 136 | result.Errorf("%s has incorrect executable bit. Expected to be executable by group, Actual: %s", ft.Path, perms.String()) 137 | result.Fail() 138 | } 139 | case "other": 140 | if perms&0001 == 0 { 141 | result.Errorf("%s has incorrect executable bit. Expected to be executable by other, Actual: %s", ft.Path, perms.String()) 142 | result.Fail() 143 | } 144 | default: 145 | result.Errorf("%s not recognised as a valid option", ft.IsExecutableBy) 146 | result.Fail() 147 | } 148 | } 149 | if ft.Uid != defaultOwnership || ft.Gid != defaultOwnership { 150 | header, ok := info.Sys().(*tar.Header) 151 | if ok { 152 | if ft.Uid != defaultOwnership && header.Uid != ft.Uid { 153 | result.Errorf("%s has incorrect user ownership. Expected: %d, Actual: %d", ft.Path, ft.Uid, header.Uid) 154 | result.Fail() 155 | } 156 | if ft.Gid != defaultOwnership && header.Gid != ft.Gid { 157 | result.Errorf("%s has incorrect group ownership. Expected: %d, Actual: %d", ft.Path, ft.Gid, header.Gid) 158 | result.Fail() 159 | } 160 | } else { 161 | result.Errorf("Error checking ownership of file %s", ft.Path) 162 | result.Fail() 163 | } 164 | } 165 | return result 166 | } 167 | -------------------------------------------------------------------------------- /pkg/types/v2/licenses.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v2 16 | 17 | import ( 18 | "fmt" 19 | "path" 20 | "strings" 21 | 22 | "github.com/sirupsen/logrus" 23 | 24 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 25 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 26 | "github.com/GoogleContainerTools/container-structure-test/pkg/utils" 27 | ) 28 | 29 | // Not currently used, but leaving the possibility open 30 | type LicenseTest struct { 31 | Debian bool `yaml:"debian"` 32 | Files []string `yaml:"files"` 33 | } 34 | 35 | var ( 36 | // Whitelist is the list of packages that we want to automatically pass this 37 | // check even if it would normally fail for one reason or another. 38 | whitelist = []string{"libgnutls30"} 39 | 40 | // Blacklist is the set of words that, if contained in a license file, should cause a failure. 41 | // This will most likely just be names of unsupported licenses. 42 | blacklist = []string{"AGPL", "WTFPL"} 43 | ) 44 | 45 | func checkFile(licenseFile string, driver drivers.Driver) error { 46 | // Read through the copyright file and make sure don't have an unauthorized license 47 | license, err := driver.ReadFile(licenseFile) 48 | if err != nil { 49 | return fmt.Errorf("Error reading license file for %s: %s", licenseFile, err.Error()) 50 | } 51 | contents := strings.ToUpper(string(license)) 52 | for _, b := range blacklist { 53 | if strings.Contains(contents, b) { 54 | return fmt.Errorf("Invalid license for %s, license contains %s", licenseFile, b) 55 | } 56 | } 57 | return nil 58 | } 59 | 60 | func (lt LicenseTest) Run(driver drivers.Driver) *types.TestResult { 61 | result := &types.TestResult{ 62 | Name: lt.LogName(), 63 | Pass: true, 64 | Errors: make([]string, 0), 65 | } 66 | logrus.Debug(lt.LogName()) 67 | if lt.Debian { 68 | root := utils.DebianRoot 69 | packages, err := driver.ReadDir(root) 70 | if err != nil { 71 | result.Errorf("Error reading directory: %s", err) 72 | result.Fail() 73 | return result 74 | } 75 | for _, p := range packages { 76 | if !p.IsDir() { 77 | continue 78 | } 79 | logrus.Debugf(p.Name()) 80 | // Skip over packages in the whitelist 81 | whitelisted := false 82 | for _, w := range whitelist { 83 | if w == p.Name() { 84 | whitelisted = true 85 | break 86 | } 87 | } 88 | if whitelisted { 89 | continue 90 | } 91 | 92 | // If package doesn't have copyright file, log an error. 93 | licenseFile := path.Join(root, p.Name(), utils.LicenseFile) 94 | _, err := driver.StatFile(licenseFile) 95 | if err != nil { 96 | result.Errorf("Error reading license file for %s: %s", p.Name(), err.Error()) 97 | result.Fail() 98 | } 99 | 100 | if err = checkFile(licenseFile, driver); err != nil { 101 | result.Error(err.Error()) 102 | result.Fail() 103 | } 104 | } 105 | } 106 | 107 | for _, file := range lt.Files { 108 | if err := checkFile(file, driver); err != nil { 109 | result.Error(err.Error()) 110 | result.Fail() 111 | } 112 | } 113 | return result 114 | } 115 | 116 | func (lt LicenseTest) LogName() string { 117 | return "License Test" 118 | } 119 | -------------------------------------------------------------------------------- /pkg/types/v2/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v2 16 | 17 | import ( 18 | "github.com/sirupsen/logrus" 19 | 20 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 21 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 22 | "github.com/GoogleContainerTools/container-structure-test/pkg/utils" 23 | ) 24 | 25 | type MetadataTest struct { 26 | EnvVars []types.EnvVar `yaml:"envVars"` 27 | UnboundEnvVars []types.EnvVar `yaml:"unboundEnvVars"` 28 | ExposedPorts []string `yaml:"exposedPorts"` 29 | UnexposedPorts []string `yaml:"unexposedPorts"` 30 | Entrypoint *[]string `yaml:"entrypoint"` 31 | Cmd *[]string `yaml:"cmd"` 32 | Workdir string `yaml:"workdir"` 33 | Volumes []string `yaml:"volumes"` 34 | UnmountedVolumes []string `yaml:"unmountedVolumes"` 35 | Labels []types.Label `yaml:"labels"` 36 | User string `yaml:"user"` 37 | } 38 | 39 | func (mt MetadataTest) IsEmpty() bool { 40 | return len(mt.EnvVars) == 0 && 41 | len(mt.UnboundEnvVars) == 0 && 42 | len(mt.ExposedPorts) == 0 && 43 | len(mt.UnexposedPorts) == 0 && 44 | mt.Entrypoint == nil && 45 | mt.Cmd == nil && 46 | mt.Workdir == "" && 47 | mt.User == "" && 48 | len(mt.Volumes) == 0 && 49 | len(mt.UnmountedVolumes) == 0 && 50 | len(mt.Labels) == 0 51 | } 52 | 53 | func (mt MetadataTest) LogName() string { 54 | return "Metadata Test" 55 | } 56 | 57 | func (mt MetadataTest) Validate(channel chan interface{}) bool { 58 | res := &types.TestResult{ 59 | Name: mt.LogName(), 60 | } 61 | for _, envVar := range mt.EnvVars { 62 | if envVar.Key == "" { 63 | res.Error("Environment variable key cannot be empty") 64 | } 65 | } 66 | for _, label := range mt.Labels { 67 | if label.Key == "" { 68 | res.Error("Label key cannot be empty") 69 | } 70 | } 71 | for _, port := range mt.ExposedPorts { 72 | if port == "" { 73 | res.Error("Port cannot be empty") 74 | } 75 | } 76 | for _, volume := range mt.Volumes { 77 | if volume == "" { 78 | res.Error("Volume cannot be empty") 79 | } 80 | } 81 | if len(res.Errors) > 0 { 82 | channel <- res 83 | return false 84 | } 85 | return true 86 | } 87 | 88 | func (mt MetadataTest) Run(driver drivers.Driver) *types.TestResult { 89 | result := &types.TestResult{ 90 | Name: mt.LogName(), 91 | Pass: true, 92 | } 93 | logrus.Debug(mt.LogName()) 94 | imageConfig, err := driver.GetConfig() 95 | if err != nil { 96 | result.Errorf("Error retrieving image config: %s", err.Error()) 97 | result.Fail() 98 | return result 99 | } 100 | 101 | for _, pair := range mt.EnvVars { 102 | if val, ok := imageConfig.Env[pair.Key]; ok { 103 | var match bool 104 | if pair.IsRegex { 105 | match = utils.CompileAndRunRegex(pair.Value, val, true) 106 | } else { 107 | match = (pair.Value == val) 108 | } 109 | if !match { 110 | result.Errorf("env var %s value %s does not match expected value: %s", pair.Key, val, pair.Value) 111 | result.Fail() 112 | } 113 | } else { 114 | result.Errorf("variable %s not found in image env", pair.Key) 115 | result.Fail() 116 | } 117 | } 118 | 119 | for _, pair := range mt.UnboundEnvVars { 120 | if _, ok := imageConfig.Env[pair.Key]; ok { 121 | result.Errorf("env variable %s should not be present in image metadata", pair.Key) 122 | result.Fail() 123 | } 124 | } 125 | 126 | for _, pair := range mt.Labels { 127 | if val, ok := imageConfig.Labels[pair.Key]; ok { 128 | var match bool 129 | if pair.IsRegex { 130 | match = utils.CompileAndRunRegex(pair.Value, val, true) 131 | } else { 132 | match = (pair.Value == val) 133 | } 134 | if !match { 135 | result.Errorf("label %s value %s does not match expected value: %s", pair.Key, val, pair.Value) 136 | result.Fail() 137 | } 138 | } else { 139 | result.Errorf("label %s not found in image metadata", pair.Key) 140 | result.Fail() 141 | } 142 | } 143 | 144 | if mt.Cmd != nil { 145 | if len(*mt.Cmd) != len(imageConfig.Cmd) { 146 | result.Errorf("Image Cmd %v does not match expected Cmd: %v", imageConfig.Cmd, *mt.Cmd) 147 | result.Fail() 148 | } else if len(*mt.Cmd) > 0 { 149 | for i := range *mt.Cmd { 150 | if (*mt.Cmd)[i] != imageConfig.Cmd[i] { 151 | result.Errorf("Image config Cmd %v does not match expected value: %s", imageConfig.Cmd, *mt.Cmd) 152 | result.Fail() 153 | } 154 | } 155 | } 156 | } 157 | 158 | if mt.Entrypoint != nil { 159 | if len(*mt.Entrypoint) != len(imageConfig.Entrypoint) { 160 | result.Errorf("Image entrypoint %v does not match expected entrypoint: %v", imageConfig.Entrypoint, *mt.Entrypoint) 161 | result.Fail() 162 | } else { 163 | for i := range *mt.Entrypoint { 164 | if (*mt.Entrypoint)[i] != imageConfig.Entrypoint[i] { 165 | result.Errorf("Image config entrypoint %v does not match expected value: %s", imageConfig.Entrypoint, *mt.Entrypoint) 166 | result.Fail() 167 | } 168 | } 169 | } 170 | } 171 | 172 | if mt.Workdir != "" && mt.Workdir != imageConfig.Workdir { 173 | result.Errorf("Image workdir %s does not match config workdir: %s", imageConfig.Workdir, mt.Workdir) 174 | result.Fail() 175 | } 176 | 177 | if mt.User != "" && mt.User != imageConfig.User { 178 | result.Errorf("Image user %s does not match config user: %s", imageConfig.User, mt.User) 179 | result.Fail() 180 | } 181 | 182 | for _, port := range mt.ExposedPorts { 183 | if !utils.ValueInList(port, imageConfig.ExposedPorts) { 184 | result.Errorf("Port %s not found in config", port) 185 | result.Fail() 186 | } 187 | } 188 | 189 | for _, port := range mt.UnexposedPorts { 190 | if utils.ValueInList(port, imageConfig.ExposedPorts) { 191 | result.Errorf("Port %s should not be exposed", port) 192 | result.Fail() 193 | } 194 | } 195 | 196 | for _, volume := range mt.Volumes { 197 | if !utils.ValueInList(volume, imageConfig.Volumes) { 198 | result.Errorf("Volume %s not found in config", volume) 199 | result.Fail() 200 | } 201 | } 202 | 203 | for _, volume := range mt.UnmountedVolumes { 204 | if utils.ValueInList(volume, imageConfig.Volumes) { 205 | result.Errorf("Volume %s should not be mounted", volume) 206 | result.Fail() 207 | } 208 | } 209 | return result 210 | } 211 | -------------------------------------------------------------------------------- /pkg/types/v2/structure.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v2 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/sirupsen/logrus" 21 | 22 | "github.com/GoogleContainerTools/container-structure-test/pkg/drivers" 23 | types "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" 24 | ) 25 | 26 | type StructureTest struct { 27 | DriverImpl func(drivers.DriverConfig) (drivers.Driver, error) 28 | DriverArgs drivers.DriverConfig 29 | SchemaVersion string `yaml:"schemaVersion"` 30 | GlobalEnvVars []types.EnvVar `yaml:"globalEnvVars"` 31 | CommandTests []CommandTest `yaml:"commandTests"` 32 | FileExistenceTests []FileExistenceTest `yaml:"fileExistenceTests"` 33 | FileContentTests []FileContentTest `yaml:"fileContentTests"` 34 | MetadataTest MetadataTest `yaml:"metadataTest"` 35 | LicenseTests []LicenseTest `yaml:"licenseTests"` 36 | ContainerRunOptions types.ContainerRunOptions `yaml:"containerRunOptions"` 37 | } 38 | 39 | func (st *StructureTest) NewDriver() (drivers.Driver, error) { 40 | args := st.DriverArgs 41 | if st.ContainerRunOptions.IsSet() { 42 | args.RunOpts = st.ContainerRunOptions 43 | } 44 | return st.DriverImpl(args) 45 | } 46 | 47 | func (st *StructureTest) SetDriverImpl(f func(drivers.DriverConfig) (drivers.Driver, error), args drivers.DriverConfig) { 48 | st.DriverImpl = f 49 | st.DriverArgs = args 50 | } 51 | 52 | func (st *StructureTest) RunAll(channel chan interface{}, file string) { 53 | fileProcessed := make(chan bool, 1) 54 | go st.runAll(channel, fileProcessed) 55 | <-fileProcessed 56 | } 57 | 58 | func (st *StructureTest) runAll(channel chan interface{}, fileProcessed chan bool) { 59 | st.RunCommandTests(channel) 60 | st.RunFileContentTests(channel) 61 | st.RunFileExistenceTests(channel) 62 | st.RunLicenseTests(channel) 63 | st.RunMetadataTests(channel) 64 | fileProcessed <- true 65 | } 66 | 67 | func (st *StructureTest) RunCommandTests(channel chan interface{}) { 68 | for _, test := range st.CommandTests { 69 | if !test.Validate(channel) { 70 | continue 71 | } 72 | res := &types.TestResult{ 73 | Name: test.Name, 74 | Pass: false, 75 | } 76 | driver, err := st.NewDriver() 77 | if err != nil { 78 | res.Errorf("error creating driver: %s", err.Error()) 79 | channel <- res 80 | continue 81 | } 82 | defer driver.Destroy() 83 | if err = driver.SetEnv(st.GlobalEnvVars); err != nil { 84 | res.Errorf("error setting env vars: %s", err.Error()) 85 | channel <- res 86 | continue 87 | } 88 | if err = driver.Setup(test.EnvVars, test.Setup); err != nil { 89 | res.Errorf("error in setup: %s", err.Error()) 90 | channel <- res 91 | continue 92 | } 93 | defer func() { 94 | if err := driver.Teardown(test.Teardown); err != nil { 95 | logrus.Error(err.Error()) 96 | } 97 | }() 98 | channel <- test.Run(driver) 99 | } 100 | } 101 | 102 | func (st *StructureTest) RunFileExistenceTests(channel chan interface{}) { 103 | for _, test := range st.FileExistenceTests { 104 | if !test.Validate(channel) { 105 | continue 106 | } 107 | res := &types.TestResult{ 108 | Name: test.Name, 109 | Pass: false, 110 | } 111 | driver, err := st.NewDriver() 112 | if err != nil { 113 | res.Errorf("error creating driver: %s", err.Error()) 114 | channel <- res 115 | continue 116 | } 117 | if err = driver.SetEnv(st.GlobalEnvVars); err != nil { 118 | res.Errorf("error setting env vars: %s", err.Error()) 119 | channel <- res 120 | continue 121 | } 122 | channel <- test.Run(driver) 123 | driver.Destroy() 124 | } 125 | } 126 | 127 | func (st *StructureTest) RunFileContentTests(channel chan interface{}) { 128 | for _, test := range st.FileContentTests { 129 | if !test.Validate(channel) { 130 | continue 131 | } 132 | res := &types.TestResult{ 133 | Name: test.Name, 134 | Pass: false, 135 | } 136 | driver, err := st.NewDriver() 137 | if err != nil { 138 | res.Errorf("error creating driver: %s", err.Error()) 139 | channel <- res 140 | continue 141 | } 142 | if err = driver.SetEnv(st.GlobalEnvVars); err != nil { 143 | res.Errorf("error setting env vars: %s", err.Error()) 144 | channel <- res 145 | continue 146 | } 147 | channel <- test.Run(driver) 148 | driver.Destroy() 149 | } 150 | } 151 | 152 | func (st *StructureTest) RunMetadataTests(channel chan interface{}) { 153 | if st.MetadataTest.IsEmpty() { 154 | logrus.Debug("Skipping empty metadata test") 155 | return 156 | } 157 | if !st.MetadataTest.Validate(channel) { 158 | return 159 | } 160 | driver, err := st.NewDriver() 161 | if err != nil { 162 | channel <- &types.TestResult{ 163 | Name: st.MetadataTest.LogName(), 164 | Errors: []string{ 165 | fmt.Sprintf("error creating driver: %s", err.Error()), 166 | }, 167 | } 168 | return 169 | } 170 | channel <- st.MetadataTest.Run(driver) 171 | driver.Destroy() 172 | } 173 | 174 | func (st *StructureTest) RunLicenseTests(channel chan interface{}) { 175 | for _, test := range st.LicenseTests { 176 | driver, err := st.NewDriver() 177 | if err != nil { 178 | logrus.Fatal(err.Error()) 179 | } 180 | channel <- test.Run(driver) 181 | driver.Destroy() 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "regexp" 21 | 22 | "github.com/sirupsen/logrus" 23 | ) 24 | 25 | var yesResponses = []string{"y", "Y", "yes", "Yes", "YES"} 26 | var noResponses = []string{"n", "N", "no", "No", "NO"} 27 | 28 | const ( 29 | NoopCommand string = "NOOP_COMMAND_DO_NOT_RUN" 30 | DebianRoot string = "/usr/share/doc" 31 | LicenseFile string = "copyright" 32 | ) 33 | 34 | func CompileAndRunRegex(regex string, base string, shouldMatch bool) bool { 35 | r, rErr := regexp.Compile(regex) 36 | if rErr != nil { 37 | logrus.Errorf("Error compiling regex %s : %s", regex, rErr.Error()) 38 | return false 39 | } 40 | return shouldMatch == r.MatchString(base) 41 | } 42 | 43 | // adapted from https://gist.github.com/albrow/5882501 44 | func UserConfirmation(message string, force bool) bool { 45 | fmt.Println(message) 46 | if force { 47 | fmt.Println("Forcing test run!") 48 | return true 49 | } 50 | 51 | var input string 52 | _, err := fmt.Scanln(&input) 53 | if err != nil { 54 | logrus.Errorf("error reading input from stdin: %s", err.Error()) 55 | return false 56 | } 57 | for _, response := range yesResponses { 58 | if input == response { 59 | return true 60 | } 61 | } 62 | for _, response := range noResponses { 63 | if input == response { 64 | return false 65 | } 66 | } 67 | fmt.Println("Please type yes or no to continue or exit") 68 | return UserConfirmation(message, force) 69 | } 70 | 71 | func ValueInList(target string, list []string) bool { 72 | for _, value := range list { 73 | if target == value { 74 | return true 75 | } 76 | } 77 | return false 78 | } 79 | 80 | func SubstituteEnvVar(arg string, env map[string]string) string { 81 | f := func(key string) string { 82 | return env[key] 83 | } 84 | subbed := os.Expand(arg, f) 85 | return subbed 86 | } 87 | 88 | func SubstituteEnvVars(args []string, env map[string]string) []string { 89 | finalArgs := []string{} 90 | for _, arg := range args { 91 | finalArgs = append(finalArgs, SubstituteEnvVar(arg, env)) 92 | } 93 | return finalArgs 94 | } 95 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package version 16 | 17 | import ( 18 | "fmt" 19 | "runtime" 20 | ) 21 | 22 | // The current version of container-structure-test 23 | // This is a private field and is set through a compilation flag from the Makefile 24 | 25 | var version = "v0.0.0-unset" 26 | 27 | var buildDate string 28 | var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) 29 | 30 | type Info struct { 31 | Version string 32 | GitVersion string 33 | BuildDate string 34 | GoVersion string 35 | Compiler string 36 | Platform string 37 | } 38 | 39 | // Get returns the version and buildtime information about the binary 40 | func GetVersion() *Info { 41 | // These variables typically come from -ldflags settings to `go build` 42 | return &Info{ 43 | Version: version, 44 | BuildDate: buildDate, 45 | GoVersion: runtime.Version(), 46 | Compiler: runtime.Compiler, 47 | Platform: platform, 48 | } 49 | } 50 | 51 | // func GetVersion() string { 52 | // return version 53 | // } 54 | -------------------------------------------------------------------------------- /repositories.bzl: -------------------------------------------------------------------------------- 1 | """Repository rules for fetching pre-built container-test binaries""" 2 | load("@aspect_bazel_lib//lib:repositories.bzl", "register_jq_toolchains") 3 | load("//bazel:toolchains_repo.bzl", "PLATFORMS", "toolchains_repo") 4 | 5 | # TODO(alexeagle): automate updates when new releases 6 | # Run following command to make sure all checksums are correct. 7 | 8 | # bazel build @structure_test_st_darwin_amd64//... @structure_test_st_darwin_arm64//... @structure_test_st_linux_arm64//... \ 9 | # @structure_test_st_linux_s390x//... @structure_test_st_linux_amd64//... @structure_test_st_windows_amd64//... 10 | 11 | _VERSION="v1.19.2" 12 | _HASHES = { 13 | "darwin-amd64": "sha256-mkBKyy32nnivskrvlj0BbPnauhXnUckmBJJZzrDdKYU=", 14 | "darwin-arm64": "sha256-qdy4KDN143ar99X2kIwvXwyQO/NBqwMCZhmVJ65Pu3Y=", 15 | "linux-amd64": "sha256-u6b/uaz5Z0QMHMD+Iz2uM9ClCq4ULxLIWCYZAue/tAU=", 16 | "linux-arm64": "sha256-ebFieFZDy+X9ZbNAhdUi6KPKRyBe+bsizBaBqoimZ88=", 17 | "linux-ppc64le": "sha256-pkvTL+pb9/kxVxs0HBPf3CgTLD4z6mkdXlrZRU+iiIE=", 18 | "linux-s390x": "sha256-v2Nu34HxrGtJ1Op8qWMtoFi36CNW4hI83RRTVd7uq7s=", 19 | "windows-amd64.exe": "sha256-J9+eC2BlqXXhLIDyjMpFm10uFAQcV8HzPOuz76y1WbE=", 20 | } 21 | 22 | STRUCTURE_TEST_BUILD_TMPL = """\ 23 | # Generated by container/repositories.bzl 24 | load("@container_structure_test//bazel:toolchain.bzl", "structure_test_toolchain") 25 | structure_test_toolchain( 26 | name = "structure_test_toolchain", 27 | structure_test = "structure_test" 28 | ) 29 | """ 30 | 31 | def _structure_test_repo_impl(repository_ctx): 32 | platform = repository_ctx.attr.platform.replace("_", "-") 33 | 34 | if platform.find("windows") != -1: 35 | platform = platform + ".exe" 36 | url = "https://github.com/GoogleContainerTools/container-structure-test/releases/download/{version}/container-structure-test-{platform}".format( 37 | version = _VERSION, 38 | platform = platform, 39 | ) 40 | repository_ctx.download( 41 | url = url, 42 | output = "structure_test", 43 | integrity = _HASHES[platform], 44 | executable = True, 45 | ) 46 | repository_ctx.file("BUILD.bazel", STRUCTURE_TEST_BUILD_TMPL) 47 | 48 | structure_test_repositories = repository_rule( 49 | _structure_test_repo_impl, 50 | doc = "Fetch external tools needed for structure test toolchain", 51 | attrs = { 52 | "platform": attr.string(mandatory = True, values = PLATFORMS.keys()), 53 | }, 54 | ) 55 | 56 | # Wrapper macro around everything above, this is the primary API 57 | def container_structure_test_register_toolchain(name, register = True): 58 | """Convenience macro for users which does typical setup. 59 | 60 | - create a repository for each built-in platform like "container_linux_amd64" - 61 | this repository is lazily fetched when node is needed for that platform. 62 | - create a repository exposing toolchains for each platform like "container_platforms" 63 | - register a toolchain pointing at each platform 64 | Users can avoid this macro and do these steps themselves, if they want more control. 65 | Args: 66 | name: base name for all created repos, like "container7" 67 | register: whether to call through to native.register_toolchains. 68 | Should be True for WORKSPACE users, but false when used under bzlmod extension 69 | """ 70 | 71 | st_toolchain_name = "structure_test_toolchains" 72 | 73 | register_jq_toolchains(register = register) 74 | 75 | for platform in PLATFORMS.keys(): 76 | structure_test_repositories( 77 | name = "{name}_st_{platform}".format(name = name, platform = platform), 78 | platform = platform, 79 | ) 80 | 81 | if register: 82 | native.register_toolchains("@{}//:{}_toolchain".format(st_toolchain_name, platform)) 83 | 84 | toolchains_repo( 85 | name = st_toolchain_name, 86 | toolchain_type = "@container_structure_test//bazel:structure_test_toolchain_type", 87 | # avoiding use of .format since {platform} is formatted by toolchains_repo for each platform. 88 | toolchain = "@%s_st_{platform}//:structure_test_toolchain" % name, 89 | ) 90 | 91 | def _st_extension_impl(_): 92 | container_structure_test_register_toolchain("structure_test", register = False) 93 | 94 | extension = module_extension( 95 | implementation = _st_extension_impl, 96 | ) 97 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | cloudbuild.yaml 2 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | ADD out/container-structure-test /container-structure-test 3 | ENTRYPOINT ["/container-structure-test"] 4 | -------------------------------------------------------------------------------- /tests/Dockerfile.cap: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | RUN apt-get update && apt-get install -y libcap2-bin 3 | -------------------------------------------------------------------------------- /tests/Dockerfile.metadata: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | ENV SOME_KEY="SOME_VAL" \ 4 | EMPTY_VAR="" \ 5 | FOO_BAR="FOO\:BAR=BAZ" \ 6 | REGEX_VAR="test-2.1.8" 7 | 8 | LABEL localnet.localdomain.commit_hash="0123456789abcdef0123456789abcdef01234567" \ 9 | localnet.my-domain.my-label="my test label" \ 10 | label-with-empty-val="" 11 | -------------------------------------------------------------------------------- /tests/Dockerfile.unprivileged: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | RUN useradd --create-home --uid 1001 nonroot 4 | 5 | USER 1001 6 | -------------------------------------------------------------------------------- /tests/amd64/ubuntu_22_04_containeropts_env_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test envVars containerRunOptions" 4 | command: "printenv" 5 | expectedOutput: [".*(FOO|BAR)=keepit(secret|safe)!.*"] 6 | containerRunOptions: 7 | envVars: 8 | - FOO 9 | - BAR 10 | 11 | -------------------------------------------------------------------------------- /tests/amd64/ubuntu_22_04_containeropts_envfile_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test envFile containerRunOptions" 4 | command: "printenv" 5 | expectedOutput: [".*(FOO|BAR)=keepit(secret|safe)!.*"] 6 | containerRunOptions: 7 | envFile: tests/envfile 8 | -------------------------------------------------------------------------------- /tests/amd64/ubuntu_22_04_containeropts_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test Capabilities containerRunOptions" 4 | command: "capsh" 5 | args: ["--print"] 6 | expectedOutput: 7 | - ".*cap_sys_admin.*" 8 | - name: "Test bindMounts containerRunOptions" 9 | command: "test" 10 | args: 11 | - "-d" 12 | - "/tmp/test" 13 | exitCode: 0 14 | containerRunOptions: 15 | privileged: true 16 | capabilities: 17 | - "sys_admin" 18 | bindMounts: 19 | - "/tmp/test:/tmp/test" 20 | -------------------------------------------------------------------------------- /tests/amd64/ubuntu_22_04_containeropts_user_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: 'apt-get' 4 | command: 'bash' 5 | args: 6 | - -c 7 | - | 8 | whoami 9 | apt-get update 10 | containerRunOptions: 11 | user: 'root' 12 | -------------------------------------------------------------------------------- /tests/amd64/ubuntu_22_04_failure_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '1.0.0' 2 | commandTests: 3 | - name: 'bad apt-get-command' 4 | command: ['apt-get', 'dslkfjasl'] 5 | excludedError: ['.*FAIL.*'] 6 | expectedError: ['.*Invalid operation dslkfjasl.*'] 7 | exitCode: 1 8 | - name: 'apt-config' 9 | command: ['apt-config', 'dump'] 10 | expectedOutput: ['DPkg::Pre-Install-Pkgs "";'] 11 | - name: 'path' 12 | command: ['sh', '-c', 'echo $PATH'] 13 | expectedOutput: ['/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'] 14 | fileContentTests: 15 | - name: 'Debian Sources' 16 | excludedContents: ['.*gce_debian_mirror.*'] 17 | expectedContents: ['.*httpredir\.debian\.org.*'] 18 | path: '/etc/apt/sources.list' 19 | - name: 'Wrong Retry Policy' 20 | expectedContents: ['Acquire::Retries 4;'] 21 | path: '/etc/apt/apt.conf.d/apt-retry' 22 | fileExistenceTests: 23 | - name: 'Fake Dir' 24 | path: '/foo/bar' 25 | shouldExist: true 26 | - name: 'Wrong permissions' 27 | path: '/etc/apt/sources.list' 28 | permissions: '-rwxrwxrwx' 29 | shouldExist: true 30 | licenseTests: 31 | - debian: true 32 | files: 33 | -------------------------------------------------------------------------------- /tests/amd64/ubuntu_22_04_metadata_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | metadataTest: 3 | envVars: 4 | - key: 'SOME_KEY' 5 | value: 'SOME_VAL' 6 | - key: 'EMPTY_VAR' 7 | value: '' 8 | - key: 'FOO_BAR' 9 | value: 'FOO\:BAR=BAZ' 10 | - key: 'REGEX_VAR' 11 | value: '[a-z]+-2\.1\.*' 12 | isRegex: true 13 | unboundEnvVars: 14 | - key: 'BAR_FOO' 15 | labels: 16 | - key: 'localnet.localdomain.commit_hash' 17 | value: '0123456789abcdef0123456789abcdef01234567' 18 | - key: 'localnet.my-domain.my-label' 19 | value: 'my .+ label' 20 | isRegex: true 21 | - key: 'label-with-empty-val' 22 | value: '' 23 | unexposedPorts: ['80'] 24 | unmountedVolumes: ['/root'] 25 | -------------------------------------------------------------------------------- /tests/amd64/ubuntu_22_04_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: 'apt-get' 4 | command: 'apt-get' 5 | args: ['help'] 6 | excludedError: ['.*FAIL.*'] 7 | expectedOutput: ['.*Usage.*'] 8 | - name: 'apt-config' 9 | command: 'apt-config' 10 | args: ['dump'] 11 | expectedOutput: ['APT::AutoRemove'] 12 | - name: 'path' 13 | command: 'sh' 14 | args: ['-c', 'echo $PATH'] 15 | expectedOutput: ['/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'] 16 | fileContentTests: 17 | - name: 'Debian Sources' 18 | excludedContents: ['.*gce_debian_mirror.*'] 19 | expectedContents: ['.*archive\.ubuntu\.com.*'] 20 | path: '/etc/apt/sources.list' 21 | - name: 'Passwd file' 22 | expectedContents: ['root:x:0:0:root:/root:/bin/bash'] 23 | path: '/etc/passwd' 24 | fileExistenceTests: 25 | - name: 'Root' 26 | path: '/' 27 | shouldExist: true 28 | uid: 0 29 | gid: 0 30 | - name: 'Date' 31 | path: '/bin/date' 32 | isExecutableBy: 'owner' 33 | - name: 'Hosts File' 34 | path: '/etc/hosts' 35 | shouldExist: true 36 | - name: 'Machine ID' 37 | path: '/etc/machine-id' 38 | - name: 'Dummy File' 39 | path: '/etc/dummy' 40 | shouldExist: false 41 | licenseTests: 42 | - debian: false 43 | files: 44 | - "/usr/share/doc/ubuntu-keyring/copyright" 45 | - "/usr/share/doc/dash/copyright" 46 | -------------------------------------------------------------------------------- /tests/arm64/ubuntu_22_04_containeropts_env_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test envVars containerRunOptions" 4 | command: "printenv" 5 | expectedOutput: [".*(FOO|BAR)=keepit(secret|safe)!.*"] 6 | containerRunOptions: 7 | envVars: 8 | - FOO 9 | - BAR 10 | 11 | -------------------------------------------------------------------------------- /tests/arm64/ubuntu_22_04_containeropts_envfile_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test envFile containerRunOptions" 4 | command: "printenv" 5 | expectedOutput: [".*(FOO|BAR)=keepit(secret|safe)!.*"] 6 | containerRunOptions: 7 | envFile: tests/envfile 8 | -------------------------------------------------------------------------------- /tests/arm64/ubuntu_22_04_containeropts_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test Capabilities containerRunOptions" 4 | command: "capsh" 5 | args: ["--print"] 6 | expectedOutput: 7 | - ".*cap_sys_admin.*" 8 | - name: "Test bindMounts containerRunOptions" 9 | command: "test" 10 | args: 11 | - "-d" 12 | - "/tmp/test" 13 | exitCode: 0 14 | containerRunOptions: 15 | privileged: true 16 | capabilities: 17 | - "sys_admin" 18 | bindMounts: 19 | - "/tmp/test:/tmp/test" 20 | -------------------------------------------------------------------------------- /tests/arm64/ubuntu_22_04_containeropts_user_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: 'apt-get' 4 | command: 'bash' 5 | args: 6 | - -c 7 | - | 8 | whoami 9 | apt-get update 10 | containerRunOptions: 11 | user: 'root' 12 | -------------------------------------------------------------------------------- /tests/arm64/ubuntu_22_04_failure_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '1.0.0' 2 | commandTests: 3 | - name: 'bad apt-get-command' 4 | command: ['apt-get', 'dslkfjasl'] 5 | excludedError: ['.*FAIL.*'] 6 | expectedOutput: ['.*Usage.*'] 7 | - name: 'apt-config' 8 | command: ['apt-config', 'dump'] 9 | expectedOutput: ['Acquire::Retries "3"'] 10 | name: 'apt-config' 11 | - name: 'path' 12 | command: ['sh', '-c', 'echo $PATH'] 13 | expectedOutput: ['/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'] 14 | fileContentTests: 15 | - name: 'Debian Sources' 16 | excludedContents: ['.*gce_debian_mirror.*'] 17 | expectedContents: ['.*httpredir\.debian\.org.*'] 18 | path: '/etc/apt/sources.list' 19 | - name: 'Wrong Retry Policy' 20 | expectedContents: ['Acquire::Retries 4;'] 21 | path: '/etc/apt/apt.conf.d/apt-retry' 22 | fileExistenceTests: 23 | - name: 'Fake Dir' 24 | path: '/foo/bar' 25 | shouldExist: true 26 | - name: 'Wrong permissions' 27 | path: '/etc/apt/sources.list' 28 | permissions: '-rwxrwxrwx' 29 | shouldExist: true 30 | licenseTests: 31 | - debian: true 32 | files: 33 | -------------------------------------------------------------------------------- /tests/arm64/ubuntu_22_04_metadata_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | metadataTest: 3 | envVars: 4 | - key: 'SOME_KEY' 5 | value: 'SOME_VAL' 6 | - key: 'EMPTY_VAR' 7 | value: '' 8 | - key: 'FOO_BAR' 9 | value: 'FOO\:BAR=BAZ' 10 | - key: 'REGEX_VAR' 11 | value: '[a-z]+-2\.1\.*' 12 | isRegex: true 13 | unboundEnvVars: 14 | - key: 'BAR_FOO' 15 | labels: 16 | - key: 'localnet.localdomain.commit_hash' 17 | value: '0123456789abcdef0123456789abcdef01234567' 18 | - key: 'localnet.my-domain.my-label' 19 | value: 'my .+ label' 20 | isRegex: true 21 | - key: 'label-with-empty-val' 22 | value: '' 23 | unexposedPorts: ['80'] 24 | unmountedVolumes: ['/root'] 25 | -------------------------------------------------------------------------------- /tests/arm64/ubuntu_22_04_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: 'apt-get' 4 | command: 'apt-get' 5 | args: ['help'] 6 | excludedError: ['.*FAIL.*'] 7 | expectedOutput: ['.*Usage.*'] 8 | - name: 'apt-config' 9 | command: 'apt-config' 10 | args: ['dump'] 11 | expectedOutput: ['APT::AutoRemove'] 12 | - name: 'path' 13 | command: 'sh' 14 | args: ['-c', 'echo $PATH'] 15 | expectedOutput: ['/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'] 16 | fileContentTests: 17 | - name: 'Debian Sources' 18 | excludedContents: ['.*gce_debian_mirror.*'] 19 | expectedContents: ['.*ports\.ubuntu\.com.*'] 20 | path: '/etc/apt/sources.list' 21 | - name: 'Passwd file' 22 | expectedContents: ['root:x:0:0:root:/root:/bin/bash'] 23 | path: '/etc/passwd' 24 | fileExistenceTests: 25 | - name: 'Root' 26 | path: '/' 27 | shouldExist: true 28 | uid: 0 29 | gid: 0 30 | - name: 'Date' 31 | path: '/bin/date' 32 | isExecutableBy: 'owner' 33 | - name: 'Hosts File' 34 | path: '/etc/hosts' 35 | shouldExist: true 36 | - name: 'Machine ID' 37 | path: '/etc/machine-id' 38 | - name: 'Dummy File' 39 | path: '/etc/dummy' 40 | shouldExist: false 41 | licenseTests: 42 | - debian: false 43 | files: 44 | - "/usr/share/doc/ubuntu-keyring/copyright" 45 | - "/usr/share/doc/dash/copyright" 46 | -------------------------------------------------------------------------------- /tests/envfile: -------------------------------------------------------------------------------- 1 | FOO=keepitsecret! 2 | BAR=keepitsafe! -------------------------------------------------------------------------------- /tests/ppc64le/ubuntu_22_04_containeropts_env_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test envVars containerRunOptions" 4 | command: "printenv" 5 | expectedOutput: [".*(FOO|BAR)=keepit(secret|safe)!.*"] 6 | containerRunOptions: 7 | envVars: 8 | - FOO 9 | - BAR 10 | 11 | -------------------------------------------------------------------------------- /tests/ppc64le/ubuntu_22_04_containeropts_envfile_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test envFile containerRunOptions" 4 | command: "printenv" 5 | expectedOutput: [".*(FOO|BAR)=keepit(secret|safe)!.*"] 6 | containerRunOptions: 7 | envFile: tests/envfile 8 | -------------------------------------------------------------------------------- /tests/ppc64le/ubuntu_22_04_containeropts_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test Capabilities containerRunOptions" 4 | command: "capsh" 5 | args: ["--print"] 6 | expectedOutput: 7 | - ".*cap_sys_admin.*" 8 | - name: "Test bindMounts containerRunOptions" 9 | command: "test" 10 | args: 11 | - "-d" 12 | - "/tmp/test" 13 | exitCode: 0 14 | containerRunOptions: 15 | privileged: true 16 | capabilities: 17 | - "sys_admin" 18 | bindMounts: 19 | - "/tmp/test:/tmp/test" 20 | -------------------------------------------------------------------------------- /tests/ppc64le/ubuntu_22_04_containeropts_user_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: 'apt-get' 4 | command: 'bash' 5 | args: 6 | - -c 7 | - | 8 | whoami 9 | apt-get update 10 | containerRunOptions: 11 | user: 'root' 12 | -------------------------------------------------------------------------------- /tests/ppc64le/ubuntu_22_04_failure_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '1.0.0' 2 | commandTests: 3 | - name: 'bad apt-get-command' 4 | command: ['apt-get', 'dslkfjasl'] 5 | excludedError: ['.*FAIL.*'] 6 | expectedOutput: ['.*Usage.*'] 7 | - name: 'apt-config' 8 | command: ['apt-config', 'dump'] 9 | expectedOutput: ['Acquire::Retries "3"'] 10 | name: 'apt-config' 11 | - name: 'path' 12 | command: ['sh', '-c', 'echo $PATH'] 13 | expectedOutput: ['/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'] 14 | fileContentTests: 15 | - name: 'Debian Sources' 16 | excludedContents: ['.*gce_debian_mirror.*'] 17 | expectedContents: ['.*httpredir\.debian\.org.*'] 18 | path: '/etc/apt/sources.list' 19 | - name: 'Wrong Retry Policy' 20 | expectedContents: ['Acquire::Retries 4;'] 21 | path: '/etc/apt/apt.conf.d/apt-retry' 22 | fileExistenceTests: 23 | - name: 'Fake Dir' 24 | path: '/foo/bar' 25 | shouldExist: true 26 | - name: 'Wrong permissions' 27 | path: '/etc/apt/sources.list' 28 | permissions: '-rwxrwxrwx' 29 | shouldExist: true 30 | licenseTests: 31 | - debian: true 32 | files: 33 | -------------------------------------------------------------------------------- /tests/ppc64le/ubuntu_22_04_metadata_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | metadataTest: 3 | envVars: 4 | - key: 'SOME_KEY' 5 | value: 'SOME_VAL' 6 | - key: 'EMPTY_VAR' 7 | value: '' 8 | - key: 'FOO_BAR' 9 | value: 'FOO\:BAR=BAZ' 10 | - key: 'REGEX_VAR' 11 | value: '[a-z]+-2\.1\.*' 12 | isRegex: true 13 | unboundEnvVars: 14 | - key: 'BAR_FOO' 15 | labels: 16 | - key: 'localnet.localdomain.commit_hash' 17 | value: '0123456789abcdef0123456789abcdef01234567' 18 | - key: 'localnet.my-domain.my-label' 19 | value: 'my .+ label' 20 | isRegex: true 21 | - key: 'label-with-empty-val' 22 | value: '' 23 | unexposedPorts: ['80'] 24 | unmountedVolumes: ['/root'] 25 | -------------------------------------------------------------------------------- /tests/ppc64le/ubuntu_22_04_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: 'apt-get' 4 | command: 'apt-get' 5 | args: ['help'] 6 | excludedError: ['.*FAIL.*'] 7 | expectedOutput: ['.*Usage.*'] 8 | - name: 'apt-config' 9 | command: 'apt-config' 10 | args: ['dump'] 11 | expectedOutput: ['APT::AutoRemove'] 12 | - name: 'path' 13 | command: 'sh' 14 | args: ['-c', 'echo $PATH'] 15 | expectedOutput: ['/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'] 16 | fileContentTests: 17 | - name: 'Debian Sources' 18 | excludedContents: ['.*gce_debian_mirror.*'] 19 | expectedContents: ['.*ports\.ubuntu\.com.*'] 20 | path: '/etc/apt/sources.list' 21 | - name: 'Passwd file' 22 | expectedContents: ['root:x:0:0:root:/root:/bin/bash'] 23 | path: '/etc/passwd' 24 | fileExistenceTests: 25 | - name: 'Root' 26 | path: '/' 27 | shouldExist: true 28 | uid: 0 29 | gid: 0 30 | - name: 'Date' 31 | path: '/bin/date' 32 | isExecutableBy: 'owner' 33 | - name: 'Hosts File' 34 | path: '/etc/hosts' 35 | shouldExist: true 36 | - name: 'Machine ID' 37 | path: '/etc/machine-id' 38 | - name: 'Dummy File' 39 | path: '/etc/dummy' 40 | shouldExist: false 41 | licenseTests: 42 | - debian: false 43 | files: 44 | - "/usr/share/doc/ubuntu-keyring/copyright" 45 | - "/usr/share/doc/dash/copyright" 46 | -------------------------------------------------------------------------------- /tests/s390x/ubuntu_22_04_containeropts_env_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test envVars containerRunOptions" 4 | command: "printenv" 5 | expectedOutput: [".*(FOO|BAR)=keepit(secret|safe)!.*"] 6 | containerRunOptions: 7 | envVars: 8 | - FOO 9 | - BAR 10 | 11 | -------------------------------------------------------------------------------- /tests/s390x/ubuntu_22_04_containeropts_envfile_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test envFile containerRunOptions" 4 | command: "printenv" 5 | expectedOutput: [".*(FOO|BAR)=keepit(secret|safe)!.*"] 6 | containerRunOptions: 7 | envFile: tests/envfile 8 | -------------------------------------------------------------------------------- /tests/s390x/ubuntu_22_04_containeropts_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: "Test Capabilities containerRunOptions" 4 | command: "capsh" 5 | args: ["--print"] 6 | expectedOutput: 7 | - ".*cap_sys_admin.*" 8 | - name: "Test bindMounts containerRunOptions" 9 | command: "test" 10 | args: 11 | - "-d" 12 | - "/tmp/test" 13 | exitCode: 0 14 | containerRunOptions: 15 | privileged: true 16 | capabilities: 17 | - "sys_admin" 18 | bindMounts: 19 | - "/tmp/test:/tmp/test" 20 | -------------------------------------------------------------------------------- /tests/s390x/ubuntu_22_04_containeropts_user_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: 'apt-get' 4 | command: 'bash' 5 | args: 6 | - -c 7 | - | 8 | whoami 9 | apt-get update 10 | containerRunOptions: 11 | user: 'root' 12 | -------------------------------------------------------------------------------- /tests/s390x/ubuntu_22_04_failure_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '1.0.0' 2 | commandTests: 3 | - name: 'bad apt-get-command' 4 | command: ['apt-get', 'dslkfjasl'] 5 | excludedError: ['.*FAIL.*'] 6 | expectedOutput: ['.*Usage.*'] 7 | - name: 'apt-config' 8 | command: ['apt-config', 'dump'] 9 | expectedOutput: ['Acquire::Retries "3"'] 10 | name: 'apt-config' 11 | - name: 'path' 12 | command: ['sh', '-c', 'echo $PATH'] 13 | expectedOutput: ['/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'] 14 | fileContentTests: 15 | - name: 'Debian Sources' 16 | excludedContents: ['.*gce_debian_mirror.*'] 17 | expectedContents: ['.*httpredir\.debian\.org.*'] 18 | path: '/etc/apt/sources.list' 19 | - name: 'Wrong Retry Policy' 20 | expectedContents: ['Acquire::Retries 4;'] 21 | path: '/etc/apt/apt.conf.d/apt-retry' 22 | fileExistenceTests: 23 | - name: 'Fake Dir' 24 | path: '/foo/bar' 25 | shouldExist: true 26 | - name: 'Wrong permissions' 27 | path: '/etc/apt/sources.list' 28 | permissions: '-rwxrwxrwx' 29 | shouldExist: true 30 | licenseTests: 31 | - debian: true 32 | files: 33 | -------------------------------------------------------------------------------- /tests/s390x/ubuntu_22_04_metadata_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | metadataTest: 3 | envVars: 4 | - key: 'SOME_KEY' 5 | value: 'SOME_VAL' 6 | - key: 'EMPTY_VAR' 7 | value: '' 8 | - key: 'FOO_BAR' 9 | value: 'FOO\:BAR=BAZ' 10 | - key: 'REGEX_VAR' 11 | value: '[a-z]+-2\.1\.*' 12 | isRegex: true 13 | unboundEnvVars: 14 | - key: 'BAR_FOO' 15 | labels: 16 | - key: 'localnet.localdomain.commit_hash' 17 | value: '0123456789abcdef0123456789abcdef01234567' 18 | - key: 'localnet.my-domain.my-label' 19 | value: 'my .+ label' 20 | isRegex: true 21 | - key: 'label-with-empty-val' 22 | value: '' 23 | unexposedPorts: ['80'] 24 | unmountedVolumes: ['/root'] 25 | -------------------------------------------------------------------------------- /tests/s390x/ubuntu_22_04_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' # Make sure to test the latest schema version 2 | commandTests: 3 | - name: 'apt-get' 4 | command: 'apt-get' 5 | args: ['help'] 6 | excludedError: ['.*FAIL.*'] 7 | expectedOutput: ['.*Usage.*'] 8 | - name: 'apt-config' 9 | command: 'apt-config' 10 | args: ['dump'] 11 | expectedOutput: ['APT::AutoRemove'] 12 | - name: 'path' 13 | command: 'sh' 14 | args: ['-c', 'echo $PATH'] 15 | expectedOutput: ['/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'] 16 | fileContentTests: 17 | - name: 'Debian Sources' 18 | excludedContents: ['.*gce_debian_mirror.*'] 19 | expectedContents: ['.*ports\.ubuntu\.com.*'] 20 | path: '/etc/apt/sources.list' 21 | - name: 'Passwd file' 22 | expectedContents: ['root:x:0:0:root:/root:/bin/bash'] 23 | path: '/etc/passwd' 24 | fileExistenceTests: 25 | - name: 'Root' 26 | path: '/' 27 | shouldExist: true 28 | uid: 0 29 | gid: 0 30 | - name: 'Date' 31 | path: '/bin/date' 32 | isExecutableBy: 'owner' 33 | - name: 'Hosts File' 34 | path: '/etc/hosts' 35 | shouldExist: true 36 | - name: 'Machine ID' 37 | path: '/etc/machine-id' 38 | - name: 'Dummy File' 39 | path: '/etc/dummy' 40 | shouldExist: false 41 | licenseTests: 42 | - debian: false 43 | files: 44 | - "/usr/share/doc/ubuntu-keyring/copyright" 45 | - "/usr/share/doc/dash/copyright" 46 | -------------------------------------------------------------------------------- /testutil/util.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func CheckDeepEqual(t *testing.T, expected, actual interface{}, opts ...cmp.Option) { 10 | t.Helper() 11 | if diff := cmp.Diff(actual, expected, opts...); diff != "" { 12 | t.Errorf("%T differ (-got, +want): %s", expected, diff) 13 | return 14 | } 15 | } 16 | --------------------------------------------------------------------------------