├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md └── workflows │ ├── build.yaml │ └── test-action.yaml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile.build ├── LICENSE ├── Makefile ├── README.md ├── RELEASING.md ├── SECURITY.md ├── actions ├── Dockerfile ├── README.md ├── action.yaml └── entrypoint.sh ├── cloudbuild-release.yaml ├── cloudbuild.yaml ├── cmd ├── analyze.go ├── analyze_test.go ├── diff.go ├── diff_test.go ├── root.go ├── root_test.go ├── util │ └── output │ │ └── output.go └── version.go ├── code-of-conduct.md ├── differs ├── apt_diff.go ├── apt_diff_test.go ├── differs.go ├── differs_test.go ├── emerge_diff.go ├── emerge_diff_test.go ├── file_diff.go ├── history_diff.go ├── metadata_diff.go ├── node_diff.go ├── node_diff_test.go ├── package_differs.go ├── pip_diff.go ├── pip_diff_test.go ├── rpm_diff.go ├── rpm_diff_test.go ├── size_diff.go └── testDirs │ ├── exact.json │ ├── extra.json │ ├── noPackages │ ├── not_a_package │ │ └── file │ └── var │ │ └── db │ │ └── pkg │ │ └── dir │ │ └── file │ ├── packageEmerge │ └── var │ │ └── db │ │ └── pkg │ │ ├── dev-python │ │ ├── pkg1-0.0.1 │ │ │ └── SIZE │ │ └── pkg2-0.0.2 │ │ │ └── SIZE │ │ └── sys-libs │ │ └── pkg3-0.0.3 │ │ └── SIZE │ ├── packageMulti │ ├── node_modules │ │ ├── pac1 │ │ │ └── package.json │ │ └── pac2 │ │ │ └── package.json │ ├── usr │ │ └── local │ │ │ └── lib │ │ │ └── node_modules │ │ │ └── pac2 │ │ │ └── package.json │ └── var │ │ └── lib │ │ └── dpkg │ │ └── status │ ├── packageOne │ ├── node_modules │ │ ├── pac1 │ │ │ └── package.json │ │ └── pac3 │ │ │ └── package.json │ ├── usr │ │ └── local │ │ │ └── lib │ │ │ └── node_modules │ │ │ └── pac2 │ │ │ └── package.json │ └── var │ │ └── lib │ │ └── dpkg │ │ └── status │ ├── pipTests │ ├── noPackagesTest │ │ ├── noPackagesInstalled │ │ │ └── layer │ │ │ │ └── usr │ │ │ │ └── local │ │ │ │ └── lib │ │ │ │ └── python3.6 │ │ │ │ └── site-packages │ │ │ │ └── file │ │ └── noSitePackagesFolder │ │ │ └── layer │ │ │ └── usr │ │ │ └── local │ │ │ └── lib │ │ │ └── python3.6 │ │ │ └── notSitePackages │ │ │ └── file │ ├── packagesMultiVersion │ │ └── usr │ │ │ └── local │ │ │ └── lib │ │ │ ├── python2.7 │ │ │ └── site-packages │ │ │ │ ├── packageone-0.1.1.dist-info │ │ │ │ └── file │ │ │ │ ├── packageone │ │ │ │ └── file │ │ │ │ ├── script3-3.0.dist-info │ │ │ │ └── file │ │ │ │ └── script3.py │ │ │ └── python3.6 │ │ │ └── site-packages │ │ │ ├── packageone-3.6.9.dist-info │ │ │ └── file │ │ │ ├── packageone │ │ │ └── file │ │ │ ├── packagetwo-4.6.2.dist-info │ │ │ └── file │ │ │ ├── packagetwo │ │ │ └── file │ │ │ ├── script1-1.0.dist-info │ │ │ └── file │ │ │ ├── script1.py │ │ │ ├── script2-2.0.dist-info │ │ │ └── file │ │ │ └── script2.py │ ├── packagesSingleVersion │ │ └── usr │ │ │ └── local │ │ │ └── lib │ │ │ └── python3.6 │ │ │ └── site-packages │ │ │ ├── packageone-3.6.9.dist-info │ │ │ └── file │ │ │ ├── packageone │ │ │ └── file │ │ │ ├── packagetwo-4.6.2.dist-info │ │ │ └── file │ │ │ ├── packagetwo │ │ │ └── file │ │ │ ├── script1-1.0.dist-info │ │ │ └── file │ │ │ ├── script1.py │ │ │ ├── script2-2.0.dist-info │ │ │ └── file │ │ │ └── script2.py │ ├── pythonPathTests │ │ ├── pythonPath1 │ │ │ ├── packageseven-4.6.2.dist-info │ │ │ │ └── file │ │ │ ├── packageseven │ │ │ │ └── file │ │ │ ├── packagesix-3.6.9.dist-info │ │ │ │ └── file │ │ │ ├── packagesix │ │ │ │ └── file │ │ │ ├── script1.py │ │ │ └── script2.py │ │ ├── pythonPath2 │ │ │ └── subdir │ │ │ │ ├── packagefive-3.6.9.dist-info │ │ │ │ └── file │ │ │ │ └── packagefive │ │ │ │ └── file │ │ └── usr │ │ │ └── local │ │ │ └── lib │ │ │ └── python3.6 │ │ │ └── site-packages │ │ │ ├── packageone-3.6.9.dist-info │ │ │ └── file │ │ │ ├── packageone │ │ │ └── file │ │ │ ├── packagetwo-4.6.2.dist-info │ │ │ └── file │ │ │ └── packagetwo │ │ │ └── file │ └── pythonVersionTests │ │ ├── 2VersionLayer │ │ └── usr │ │ │ └── local │ │ │ └── lib │ │ │ ├── python2.7 │ │ │ └── file │ │ │ └── python3.6 │ │ │ └── file │ │ ├── noLibLayer │ │ └── usr │ │ │ └── local │ │ │ └── notLib │ │ │ └── file │ │ ├── noPythonLayer │ │ └── usr │ │ │ └── local │ │ │ └── lib │ │ │ └── notPython │ │ │ └── file │ │ ├── version2.7Layer │ │ └── usr │ │ │ └── local │ │ │ └── lib │ │ │ └── python2.7 │ │ │ └── file │ │ └── version3.6Layer │ │ └── usr │ │ └── local │ │ └── lib │ │ └── python3.6 │ │ └── file │ └── pipTests2 │ ├── noPackagesTest │ ├── noPackagesInstalled │ │ └── usr │ │ │ └── local │ │ │ └── lib │ │ │ └── python3.6 │ │ │ └── site-packages │ │ │ └── file │ └── noSitePackagesFolder │ │ └── usr │ │ └── local │ │ └── lib │ │ └── python3.6 │ │ └── notSitePackages │ │ └── file │ ├── packagesMultiVersion │ └── usr │ │ └── local │ │ └── lib │ │ ├── python2.7 │ │ └── site-packages │ │ │ ├── packageone-0.1.1.dist-info │ │ │ └── file │ │ │ ├── packageone │ │ │ └── file │ │ │ ├── script3-3.0.dist-info │ │ │ └── file │ │ │ └── script3.py │ │ └── python3.6 │ │ └── site-packages │ │ ├── packageone-3.6.9.dist-info │ │ └── file │ │ ├── packageone │ │ └── file │ │ ├── packagetwo-4.6.2.dist-info │ │ └── file │ │ ├── packagetwo │ │ └── file │ │ ├── script1-1.0.dist-info │ │ └── file │ │ ├── script1.py │ │ ├── script2-2.0.dist-info │ │ └── file │ │ └── script2.py │ ├── packagesOneLayer │ └── usr │ │ └── local │ │ └── lib │ │ └── python3.6 │ │ └── site-packages │ │ ├── packageone-3.6.9.dist-info │ │ └── file │ │ ├── packageone │ │ └── file │ │ ├── packagetwo-4.6.2.dist-info │ │ └── file │ │ ├── packagetwo │ │ └── file │ │ ├── script1-1.0.dist-info │ │ └── file │ │ ├── script1.py │ │ ├── script2-2.0.dist-info │ │ └── file │ │ └── script2.py │ └── pythonVersionTests │ ├── 2VersionLayer │ └── usr │ │ └── local │ │ └── lib │ │ ├── python2.7 │ │ └── file │ │ └── python3.6 │ │ └── file │ ├── noLibLayer │ └── usr │ │ └── local │ │ └── notLib │ │ └── file │ ├── noPythonLayer │ └── usr │ │ └── local │ │ └── lib │ │ └── notPython │ │ └── file │ ├── version2.7Layer │ └── usr │ │ └── local │ │ └── lib │ │ └── python2.7 │ │ └── file │ └── version3.6Layer │ └── usr │ └── local │ └── lib │ └── python3.6 │ └── file ├── go.mod ├── go.sum ├── hack ├── hooks │ └── pre-commit ├── release.sh └── release_notes │ └── listpullreqs.go ├── main.go ├── pkg └── util │ ├── fs_utils.go │ ├── image_utils.go │ ├── tar_utils.go │ ├── test_utils.go │ └── transport_builder.go ├── setup-tests ├── Dockerfile.diffBase ├── Dockerfile.diffLayerBase ├── Dockerfile.diffLayerModified ├── Dockerfile.diffModified ├── README.md └── init-tests.sh ├── test.sh ├── tests ├── apt_analysis_expected.json ├── apt_diff_expected.json ├── apt_sorted_diff_expected.json ├── file_diff_expected.json ├── file_layer_analysis_expected.json ├── file_layer_diff_expected.json ├── file_sorted_analysis_expected.json ├── hist_diff_expected.json ├── integration_test.go ├── metadata_diff_expected.json ├── multi_diff_expected.json ├── node_analysis_expected.json ├── node_diff_order_expected.json ├── pip_analysis_expected.json ├── rpm_analysis_expected.json ├── rpm_diff_expected.json ├── size_analysis_expected.json ├── size_diff_expected.json ├── size_layer_analysis_expected.json └── size_layer_diff_expected.json ├── util ├── analyze_output_utils.go ├── diff_output_utils.go ├── diff_utils.go ├── format_utils.go ├── fs_utils_test.go ├── image_utils_test.go ├── output_sort_utils.go ├── output_sort_utils_test.go ├── output_text_utils.go ├── package_diff_utils.go ├── package_diff_utils_test.go ├── size_utils.go ├── tar_utils_test.go ├── template_utils.go ├── testTars │ ├── la-croix-dir-update-actual │ │ ├── lime.txt │ │ ├── nest │ │ │ ├── f1.txt │ │ │ └── f2.txt │ │ ├── passionfruit.txt │ │ └── peach-pear.txt │ ├── la-croix-dir-update.tar │ ├── la-croix-starter │ │ ├── lime.txt │ │ ├── nest │ │ │ └── f1.txt │ │ ├── passionfruit.txt │ │ └── peach-pear.txt │ ├── la-croix-update-actual │ │ ├── lime.txt │ │ ├── nest │ │ │ └── f1.txt │ │ ├── passionfruit.txt │ │ └── peach-pear.txt │ ├── la-croix-update.tar │ ├── la-croix-wh-actual │ │ ├── nest2 │ │ │ └── hello │ │ ├── passionfruit.txt │ │ └── peach-pear.txt │ ├── la-croix-wh.tar │ ├── la-croix1-actual │ │ ├── lime.txt │ │ ├── passionfruit.txt │ │ └── peach-pear.txt │ ├── la-croix1.tar │ ├── la-croix2-actual │ │ ├── lime.txt │ │ ├── nest │ │ │ └── f1.txt │ │ ├── passionfruit.txt │ │ └── peach-pear.txt │ ├── la-croix2.tar │ ├── la-croix3-actual │ │ ├── lime.txt │ │ ├── nest │ │ │ └── f1.txt │ │ ├── nested-dir.tar │ │ ├── passionfruit.txt │ │ └── peach-pear.txt │ ├── la-croix3-full │ │ ├── lime.txt │ │ ├── nest │ │ │ └── f1.txt │ │ ├── nested-dir │ │ │ └── f2.txt │ │ ├── passionfruit.txt │ │ └── peach-pear.txt │ └── la-croix3.tar └── test_files │ ├── dir1 │ ├── file1 │ ├── file2 │ └── file3 │ ├── dir1_copy │ ├── file1 │ ├── file2 │ └── file3 │ ├── dir2 │ ├── file1 │ ├── file2 │ └── file4 │ ├── dir2_modified │ ├── file1 │ ├── file2 │ └── file4 │ ├── file1 │ ├── file1_copy │ └── file2 └── version └── version.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @nkubala @tstromberg 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ### Expected behavior 7 | 8 | ### Actual behavior 9 | 10 | ### Information 11 | 12 | - container-diff version: version... 13 | - Operating system: ... 14 | 15 | ### Steps to reproduce the behavior 16 | 17 | 1. ... 18 | 2. ... 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: "Build and test" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: {} 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 17 | - name: Install Go 18 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 19 | with: 20 | go-version-file: go.mod 21 | - name: Install addlicense 22 | run: go install github.com/google/addlicense@v1.1.1 23 | - name: Run tests 24 | run: make test 25 | - name: Run integration tests 26 | run: make integration 27 | -------------------------------------------------------------------------------- /.github/workflows/test-action.yaml: -------------------------------------------------------------------------------- 1 | name: Test container-diff Action 2 | 3 | on: 4 | pull_request: [] 5 | 6 | jobs: 7 | test-container-diff: 8 | name: Test container-diff 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | # Add more test cases here as necessary 14 | args: 15 | - vanessa/salad --type=file --output=./data.json --json 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Run container-diff 20 | uses: ./actions 21 | with: 22 | args: ${{ matrix.args }} 23 | - name: View output 24 | run: cat ./data.json 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/* 2 | .idea/ 3 | *.iml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | os: linux 3 | go: 4 | - "1.15.x" 5 | sudo: required 6 | services: 7 | - docker 8 | 9 | go_import_path: github.com/GoogleContainerTools/container-diff 10 | 11 | script: 12 | - travis_wait make cross 13 | - travis_wait make test integration 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Setup 7 | 8 | If you haven't written Go code on your machine before, first follow the official 9 | Golang instructions for setting up your environment: https://golang.org/doc/code.html 10 | 11 | Once you have your environment set up, create a fork of the container-diff repository 12 | with your personal GitHub account. Then, clone the fork into your `$GOPATH`: 13 | 14 | ```bash 15 | git clone git@github.com:/container-diff.git 16 | $GOPATH/src/github.com/GoogleContainerTools/container-diff && 17 | cd $GOPATH/src/github.com/GoogleContainerTools/container-diff && 18 | git remote add upstream git@github.com:GoogleContainerTools/container-diff.git 19 | ``` 20 | 21 | The last command here sets the official repository as an upstream repository for 22 | your fork, so you can keep your fork in sync with `MASTER`: 23 | 24 | ```bash 25 | (container-diff) git pull upstream master && git push origin master 26 | ``` 27 | 28 | ## Building 29 | 30 | From the project root, run `make clean && make`. 31 | 32 | ## Code reviews 33 | 34 | All submissions, including submissions by project members, require review. We 35 | use GitHub pull requests for this purpose. Consult 36 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 37 | information on using pull requests. 38 | 39 | ## Contributor License Agreement 40 | 41 | Contributions to this project must be accompanied by a Contributor License 42 | Agreement. You (or your employer) retain the copyright to your contribution, 43 | this simply gives us permission to use and redistribute your contributions as 44 | part of the project. Head over to to see 45 | your current agreements on file or to sign a new one. 46 | 47 | You generally only need to submit a CLA once, so if you've already submitted one 48 | (even if it was for a different project), you probably don't need to do it 49 | again. 50 | 51 | ## Tests 52 | 53 | Before sending a PR, please make sure the included tests pass. 54 | You can run these by running 55 | 56 | ```shell 57 | make test integration 58 | ``` 59 | 60 | from the project root. 61 | 62 | You can also configure the included git hook to run tests automatically on commit. 63 | To do so, run: 64 | 65 | ```shell 66 | ln -s $(pwd)/hack/hooks/* .git/hooks 67 | ``` 68 | 69 | from the project root. 70 | 71 | ## Dependencies 72 | 73 | This project uses [dep](https://github.com/golang/dep) for managing dependencies and the `vendor` directory. 74 | You should not need to know about this tool unless you are trying to add a new dependency or update an existing one. 75 | 76 | See the [dep documentation](https://github.com/golang/dep#adding-a-dependency) for information on how to add a dependency. 77 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | # Dockerfile used to build a build step that builds container-diff in CI. 2 | FROM golang:1.21 3 | RUN apt-get update && apt-get install make 4 | WORKDIR /workspace 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 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 | # On release, remember to also bump const in version/version.go 16 | GIT_VERSION ?= $(shell git describe --always --tags --long --dirty) 17 | 18 | GOOS ?= $(shell go env GOOS) 19 | GOARCH = amd64 20 | BUILD_DIR ?= ./out 21 | ORG := github.com/GoogleContainerTools 22 | PROJECT := container-diff 23 | REPOPATH ?= $(ORG)/$(PROJECT) 24 | RELEASE_BUCKET ?= $(PROJECT) 25 | 26 | SUPPORTED_PLATFORMS := linux-$(GOARCH) darwin-$(GOARCH) windows-$(GOARCH).exe 27 | BUILD_PACKAGE = $(REPOPATH) 28 | 29 | # These build tags are from the containers/image library. 30 | # 31 | # container_image_ostree_stub allows building the library without requiring the libostree development libraries 32 | # container_image_openpgp forces a Golang-only OpenPGP implementation for signature verification instead of the default cgo/gpgme-based implementation 33 | GO_BUILD_TAGS := "container_image_ostree_stub containers_image_openpgp" 34 | GO_LDFLAGS := "-X $(REPOPATH)/version.gitVersion=$(GIT_VERSION)" 35 | GO_FILES := $(shell go list -f '{{join .Deps "\n"}}' $(BUILD_PACKAGE) | grep $(ORG) | xargs go list -f '{{ range $$file := .GoFiles }} {{$$.Dir}}/{{$$file}}{{"\n"}}{{end}}') 36 | 37 | $(BUILD_DIR)/$(PROJECT): $(BUILD_DIR)/$(PROJECT)-$(GOOS)-$(GOARCH) 38 | cp $(BUILD_DIR)/$(PROJECT)-$(GOOS)-$(GOARCH) $@ 39 | 40 | $(BUILD_DIR)/$(PROJECT)-%-$(GOARCH): $(GO_FILES) $(BUILD_DIR) 41 | GOOS=$* GOARCH=$(GOARCH) CGO_ENABLED=0 go build -tags $(GO_BUILD_TAGS) -ldflags $(GO_LDFLAGS) -o $@ $(BUILD_PACKAGE) 42 | 43 | %.sha256: % 44 | shasum -a 256 $< &> $@ 45 | 46 | %.exe: % 47 | mv $< $@ 48 | 49 | $(BUILD_DIR): 50 | mkdir -p $(BUILD_DIR) 51 | 52 | .PRECIOUS: $(foreach platform, $(SUPPORTED_PLATFORMS), $(BUILD_DIR)/$(PROJECT)-$(platform)) 53 | 54 | .PHONY: cross 55 | cross: $(foreach platform, $(SUPPORTED_PLATFORMS), $(BUILD_DIR)/$(PROJECT)-$(platform).sha256) 56 | 57 | .PHONY: test 58 | test: $(BUILD_DIR)/$(PROJECT) 59 | @ ./test.sh 60 | 61 | .PHONY: integration 62 | integration: $(BUILD_DIR)/$(PROJECT) 63 | go test -v -tags integration $(REPOPATH)/tests -timeout 20m 64 | 65 | .PHONY: release 66 | release: cross 67 | gsutil cp $(BUILD_DIR)/$(PROJECT)-* gs://$(RELEASE_BUCKET)/$(shell $(BUILD_DIR)/$(PROJECT) version --short)/ 68 | gsutil cp $(BUILD_DIR)/$(PROJECT)-* gs://$(RELEASE_BUCKET)/latest/ 69 | 70 | .PHONY: clean 71 | clean: 72 | rm -rf $(BUILD_DIR) 73 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing container-diff 2 | 3 | The github.com/GoogleContainerTools/container-diff uses Container Builder triggers to build and release binaries. 4 | These triggers are setup via the Cloud Console, but the builds they execute live in this repo. 5 | 6 | ## Continuous Builds 7 | 8 | Every commit to master is built and pushed automatically to a GCS location named via the COMMIT_SHA. 9 | 10 | ```shell 11 | $ gsutil ls gs://container-diff/builds/ 12 | gs://container-diff/builds/b726215b8b978e1d85257af5c2fd0a6fb8e116fd/ 13 | 14 | $ gsutil ls gs://container-diff/builds/b726215b8b978e1d85257af5c2fd0a6fb8e116fd/ 15 | gs://container-diff/builds/b726215b8b978e1d85257af5c2fd0a6fb8e116fd/container-diff-darwin-amd64.sha256 16 | gs://container-diff/builds/b726215b8b978e1d85257af5c2fd0a6fb8e116fd/container-diff-linux-amd64 17 | gs://container-diff/builds/b726215b8b978e1d85257af5c2fd0a6fb8e116fd/container-diff-linux-amd64.sha256 18 | gs://container-diff/builds/b726215b8b978e1d85257af5c2fd0a6fb8e116fd/container-diff-windows-amd64.exe.sha256 19 | ``` 20 | 21 | The artifacts built and stored are roughly equivalent to the `make cross` target in our Makefile. 22 | 23 | The `cloudbuild.yaml` at the project root is used to build these artifdacts. 24 | 25 | ## Release Builds 26 | 27 | When a new tag is pushed to Github, a second Container Builder pipeline is executed to build and upload release binaries. 28 | These are stored in another GCS location, in the same bucket. 29 | These artifacts are named via the git TAG name. 30 | 31 | ```shell 32 | $ gsutil ls gs://container-diff/ 33 | gs://container-diff/builds/ 34 | gs://container-diff/latest/ 35 | gs://container-diff/v0.2.0/ 36 | gs://container-diff/v0.4.0/ 37 | gs://container-diff/v0.4.1/ 38 | gs://container-diff/v0.5.0/ 39 | ``` 40 | 41 | A second, `latest` location is setup as an alias to the latest release. 42 | This upload and aliases is handled automatically via the `cloudbuild-release.yaml` file located at the project root. 43 | 44 | ## Release Instructions 45 | 46 | To perform a release, follow these steps: 47 | 48 | 1. Select the `commit` to create the release at, preferably from the `master` branch. 49 | 2. Create a new git `tag` and matching Github `release`, pointing to this commit. 50 | This can be done either through the UI or CLI. 51 | 3. Write a descriptive release page. 52 | You can use the notes from the last release to help seed the template. 53 | 3. Wait for the Container Builder release build to complete. 54 | You can follow this in the [UI](https://cloud.google.com/gcr/triggers). 55 | 4. Mirror the release artifacts to the Github release page. 56 | (Download them from GCS and re-upload them to Github). 57 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | To report a security issue, please use http://g.co/vulnz. We use 2 | http://g.co/vulnz for our intake, and do coordination and disclosure here on 3 | GitHub (including using GitHub Security Advisory). The Google Security Team will 4 | respond within 5 working days of your report on g.co/vulnz. 5 | 6 | -------------------------------------------------------------------------------- /actions/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm 2 | 3 | # docker build -f actions/Dockerfile -t googlecontainertools/container-diff . 4 | 5 | LABEL "com.github.actions.name"="container-diff GitHub Action" 6 | LABEL "com.github.actions.description"="use Container-Diff in Github Actions Workflows" 7 | LABEL "com.github.actions.icon"="cloud" 8 | LABEL "com.github.actions.color"="blue" 9 | 10 | LABEL "repository"="https://www.github.com/GoogleContainerTools/container-diff" 11 | LABEL "homepage"="https://www.github.com/GoogleContainerTools/container-diff" 12 | LABEL "maintainer"="Google Inc." 13 | 14 | # Install container-diff latest release 15 | RUN apt-get update && apt-get install -y curl && \ 16 | curl -LO https://storage.googleapis.com/container-diff/latest/container-diff-linux-amd64 && \ 17 | install container-diff-linux-amd64 /usr/local/bin/container-diff 18 | 19 | ADD entrypoint.sh /entrypoint.sh 20 | 21 | RUN mkdir -p /root/.docker && \ 22 | echo {} > /root/.docker/config.json && \ 23 | chmod u+x /entrypoint.sh 24 | 25 | ENTRYPOINT ["/entrypoint.sh"] 26 | -------------------------------------------------------------------------------- /actions/README.md: -------------------------------------------------------------------------------- 1 | # Container Diff for Github Actions 2 | 3 | This is a Github Action to allow you to run Container Diff in a 4 | [Github Actions](https://help.github.com/articles/about-github-actions/#about-github-actions) 5 | workflow. The intended use case is to build a Docker container from the repository, 6 | push it to Docker Hub, and then use container-diff to extract metadata for it that 7 | you can use in other workflows (such as deploying to Github pages). You can also run 8 | container diff to extract metadata for a container you've just built locally in the action. 9 | 10 | ## 1. Action Parameters 11 | 12 | The action accepts the following parameters: 13 | 14 | | Name | Description | Type| Default | Required | 15 | |------|-------------|-----|---------|----------| 16 | | command | main command for container-diff | string | analyze | false | 17 | | args | The full list of arguments to follow container-diff (see example below) | string | help | true | 18 | 19 | See below for a simple example. Another interesting use case would be to generate metadata and upload 20 | to an OCI registry using [OCI Registry As Storage](https://oras.land/). 21 | 22 | ## 2. Run Container Diff 23 | 24 | Given an existing container on Docker Hub, we can run container diff 25 | without doing any kind of build. 26 | 27 | ```yaml 28 | name: Run container-diff 29 | 30 | on: 31 | pull_request: [] 32 | 33 | jobs: 34 | container-diff: 35 | name: Run container-diff 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | - name: Run container-diff 40 | uses: GoogleContainerTools/container-diff/actions@master 41 | with: 42 | # Note this command is the default and does not need to be included 43 | command: analyze 44 | args: vanessa/salad --type=file --output=./data.json --json 45 | - name: View output 46 | run: cat ./data.json 47 | ``` 48 | 49 | In the above, we run container-diff to output apt and pip packages, history, 50 | and the filesystem for the container "vanessa/salad" that already exists on 51 | Docker Hub. We save the result to a data.json output file. The final step in 52 | the workflow (list) is a courtesy to show that the data.json file is generated. 53 | -------------------------------------------------------------------------------- /actions/action.yaml: -------------------------------------------------------------------------------- 1 | name: container-diff 2 | 3 | inputs: 4 | command: 5 | required: true 6 | description: "Container diff command to use (defaults to analyze)" 7 | default: analyze 8 | args: 9 | description: "String of arguments to pass to the container-diff command" 10 | default: help 11 | 12 | runs: 13 | using: 'docker' 14 | image: 'Dockerfile' 15 | -------------------------------------------------------------------------------- /actions/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | command="${INPUT_COMMAND} ${INPUT_ARGS}" 4 | echo "container-diff ${command}" 5 | /usr/local/bin/container-diff ${command} 6 | -------------------------------------------------------------------------------- /cloudbuild-release.yaml: -------------------------------------------------------------------------------- 1 | # This cloudbuild is run on the creation of new tags, which should signify releases. 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', 'Dockerfile.build', '.'] 6 | # Do the go build. 7 | - name: 'builder' 8 | args: ['make', 'cross'] 9 | # Upload to GCS. 10 | - name: 'gcr.io/cloud-builders/gsutil' 11 | args: ['cp', '-r', 'out/*', 'gs://container-diff/$TAG_NAME/'] 12 | # Tag as 'latest'. 13 | - name: 'gcr.io/cloud-builders/gsutil' 14 | args: ['cp', '-r', 'gs://container-diff/$TAG_NAME/*', 'gs://container-diff/latest/'] 15 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | # Build the container that will do the go build. 3 | - name: 'gcr.io/cloud-builders/docker' 4 | args: ['build', '-t', 'builder', '-f', 'Dockerfile.build', '.'] 5 | # Do the go build. 6 | - name: 'builder' 7 | args: ['make', 'cross'] 8 | # Upload to GCS. 9 | - name: 'gcr.io/cloud-builders/gsutil' 10 | args: ['cp', '-r', 'out/*', 'gs://container-diff/builds/$COMMIT_SHA/'] 11 | -------------------------------------------------------------------------------- /cmd/analyze.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 cmd 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | "github.com/GoogleContainerTools/container-diff/cmd/util/output" 24 | "github.com/GoogleContainerTools/container-diff/differs" 25 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 26 | "github.com/pkg/errors" 27 | "github.com/sirupsen/logrus" 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | var analyzeCmd = &cobra.Command{ 32 | Use: "analyze image", 33 | Short: "Analyzes an image: container-diff analyze image", 34 | Long: `Analyzes an image using the specifed analyzers as indicated via --type flag(s). 35 | 36 | For details on how to specify images, run: container-diff help`, 37 | Args: func(cmd *cobra.Command, args []string) error { 38 | if err := validateArgs(args, checkAnalyzeArgNum, checkIfValidAnalyzer); err != nil { 39 | return err 40 | } 41 | return nil 42 | }, 43 | Run: func(cmd *cobra.Command, args []string) { 44 | if err := analyzeImage(args[0], types); err != nil { 45 | logrus.Error(err) 46 | os.Exit(1) 47 | } 48 | }, 49 | } 50 | 51 | func checkAnalyzeArgNum(args []string) error { 52 | if len(args) != 1 { 53 | return errors.New("'analyze' requires one image as an argument: container-diff analyze [image]") 54 | } 55 | return nil 56 | } 57 | 58 | func analyzeImage(imageName string, analyzerArgs []string) error { 59 | analyzeTypes, err := differs.GetAnalyzers(analyzerArgs) 60 | if err != nil { 61 | return errors.Wrap(err, "getting analyzers") 62 | } 63 | 64 | image, err := getImage(imageName) 65 | if err != nil { 66 | return errors.Wrapf(err, "error retrieving image %s", imageName) 67 | } 68 | 69 | if noCache && !save { 70 | defer pkgutil.CleanupImage(image) 71 | } 72 | if err != nil { 73 | return fmt.Errorf("error processing image: %s", err) 74 | } 75 | 76 | req := differs.SingleRequest{ 77 | Image: image, 78 | AnalyzeTypes: analyzeTypes} 79 | analyses, err := req.GetAnalysis() 80 | if err != nil { 81 | return fmt.Errorf("error performing image analysis: %s", err) 82 | } 83 | 84 | logrus.Info("retrieving analyses") 85 | outputResults(analyses) 86 | 87 | if noCache && save { 88 | logrus.Infof("image was saved at %s", image.FSPath) 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func init() { 95 | RootCmd.AddCommand(analyzeCmd) 96 | addSharedFlags(analyzeCmd) 97 | output.AddFlags(analyzeCmd) 98 | } 99 | -------------------------------------------------------------------------------- /cmd/analyze_test.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 cmd 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | var analyzeArgNumTests = []testpair{ 24 | {[]string{}, false}, 25 | {[]string{"one"}, true}, 26 | {[]string{"one", "two"}, false}, 27 | } 28 | 29 | func TestAnalyzeArgNum(t *testing.T) { 30 | for _, test := range analyzeArgNumTests { 31 | err := checkAnalyzeArgNum(test.input) 32 | if (err == nil) != test.shouldError { 33 | if test.shouldError { 34 | t.Errorf("Expected error but got none") 35 | } else { 36 | t.Errorf("Got unexpected error: %s", err) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cmd/diff.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 cmd 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "strings" 23 | "sync" 24 | 25 | "github.com/GoogleContainerTools/container-diff/cmd/util/output" 26 | "github.com/GoogleContainerTools/container-diff/differs" 27 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 28 | "github.com/GoogleContainerTools/container-diff/util" 29 | "github.com/pkg/errors" 30 | "github.com/sirupsen/logrus" 31 | "github.com/spf13/cobra" 32 | ) 33 | 34 | var filename string 35 | 36 | var diffCmd = &cobra.Command{ 37 | Use: "diff image1 image2", 38 | Short: "Compare two images: container-diff diff image1 image2", 39 | Long: `Compares two images using the specifed analyzers as indicated via --type flag(s). 40 | 41 | For details on how to specify images, run: container-diff help`, 42 | Args: func(cmd *cobra.Command, args []string) error { 43 | if err := validateArgs(args, checkDiffArgNum, checkIfValidAnalyzer, checkFilenameFlag); err != nil { 44 | return err 45 | } 46 | return nil 47 | }, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | if err := diffImages(args[0], args[1], types); err != nil { 50 | logrus.Error(err) 51 | os.Exit(1) 52 | } 53 | }, 54 | } 55 | 56 | func checkDiffArgNum(args []string) error { 57 | if len(args) != 2 { 58 | return errors.New("'diff' requires two images as arguments: container-diff diff [image1] [image2]") 59 | } 60 | return nil 61 | } 62 | 63 | func checkFilenameFlag(_ []string) error { 64 | if filename == "" { 65 | return nil 66 | } 67 | for _, t := range types { 68 | if t == "file" { 69 | return nil 70 | } 71 | } 72 | return errors.New("please include --type=file with the --filename flag") 73 | } 74 | 75 | // processImage is a concurrency-friendly wrapper around getImageForName 76 | func processImage(imageName string, errChan chan<- error) *pkgutil.Image { 77 | image, err := getImage(imageName) 78 | if err != nil { 79 | errChan <- fmt.Errorf("error retrieving image %s: %s", imageName, err) 80 | } 81 | return &image 82 | } 83 | 84 | // collects errors from a channel and combines them 85 | // assumes channel has already been closed 86 | func readErrorsFromChannel(c chan error) error { 87 | errs := []string{} 88 | for err := range c { 89 | errs = append(errs, err.Error()) 90 | } 91 | 92 | if len(errs) > 0 { 93 | return errors.New(strings.Join(errs, "\n")) 94 | } 95 | return nil 96 | } 97 | 98 | func diffImages(image1Arg, image2Arg string, diffArgs []string) error { 99 | diffTypes, err := differs.GetAnalyzers(diffArgs) 100 | if err != nil { 101 | return errors.Wrap(err, "getting analyzers") 102 | } 103 | 104 | var wg sync.WaitGroup 105 | wg.Add(2) 106 | 107 | logrus.Infof("starting diff on images %s and %s, using differs: %s\n", image1Arg, image2Arg, diffArgs) 108 | 109 | var image1, image2 *pkgutil.Image 110 | errChan := make(chan error, 2) 111 | 112 | go func() { 113 | defer wg.Done() 114 | image1 = processImage(image1Arg, errChan) 115 | }() 116 | go func() { 117 | defer wg.Done() 118 | image2 = processImage(image2Arg, errChan) 119 | }() 120 | 121 | wg.Wait() 122 | close(errChan) 123 | 124 | if noCache && !save { 125 | defer pkgutil.CleanupImage(*image1) 126 | defer pkgutil.CleanupImage(*image2) 127 | } 128 | 129 | if err := readErrorsFromChannel(errChan); err != nil { 130 | return err 131 | } 132 | 133 | logrus.Info("computing diffs") 134 | req := differs.DiffRequest{ 135 | Image1: *image1, 136 | Image2: *image2, 137 | DiffTypes: diffTypes} 138 | diffs, err := req.GetDiff() 139 | if err != nil { 140 | return fmt.Errorf("could not retrieve diff: %s", err) 141 | } 142 | outputResults(diffs) 143 | 144 | if filename != "" { 145 | logrus.Info("computing filename diffs") 146 | err := diffFile(image1, image2) 147 | if err != nil { 148 | return err 149 | } 150 | } 151 | 152 | if noCache && save { 153 | logrus.Infof("images were saved at %s and %s", image1.FSPath, 154 | image2.FSPath) 155 | } 156 | return nil 157 | } 158 | 159 | func diffFile(image1, image2 *pkgutil.Image) error { 160 | diff, err := util.DiffFile(image1, image2, filename) 161 | if err != nil { 162 | return err 163 | } 164 | writer, err := getWriter(outputFile) 165 | if err != nil { 166 | return err 167 | } 168 | util.TemplateOutput(writer, diff, "FilenameDiff") 169 | if err != nil { 170 | logrus.Error(err) 171 | return err 172 | } 173 | return nil 174 | } 175 | 176 | func init() { 177 | diffCmd.Flags().StringVarP(&filename, "filename", "f", "", "Set this flag to the path of a file in both containers to view the diff of the file. Must be used with --type=file flag.") 178 | RootCmd.AddCommand(diffCmd) 179 | addSharedFlags(diffCmd) 180 | output.AddFlags(diffCmd) 181 | } 182 | -------------------------------------------------------------------------------- /cmd/diff_test.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 cmd 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | var diffArgNumTests = []testpair{ 24 | {[]string{}, true}, 25 | {[]string{"one"}, true}, 26 | {[]string{"one", "two"}, false}, 27 | {[]string{"one", "two", "three"}, true}, 28 | } 29 | 30 | func TestDiffArgNum(t *testing.T) { 31 | for _, test := range diffArgNumTests { 32 | err := checkDiffArgNum(test.input) 33 | checkError(t, err, test.shouldError) 34 | } 35 | } 36 | 37 | type imageDiff struct { 38 | image1 string 39 | image2 string 40 | shouldError bool 41 | } 42 | 43 | var imageDiffs = []imageDiff{ 44 | {"", "", true}, 45 | {"gcr.io/google-appengine/python", "gcr.io/google-appengine/debian9", false}, 46 | {"gcr.io/google-appengine/python", "cats", true}, 47 | {"mcr.microsoft.com/mcr/hello-world:latest", "mcr.microsoft.com/mcr/hello-world:latest", false}, 48 | } 49 | 50 | func TestDiffImages(t *testing.T) { 51 | for _, test := range imageDiffs { 52 | err := diffImages(test.image1, test.image2, []string{"apt"}) 53 | checkError(t, err, test.shouldError) 54 | err = diffImages(test.image1, test.image2, []string{"metadata"}) 55 | checkError(t, err, test.shouldError) 56 | } 57 | } 58 | 59 | func checkError(t *testing.T, err error, shouldError bool) { 60 | if (err == nil) == shouldError { 61 | if shouldError { 62 | t.Errorf("expected error but got none") 63 | } else { 64 | t.Errorf("got unexpected error: %s", err) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cmd/root_test.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 cmd 18 | 19 | import ( 20 | "os" 21 | "path" 22 | "path/filepath" 23 | "reflect" 24 | "testing" 25 | 26 | homedir "github.com/mitchellh/go-homedir" 27 | ) 28 | 29 | type testpair struct { 30 | input []string 31 | shouldError bool 32 | } 33 | 34 | func TestCacheDir(t *testing.T) { 35 | homeDir, err := homedir.Dir() 36 | if err != nil { 37 | t.Errorf("error getting home dir: %s", err.Error()) 38 | } 39 | tests := []struct { 40 | name string 41 | cliFlag string 42 | envVar string 43 | expectedDir string 44 | imageName string 45 | }{ 46 | { 47 | name: "default cache is at $HOME", 48 | cliFlag: "", 49 | envVar: "", 50 | expectedDir: filepath.Join(homeDir, ".container-diff", "cache"), 51 | imageName: "pancakes", 52 | }, 53 | { 54 | name: "setting cache via --cache-dir", 55 | cliFlag: "/tmp", 56 | envVar: "", 57 | expectedDir: "/tmp/.container-diff/cache", 58 | imageName: "pancakes", 59 | }, 60 | { 61 | name: "setting cache via CONTAINER_DIFF_CACHEDIR", 62 | cliFlag: "", 63 | envVar: "/tmp", 64 | expectedDir: "/tmp/.container-diff/cache", 65 | imageName: "pancakes", 66 | }, 67 | { 68 | name: "command line --cache-dir takes preference to CONTAINER_DIFF_CACHEDIR", 69 | cliFlag: "/tmp", 70 | envVar: "/opt", 71 | expectedDir: "/tmp/.container-diff/cache", 72 | imageName: "pancakes", 73 | }, 74 | } 75 | 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | // set any environment variables 79 | if tt.envVar != "" { 80 | os.Setenv("CONTAINER_DIFF_CACHEDIR", tt.envVar) 81 | } 82 | // Set global flag for cache based on --cache-dir 83 | cacheDir = tt.cliFlag 84 | 85 | // call getCacheDir and make sure return is equal to expected 86 | actualDir, err := getCacheDir(tt.imageName) 87 | if err != nil { 88 | t.Errorf("Error getting cache dir %s: %s", tt.name, err.Error()) 89 | } 90 | 91 | if path.Dir(actualDir) != tt.expectedDir { 92 | t.Errorf("%s\nexpected: %v\ngot: %v", tt.name, tt.expectedDir, actualDir) 93 | } 94 | }, 95 | ) 96 | } 97 | } 98 | 99 | func TestMultiValueFlag_Set_shouldDedupeRepeatedArguments(t *testing.T) { 100 | var arg multiValueFlag 101 | arg.Set("value1") 102 | arg.Set("value2") 103 | arg.Set("value3") 104 | 105 | arg.Set("value2") 106 | if len(arg) != 3 || reflect.DeepEqual(arg, []string{"value1", "value2", "value3"}) { 107 | t.Error("multiValueFlag should dedupe repeated arguments") 108 | } 109 | } 110 | 111 | func Test_KeyValueArg_Set_shouldSplitArgument(t *testing.T) { 112 | arg := make(keyValueFlag) 113 | arg.Set("key=value") 114 | if arg["key"] != "value" { 115 | t.Error("Invalid split. key=value should be split to key=>value") 116 | } 117 | } 118 | 119 | func Test_KeyValueArg_Set_shouldAcceptEqualAsValue(t *testing.T) { 120 | arg := make(keyValueFlag) 121 | arg.Set("key=value=something") 122 | if arg["key"] != "value=something" { 123 | t.Error("Invalid split. key=value=something should be split to key=>value=something") 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /cmd/util/output/output.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 output 18 | 19 | import ( 20 | "fmt" 21 | "github.com/spf13/cobra" 22 | "os" 23 | ) 24 | 25 | var quiet bool 26 | 27 | // PrintToStdErr prints to stderr if quiet flag isn't enabled 28 | func PrintToStdErr(output string, vars ...interface{}) { 29 | if !quiet { 30 | fmt.Fprintf(os.Stderr, output, vars...) 31 | } 32 | } 33 | 34 | // AddFlags adds quiet flag to suppress output to stderr 35 | func AddFlags(cmd *cobra.Command) { 36 | cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "Suppress output to stderr.") 37 | } 38 | -------------------------------------------------------------------------------- /cmd/version.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 cmd 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/GoogleContainerTools/container-diff/version" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | var versionCmd = &cobra.Command{ 27 | Use: "version", 28 | Short: "Print the version of container-diff", 29 | Long: `Print the version of container-diff.`, 30 | Args: cobra.ExactArgs(0), 31 | Run: func(command *cobra.Command, args []string) { 32 | if shortVersion { 33 | fmt.Println(version.GetShortVersion()) 34 | } else { 35 | fmt.Println(version.GetVersion()) 36 | } 37 | }, 38 | } 39 | 40 | // `version --short` is useful for `make release` 41 | var shortVersion bool 42 | 43 | func init() { 44 | versionCmd.Flags().BoolVarP(&shortVersion, "short", "", false, "Output single vX.Y.Z word") 45 | RootCmd.AddCommand(versionCmd) 46 | } 47 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to the maintainers, the Project Steward(s) for 73 | container-diff. It is the Project Steward’s duty to receive and address reported 74 | violations of the code of conduct. They will then work with a committee 75 | consisting of representatives from the Open Source Programs Office and the 76 | Google Open Source Strategy team. If for any reason you are uncomfortable 77 | reaching out the Project Steward, please email opensource@google.com. 78 | 79 | We will investigate every complaint, but you may not receive a direct response. 80 | We will use our discretion in determining when and how to follow up on reported 81 | incidents, which may range from not taking action to permanent expulsion from 82 | the project and project-sponsored spaces. We will notify the accused of the 83 | report and provide them an opportunity to discuss it before any action is taken. 84 | The identity of the reporter will be omitted from the details of the report 85 | supplied to the accused. In potentially harmful situations, such as ongoing 86 | harassment or threats to anyone's safety, we may take action without notice. 87 | 88 | ## Attribution 89 | 90 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 91 | available at 92 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 93 | -------------------------------------------------------------------------------- /differs/apt_diff.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 differs 18 | 19 | import ( 20 | "bufio" 21 | "os" 22 | "path/filepath" 23 | "strconv" 24 | "strings" 25 | 26 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 27 | "github.com/GoogleContainerTools/container-diff/util" 28 | "github.com/sirupsen/logrus" 29 | ) 30 | 31 | // APT package database location 32 | const dpkgStatusFile string = "var/lib/dpkg/status" 33 | 34 | type AptAnalyzer struct { 35 | } 36 | 37 | func (a AptAnalyzer) Name() string { 38 | return "AptAnalyzer" 39 | } 40 | 41 | // AptDiff compares the packages installed by apt-get. 42 | func (a AptAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { 43 | diff, err := singleVersionDiff(image1, image2, a) 44 | return diff, err 45 | } 46 | 47 | func (a AptAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { 48 | analysis, err := singleVersionAnalysis(image, a) 49 | return analysis, err 50 | } 51 | 52 | func (a AptAnalyzer) getPackages(image pkgutil.Image) (map[string]util.PackageInfo, error) { 53 | return readStatusFile(image.FSPath) 54 | } 55 | 56 | func readStatusFile(root string) (map[string]util.PackageInfo, error) { 57 | packages := make(map[string]util.PackageInfo) 58 | if _, err := os.Stat(root); err != nil { 59 | // invalid image directory path 60 | return packages, err 61 | } 62 | statusFile := filepath.Join(root, dpkgStatusFile) 63 | if _, err := os.Stat(statusFile); err != nil { 64 | // status file does not exist in this layer 65 | return packages, nil 66 | } 67 | if file, err := os.Open(statusFile); err == nil { 68 | // make sure it gets closed 69 | defer file.Close() 70 | 71 | // create a new scanner and read the file line by line 72 | scanner := bufio.NewScanner(file) 73 | var currPackage string 74 | for scanner.Scan() { 75 | currPackage = parseLine(scanner.Text(), currPackage, packages) 76 | } 77 | } else { 78 | return packages, err 79 | } 80 | 81 | return packages, nil 82 | } 83 | 84 | func parseLine(text string, currPackage string, packages map[string]util.PackageInfo) string { 85 | line := strings.Split(text, ": ") 86 | if len(line) == 2 { 87 | key := line[0] 88 | value := line[1] 89 | 90 | switch key { 91 | case "Package": 92 | return value 93 | case "Version": 94 | if packages[currPackage].Version != "" { 95 | logrus.Warningln("Multiple versions of same package detected. Diffing such multi-versioning not yet supported.") 96 | return currPackage 97 | } 98 | modifiedValue := strings.Replace(value, "+", " ", 1) 99 | currPackageInfo, ok := packages[currPackage] 100 | if !ok { 101 | currPackageInfo = util.PackageInfo{} 102 | } 103 | currPackageInfo.Version = modifiedValue 104 | packages[currPackage] = currPackageInfo 105 | return currPackage 106 | 107 | case "Installed-Size": 108 | currPackageInfo, ok := packages[currPackage] 109 | if !ok { 110 | currPackageInfo = util.PackageInfo{} 111 | } 112 | var size int64 113 | var err error 114 | size, err = strconv.ParseInt(value, 10, 64) 115 | if err != nil { 116 | logrus.Errorf("Could not get size for %s: %s", currPackage, err) 117 | size = -1 118 | } 119 | // Installed-Size is in KB, so we convert it to bytes to keep consistent with the tool's size units 120 | currPackageInfo.Size = size * 1024 121 | packages[currPackage] = currPackageInfo 122 | return currPackage 123 | default: 124 | return currPackage 125 | } 126 | } 127 | return currPackage 128 | } 129 | 130 | type AptLayerAnalyzer struct { 131 | } 132 | 133 | func (a AptLayerAnalyzer) Name() string { 134 | return "AptLayerAnalyzer" 135 | } 136 | 137 | // AptDiff compares the packages installed by apt-get. 138 | func (a AptLayerAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { 139 | diff, err := singleVersionLayerDiff(image1, image2, a) 140 | return diff, err 141 | } 142 | 143 | func (a AptLayerAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { 144 | analysis, err := singleVersionLayerAnalysis(image, a) 145 | return analysis, err 146 | } 147 | 148 | func (a AptLayerAnalyzer) getPackages(image pkgutil.Image) ([]map[string]util.PackageInfo, error) { 149 | var packages []map[string]util.PackageInfo 150 | if _, err := os.Stat(image.FSPath); err != nil { 151 | // invalid image directory path 152 | return packages, err 153 | } 154 | statusFile := filepath.Join(image.FSPath, dpkgStatusFile) 155 | if _, err := os.Stat(statusFile); err != nil { 156 | // status file does not exist in this image 157 | return packages, nil 158 | } 159 | for _, layer := range image.Layers { 160 | layerPackages, err := readStatusFile(layer.FSPath) 161 | if err != nil { 162 | return packages, err 163 | } 164 | packages = append(packages, layerPackages) 165 | } 166 | 167 | return packages, nil 168 | } 169 | -------------------------------------------------------------------------------- /differs/apt_diff_test.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 differs 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 24 | "github.com/GoogleContainerTools/container-diff/util" 25 | ) 26 | 27 | func TestParseLine(t *testing.T) { 28 | testCases := []struct { 29 | descrip string 30 | line string 31 | packages map[string]util.PackageInfo 32 | currPackage string 33 | expPackage string 34 | expected map[string]util.PackageInfo 35 | }{ 36 | { 37 | descrip: "Not applicable line", 38 | line: "Garbage: garbage info", 39 | packages: map[string]util.PackageInfo{}, 40 | expPackage: "", 41 | expected: map[string]util.PackageInfo{}, 42 | }, 43 | { 44 | descrip: "Package line", 45 | line: "Package: La-Croix", 46 | currPackage: "Tea", 47 | expPackage: "La-Croix", 48 | packages: map[string]util.PackageInfo{}, 49 | expected: map[string]util.PackageInfo{}, 50 | }, 51 | { 52 | descrip: "Version line", 53 | line: "Version: Lime", 54 | packages: map[string]util.PackageInfo{}, 55 | currPackage: "La-Croix", 56 | expPackage: "La-Croix", 57 | expected: map[string]util.PackageInfo{"La-Croix": {Version: "Lime"}}, 58 | }, 59 | { 60 | descrip: "Version line with deb release info", 61 | line: "Version: Lime+extra_lime", 62 | packages: map[string]util.PackageInfo{}, 63 | currPackage: "La-Croix", 64 | expPackage: "La-Croix", 65 | expected: map[string]util.PackageInfo{"La-Croix": {Version: "Lime extra_lime"}}, 66 | }, 67 | { 68 | descrip: "Size line", 69 | line: "Installed-Size: 12", 70 | packages: map[string]util.PackageInfo{}, 71 | currPackage: "La-Croix", 72 | expPackage: "La-Croix", 73 | expected: map[string]util.PackageInfo{"La-Croix": {Size: 12288}}, 74 | }, 75 | { 76 | descrip: "Pre-existing PackageInfo struct", 77 | line: "Installed-Size: 12", 78 | packages: map[string]util.PackageInfo{"La-Croix": {Version: "Lime"}}, 79 | currPackage: "La-Croix", 80 | expPackage: "La-Croix", 81 | expected: map[string]util.PackageInfo{"La-Croix": {Version: "Lime", Size: 12288}}, 82 | }, 83 | } 84 | 85 | for _, test := range testCases { 86 | currPackage := parseLine(test.line, test.currPackage, test.packages) 87 | if currPackage != test.expPackage { 88 | t.Errorf("Expected current package to be: %s, but got: %s.", test.expPackage, currPackage) 89 | } 90 | if !reflect.DeepEqual(test.packages, test.expected) { 91 | t.Errorf("Expected: %v but got: %v", test.expected, test.packages) 92 | } 93 | } 94 | } 95 | 96 | func TestGetAptPackages(t *testing.T) { 97 | testCases := []struct { 98 | descrip string 99 | path string 100 | expected map[string]util.PackageInfo 101 | err bool 102 | }{ 103 | { 104 | descrip: "no directory", 105 | path: "testDirs/notThere", 106 | expected: map[string]util.PackageInfo{}, 107 | err: true, 108 | }, 109 | { 110 | descrip: "no packages", 111 | path: "testDirs/noPackages", 112 | expected: map[string]util.PackageInfo{}, 113 | }, 114 | { 115 | descrip: "packages in expected location", 116 | path: "testDirs/packageOne", 117 | expected: map[string]util.PackageInfo{ 118 | "pac1": {Version: "1.0"}, 119 | "pac2": {Version: "2.0"}, 120 | "pac3": {Version: "3.0"}}, 121 | }, 122 | } 123 | for _, test := range testCases { 124 | d := AptAnalyzer{} 125 | image := pkgutil.Image{FSPath: test.path} 126 | packages, err := d.getPackages(image) 127 | if err != nil && !test.err { 128 | t.Errorf("Got unexpected error: %s", err) 129 | } 130 | if err == nil && test.err { 131 | t.Errorf("Expected error but got none.") 132 | } 133 | if !reflect.DeepEqual(packages, test.expected) { 134 | t.Errorf("Expected: %v but got: %v", test.expected, packages) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /differs/differs.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 differs 18 | 19 | import ( 20 | "fmt" 21 | 22 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 23 | "github.com/GoogleContainerTools/container-diff/util" 24 | "github.com/sirupsen/logrus" 25 | ) 26 | 27 | const historyAnalyzer = "history" 28 | const metadataAnalyzer = "metadata" 29 | const fileAnalyzer = "file" 30 | const layerAnalyzer = "layer" 31 | const sizeAnalyzer = "size" 32 | const sizeLayerAnalyzer = "sizelayer" 33 | const aptAnalyzer = "apt" 34 | const aptLayerAnalyzer = "aptlayer" 35 | const rpmAnalyzer = "rpm" 36 | const rpmLayerAnalyzer = "rpmlayer" 37 | const pipAnalyzer = "pip" 38 | const nodeAnalyzer = "node" 39 | const emergeAnalyzer = "emerge" 40 | 41 | type DiffRequest struct { 42 | Image1 pkgutil.Image 43 | Image2 pkgutil.Image 44 | DiffTypes []Analyzer 45 | } 46 | 47 | type SingleRequest struct { 48 | Image pkgutil.Image 49 | AnalyzeTypes []Analyzer 50 | } 51 | 52 | type Analyzer interface { 53 | Diff(image1, image2 pkgutil.Image) (util.Result, error) 54 | Analyze(image pkgutil.Image) (util.Result, error) 55 | Name() string 56 | } 57 | 58 | var Analyzers = map[string]Analyzer{ 59 | historyAnalyzer: HistoryAnalyzer{}, 60 | metadataAnalyzer: MetadataAnalyzer{}, 61 | fileAnalyzer: FileAnalyzer{}, 62 | layerAnalyzer: FileLayerAnalyzer{}, 63 | sizeAnalyzer: SizeAnalyzer{}, 64 | sizeLayerAnalyzer: SizeLayerAnalyzer{}, 65 | aptAnalyzer: AptAnalyzer{}, 66 | aptLayerAnalyzer: AptLayerAnalyzer{}, 67 | rpmAnalyzer: RPMAnalyzer{}, 68 | rpmLayerAnalyzer: RPMLayerAnalyzer{}, 69 | pipAnalyzer: PipAnalyzer{}, 70 | nodeAnalyzer: NodeAnalyzer{}, 71 | emergeAnalyzer: EmergeAnalyzer{}, 72 | } 73 | 74 | var LayerAnalyzers = [...]string{layerAnalyzer, sizeLayerAnalyzer, aptLayerAnalyzer, rpmLayerAnalyzer} 75 | 76 | func (req DiffRequest) GetDiff() (map[string]util.Result, error) { 77 | img1 := req.Image1 78 | img2 := req.Image2 79 | diffs := req.DiffTypes 80 | 81 | results := map[string]util.Result{} 82 | for _, differ := range diffs { 83 | if diff, err := differ.Diff(img1, img2); err == nil { 84 | results[differ.Name()] = diff 85 | } else { 86 | logrus.Errorf("error getting diff with %s: %s", differ.Name(), err) 87 | } 88 | } 89 | 90 | var err error 91 | if len(results) == 0 { 92 | err = fmt.Errorf("could not perform diff on %v and %v", img1, img2) 93 | } else { 94 | err = nil 95 | } 96 | 97 | return results, err 98 | } 99 | 100 | func (req SingleRequest) GetAnalysis() (map[string]util.Result, error) { 101 | img := req.Image 102 | analyses := req.AnalyzeTypes 103 | 104 | results := map[string]util.Result{} 105 | for _, analyzer := range analyses { 106 | analyzeName := analyzer.Name() 107 | if analysis, err := analyzer.Analyze(img); err == nil { 108 | results[analyzeName] = analysis 109 | } else { 110 | logrus.Errorf("error getting analysis with %s: %s", analyzeName, err) 111 | } 112 | } 113 | 114 | var err error 115 | if len(results) == 0 { 116 | err = fmt.Errorf("could not perform analysis on %v", img) 117 | } else { 118 | err = nil 119 | } 120 | 121 | return results, err 122 | } 123 | 124 | func GetAnalyzers(analyzeNames []string) ([]Analyzer, error) { 125 | var analyzeFuncs []Analyzer 126 | for _, name := range analyzeNames { 127 | if a, exists := Analyzers[name]; exists { 128 | analyzeFuncs = append(analyzeFuncs, a) 129 | } else { 130 | return nil, fmt.Errorf("unknown analyzer/differ specified: %s", name) 131 | } 132 | } 133 | if len(analyzeFuncs) == 0 { 134 | return nil, fmt.Errorf("no known analyzers/differs specified") 135 | } 136 | return analyzeFuncs, nil 137 | } 138 | -------------------------------------------------------------------------------- /differs/differs_test.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 differs 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | ) 23 | 24 | func TestGetAnalyzers(t *testing.T) { 25 | 26 | tests := []struct { 27 | name string 28 | args []string 29 | want []Analyzer 30 | wantErr bool 31 | }{ 32 | { 33 | name: "given only one type", 34 | args: []string{"history"}, 35 | want: []Analyzer{HistoryAnalyzer{}}, 36 | wantErr: false, 37 | }, 38 | { 39 | name: "given two type", 40 | args: []string{"file", "apt"}, 41 | want: []Analyzer{FileAnalyzer{}, AptAnalyzer{}}, 42 | wantErr: false, 43 | }, 44 | { 45 | name: "given non-existent type", 46 | args: []string{"faketype"}, 47 | want: nil, 48 | wantErr: true, 49 | }, 50 | { 51 | name: "no type given", 52 | args: []string{}, 53 | want: nil, 54 | wantErr: true, 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | got, err := GetAnalyzers(tt.args) 60 | if (err != nil) != tt.wantErr { 61 | t.Errorf("GetAnalyzers() error = %v, wantErr %v", err, tt.wantErr) 62 | return 63 | } else if err != nil && tt.wantErr { 64 | t.Logf("errored out as = %v", err) 65 | } 66 | if !reflect.DeepEqual(got, tt.want) { 67 | t.Errorf("GetAnalyzers() = %#v, want %#v", got, tt.want) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /differs/emerge_diff.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 differs 18 | 19 | import ( 20 | "io/ioutil" 21 | "os" 22 | "path/filepath" 23 | "strconv" 24 | "strings" 25 | 26 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 27 | "github.com/GoogleContainerTools/container-diff/util" 28 | "github.com/sirupsen/logrus" 29 | ) 30 | 31 | // Emerge package database location 32 | const emergePkgFile string = "/var/db/pkg" 33 | 34 | type EmergeAnalyzer struct{} 35 | 36 | func (em EmergeAnalyzer) Name() string { 37 | return "EmergeAnalyzer" 38 | } 39 | 40 | // Diff compares the packages installed by emerge. 41 | func (em EmergeAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { 42 | diff, err := singleVersionDiff(image1, image2, em) 43 | return diff, err 44 | } 45 | 46 | func (em EmergeAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { 47 | analysis, err := singleVersionAnalysis(image, em) 48 | return analysis, err 49 | } 50 | 51 | func (em EmergeAnalyzer) getPackages(image pkgutil.Image) (map[string]util.PackageInfo, error) { 52 | var path string 53 | if image.FSPath == "" { 54 | path = emergePkgFile 55 | } else { 56 | path = filepath.Join(image.FSPath, emergePkgFile) 57 | } 58 | 59 | packages := make(map[string]util.PackageInfo) 60 | if _, err := os.Stat(path); err != nil { 61 | // invalid image directory path 62 | logrus.Errorf("Invalid image directory path %s", path) 63 | return packages, err 64 | } 65 | 66 | contents, err := ioutil.ReadDir(path) 67 | if err != nil { 68 | logrus.Errorf("Non-content in image directory path %s", path) 69 | return packages, err 70 | } 71 | 72 | // for i := 0; i < len(contents); i++ { 73 | for _, c := range contents { 74 | // c := contents[i] 75 | pkgPrefix := c.Name() 76 | pkgContents, err := ioutil.ReadDir(filepath.Join(path, pkgPrefix)) 77 | if err != nil { 78 | return packages, err 79 | } 80 | // for j := 0; j < len(pkgContents); j++ { 81 | for _, c := range pkgContents { 82 | // c := pkgContents[j] 83 | pkgRawName := c.Name() 84 | // usually, the name of a package installed by emerge is formatted as '{pkgName}-{version}' e.g.(pymongo-3.9.0) 85 | s := strings.Split(pkgRawName, "-") 86 | if len(s) != 2 { 87 | continue 88 | } 89 | pkgName, version := s[0], s[1] 90 | pkgPath := filepath.Join(path, pkgPrefix, pkgRawName, "SIZE") 91 | size, err := getPkgSize(pkgPath) 92 | if err != nil { 93 | return packages, err 94 | } 95 | currPackage := util.PackageInfo{Version: version, Size: size} 96 | fullPackageName := strings.Join([]string{pkgPrefix, pkgName}, "/") 97 | packages[fullPackageName] = currPackage 98 | } 99 | } 100 | 101 | return packages, nil 102 | } 103 | 104 | // emerge will count the total size of a package and store it as a SIZE file in pkg metadata directory 105 | // getPkgSize read this SIZE file of a given package 106 | func getPkgSize(pkgPath string) (int64, error) { 107 | sizeFile, err := os.Open(pkgPath) 108 | if err != nil { 109 | logrus.Warnf("unable to open SIZE file for pkg %s", pkgPath) 110 | return 0, err 111 | } 112 | defer sizeFile.Close() 113 | fileBody, err := ioutil.ReadAll(sizeFile) 114 | if err != nil { 115 | logrus.Warnf("unable to read SIZE file for pkg %s", pkgPath) 116 | return 0, err 117 | } 118 | strFileBody := strings.Replace(string(fileBody), "\n", "", -1) 119 | size, err := strconv.ParseInt(strFileBody, 10, 64) 120 | if err != nil { 121 | logrus.Warnf("unable to compute size for pkg %s", pkgPath) 122 | return 0, err 123 | } 124 | return size, nil 125 | } 126 | -------------------------------------------------------------------------------- /differs/emerge_diff_test.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 differs 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 24 | "github.com/GoogleContainerTools/container-diff/util" 25 | ) 26 | 27 | func TestGetEmergePackages(t *testing.T) { 28 | testCases := []struct { 29 | descrip string 30 | path string 31 | expected map[string]util.PackageInfo 32 | err bool 33 | }{ 34 | { 35 | descrip: "no directory", 36 | path: "testDirs/notThere", 37 | expected: map[string]util.PackageInfo{}, 38 | err: true, 39 | }, 40 | { 41 | descrip: "no packages", 42 | path: "testDirs/noPackages", 43 | expected: map[string]util.PackageInfo{}, 44 | }, 45 | { 46 | descrip: "packages in expected location", 47 | path: "testDirs/packageEmerge", 48 | expected: map[string]util.PackageInfo{ 49 | "dev-python/pkg1": {Version: "0.0.1", Size: 167112}, 50 | "dev-python/pkg2": {Version: "0.0.2", Size: 167112}, 51 | "sys-libs/pkg3": {Version: "0.0.3", Size: 167112}}, 52 | }, 53 | } 54 | for _, test := range testCases { 55 | d := EmergeAnalyzer{} 56 | image := pkgutil.Image{FSPath: test.path} 57 | packages, err := d.getPackages(image) 58 | if err != nil && !test.err { 59 | t.Errorf("Got unexpected error: %s", err) 60 | } 61 | if err == nil && test.err { 62 | t.Errorf("Expected error but got none.") 63 | } 64 | if !reflect.DeepEqual(packages, test.expected) { 65 | t.Errorf("Expected: %v but got: %v", test.expected, packages) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /differs/file_diff.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 differs 18 | 19 | import ( 20 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 21 | "github.com/GoogleContainerTools/container-diff/util" 22 | "github.com/sirupsen/logrus" 23 | ) 24 | 25 | type FileAnalyzer struct { 26 | } 27 | 28 | func (a FileAnalyzer) Name() string { 29 | return "FileAnalyzer" 30 | } 31 | 32 | // FileDiff diffs two packages and compares their contents 33 | func (a FileAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { 34 | diff, err := diffImageFiles(image1.FSPath, image2.FSPath) 35 | return &util.DirDiffResult{ 36 | Image1: image1.Source, 37 | Image2: image2.Source, 38 | DiffType: "File", 39 | Diff: diff, 40 | }, err 41 | } 42 | 43 | func (a FileAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { 44 | var result util.FileAnalyzeResult 45 | 46 | imgDir, err := pkgutil.GetDirectory(image.FSPath, true) 47 | if err != nil { 48 | return result, err 49 | } 50 | 51 | result.Image = image.Source 52 | result.AnalyzeType = "File" 53 | result.Analysis = pkgutil.GetDirectoryEntries(imgDir) 54 | return &result, err 55 | } 56 | 57 | func diffImageFiles(img1, img2 string) (util.DirDiff, error) { 58 | var diff util.DirDiff 59 | 60 | img1Dir, err := pkgutil.GetDirectory(img1, true) 61 | if err != nil { 62 | return diff, err 63 | } 64 | img2Dir, err := pkgutil.GetDirectory(img2, true) 65 | if err != nil { 66 | return diff, err 67 | } 68 | 69 | diff, _ = util.DiffDirectory(img1Dir, img2Dir) 70 | return diff, nil 71 | } 72 | 73 | type FileLayerAnalyzer struct { 74 | } 75 | 76 | func (a FileLayerAnalyzer) Name() string { 77 | return "FileLayerAnalyzer" 78 | } 79 | 80 | // FileDiff diffs two packages and compares their contents 81 | func (a FileLayerAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { 82 | var dirDiffs []util.DirDiff 83 | 84 | // Go through each layer of the first image... 85 | for index, layer := range image1.Layers { 86 | if index >= len(image2.Layers) { 87 | continue 88 | } 89 | // ...else, diff as usual 90 | layer2 := image2.Layers[index] 91 | diff, err := diffImageFiles(layer.FSPath, layer2.FSPath) 92 | if err != nil { 93 | return &util.MultipleDirDiffResult{}, err 94 | } 95 | dirDiffs = append(dirDiffs, diff) 96 | } 97 | 98 | // check if there are any additional layers in either image 99 | if len(image1.Layers) != len(image2.Layers) { 100 | if len(image1.Layers) > len(image2.Layers) { 101 | logrus.Infof("%s has additional layers, please use container-diff analyze to view the files in these layers", image1.Source) 102 | } else { 103 | logrus.Infof("%s has additional layers, please use container-diff analyze to view the files in these layers", image2.Source) 104 | } 105 | } 106 | return &util.MultipleDirDiffResult{ 107 | Image1: image1.Source, 108 | Image2: image2.Source, 109 | DiffType: "FileLayer", 110 | Diff: util.MultipleDirDiff{ 111 | DirDiffs: dirDiffs, 112 | }, 113 | }, nil 114 | } 115 | 116 | func (a FileLayerAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { 117 | var directoryEntries [][]pkgutil.DirectoryEntry 118 | for _, layer := range image.Layers { 119 | layerDir, err := pkgutil.GetDirectory(layer.FSPath, true) 120 | if err != nil { 121 | return util.FileLayerAnalyzeResult{}, err 122 | } 123 | directoryEntries = append(directoryEntries, pkgutil.GetDirectoryEntries(layerDir)) 124 | } 125 | 126 | return &util.FileLayerAnalyzeResult{ 127 | Image: image.Source, 128 | AnalyzeType: "FileLayer", 129 | Analysis: directoryEntries, 130 | }, nil 131 | } 132 | -------------------------------------------------------------------------------- /differs/history_diff.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 differs 18 | 19 | import ( 20 | "strings" 21 | 22 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 23 | "github.com/GoogleContainerTools/container-diff/util" 24 | "github.com/google/go-containerregistry/pkg/v1" 25 | ) 26 | 27 | type HistoryAnalyzer struct { 28 | } 29 | 30 | type HistDiff struct { 31 | Adds []string 32 | Dels []string 33 | } 34 | 35 | func (a HistoryAnalyzer) Name() string { 36 | return "HistoryAnalyzer" 37 | } 38 | 39 | func (a HistoryAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { 40 | diff, err := getHistoryDiff(image1, image2) 41 | return &util.HistDiffResult{ 42 | Image1: image1.Source, 43 | Image2: image2.Source, 44 | DiffType: "History", 45 | Diff: diff, 46 | }, err 47 | } 48 | 49 | func (a HistoryAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { 50 | c, err := image.Image.ConfigFile() 51 | if err != nil { 52 | return util.ListAnalyzeResult{}, err 53 | } 54 | history := getHistoryList(c.History) 55 | result := util.ListAnalyzeResult{ 56 | Image: image.Source, 57 | AnalyzeType: "History", 58 | Analysis: history, 59 | } 60 | return &result, nil 61 | } 62 | 63 | func getHistoryDiff(image1, image2 pkgutil.Image) (HistDiff, error) { 64 | c1, err := image1.Image.ConfigFile() 65 | if err != nil { 66 | return HistDiff{}, err 67 | } 68 | c2, err := image2.Image.ConfigFile() 69 | if err != nil { 70 | return HistDiff{}, err 71 | } 72 | history1 := getHistoryList(c1.History) 73 | history2 := getHistoryList(c2.History) 74 | 75 | adds := util.GetAdditions(history1, history2) 76 | dels := util.GetDeletions(history1, history2) 77 | diff := HistDiff{adds, dels} 78 | return diff, nil 79 | } 80 | 81 | func getHistoryList(historyItems []v1.History) []string { 82 | strhistory := make([]string, len(historyItems)) 83 | for i, layer := range historyItems { 84 | strhistory[i] = strings.TrimSpace(layer.CreatedBy) 85 | } 86 | return strhistory 87 | } 88 | -------------------------------------------------------------------------------- /differs/metadata_diff.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 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 differs 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 24 | "github.com/GoogleContainerTools/container-diff/util" 25 | ) 26 | 27 | type MetadataAnalyzer struct { 28 | } 29 | 30 | type MetadataDiff struct { 31 | Adds []string 32 | Dels []string 33 | } 34 | 35 | func (a MetadataAnalyzer) Name() string { 36 | return "MetadataAnalyzer" 37 | } 38 | 39 | func (a MetadataAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { 40 | diff, err := getMetadataDiff(image1, image2) 41 | return &util.MetadataDiffResult{ 42 | Image1: image1.Source, 43 | Image2: image2.Source, 44 | DiffType: "Metadata", 45 | Diff: diff, 46 | }, err 47 | } 48 | 49 | func (a MetadataAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { 50 | analysis, err := getMetadataList(image) 51 | if err != nil { 52 | return &util.ListAnalyzeResult{}, err 53 | } 54 | return &util.ListAnalyzeResult{ 55 | Image: image.Source, 56 | AnalyzeType: "Metadata", 57 | Analysis: analysis, 58 | }, nil 59 | } 60 | 61 | func getMetadataDiff(image1, image2 pkgutil.Image) (MetadataDiff, error) { 62 | m1, err := getMetadataList(image1) 63 | if err != nil { 64 | return MetadataDiff{}, err 65 | } 66 | m2, err := getMetadataList(image2) 67 | if err != nil { 68 | return MetadataDiff{}, err 69 | } 70 | 71 | adds := util.GetAdditions(m1, m2) 72 | dels := util.GetDeletions(m1, m2) 73 | return MetadataDiff{adds, dels}, nil 74 | } 75 | 76 | func getMetadataList(image pkgutil.Image) ([]string, error) { 77 | configFile, err := image.Image.ConfigFile() 78 | if err != nil { 79 | return nil, err 80 | } 81 | c := configFile.Config 82 | 83 | return []string{ 84 | fmt.Sprintf("Domainname: %s", c.Domainname), 85 | fmt.Sprintf("User: %s", c.User), 86 | fmt.Sprintf("AttachStdin: %t", c.AttachStdin), 87 | fmt.Sprintf("AttachStdout: %t", c.AttachStdout), 88 | fmt.Sprintf("AttachStderr: %t", c.AttachStderr), 89 | fmt.Sprintf("ExposedPorts: %v", pkgutil.SortMap(StructMapToStringMap(c.ExposedPorts))), 90 | fmt.Sprintf("Tty: %t", c.Tty), 91 | fmt.Sprintf("OpenStdin: %t", c.OpenStdin), 92 | fmt.Sprintf("StdinOnce: %t", c.StdinOnce), 93 | fmt.Sprintf("Env: %s", strings.Join(c.Env, ",")), 94 | fmt.Sprintf("Cmd: %s", strings.Join(c.Cmd, ",")), 95 | fmt.Sprintf("ArgsEscaped: %t", c.ArgsEscaped), 96 | fmt.Sprintf("Volumes: %v", pkgutil.SortMap(StructMapToStringMap(c.Volumes))), 97 | fmt.Sprintf("Workdir: %s", c.WorkingDir), 98 | fmt.Sprintf("Entrypoint: %s", strings.Join(c.Entrypoint, ",")), 99 | fmt.Sprintf("NetworkDisabled: %t", c.NetworkDisabled), 100 | fmt.Sprintf("MacAddress: %s", c.MacAddress), 101 | fmt.Sprintf("OnBuild: %s", strings.Join(c.OnBuild, ",")), 102 | fmt.Sprintf("Labels: %v", pkgutil.SortMap(c.Labels)), 103 | fmt.Sprintf("StopSignal: %s", c.StopSignal), 104 | fmt.Sprintf("Shell: %s", strings.Join(c.Shell, ",")), 105 | }, nil 106 | } 107 | 108 | // StructMapToStringMap converts map[string]struct{} to map[string]string knowing that the 109 | // struct in the value is always empty 110 | func StructMapToStringMap(m map[string]struct{}) map[string]string { 111 | newMap := make(map[string]string) 112 | for k := range m { 113 | newMap[k] = fmt.Sprintf("%s", m[k]) 114 | } 115 | return newMap 116 | } 117 | -------------------------------------------------------------------------------- /differs/node_diff.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 differs 18 | 19 | import ( 20 | "encoding/json" 21 | "io/ioutil" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | 26 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 27 | "github.com/GoogleContainerTools/container-diff/util" 28 | "github.com/sirupsen/logrus" 29 | ) 30 | 31 | type NodeAnalyzer struct { 32 | } 33 | 34 | func (a NodeAnalyzer) Name() string { 35 | return "NodeAnalyzer" 36 | } 37 | 38 | // NodeDiff compares the packages installed by apt-get. 39 | func (a NodeAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { 40 | diff, err := multiVersionDiff(image1, image2, a) 41 | return diff, err 42 | } 43 | 44 | func (a NodeAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { 45 | analysis, err := multiVersionAnalysis(image, a) 46 | return analysis, err 47 | } 48 | 49 | func (a NodeAnalyzer) getPackages(image pkgutil.Image) (map[string]map[string]util.PackageInfo, error) { 50 | path := image.FSPath 51 | packages := make(map[string]map[string]util.PackageInfo) 52 | if _, err := os.Stat(path); err != nil { 53 | // path provided invalid 54 | return packages, err 55 | } 56 | layerStems, err := buildNodePaths(path) 57 | if err != nil { 58 | logrus.Warningf("Error building JSON paths at %s: %s\n", path, err) 59 | return packages, err 60 | } 61 | 62 | for _, modulesDir := range layerStems { 63 | packageJSONs, _ := util.BuildLayerTargets(modulesDir, "package.json") 64 | for _, currPackage := range packageJSONs { 65 | if _, err := os.Stat(currPackage); err != nil { 66 | // package.json file does not exist at this target path 67 | continue 68 | } 69 | packageJSON, err := readPackageJSON(currPackage) 70 | if err != nil { 71 | logrus.Warningf("Error reading package JSON at %s: %s\n", currPackage, err) 72 | return packages, err 73 | } 74 | // Build PackageInfo for this package occurence 75 | var currInfo util.PackageInfo 76 | currInfo.Version = packageJSON.Version 77 | packagePath := strings.TrimSuffix(currPackage, "package.json") 78 | currInfo.Size = pkgutil.GetSize(packagePath) 79 | mapPath := strings.Replace(packagePath, path, "", 1) 80 | // Check if other package version already recorded 81 | if _, ok := packages[packageJSON.Name]; !ok { 82 | // package not yet seen 83 | infoMap := make(map[string]util.PackageInfo) 84 | infoMap[mapPath] = currInfo 85 | packages[packageJSON.Name] = infoMap 86 | continue 87 | } 88 | packages[packageJSON.Name][mapPath] = currInfo 89 | 90 | } 91 | } 92 | return packages, nil 93 | } 94 | 95 | type nodePackage struct { 96 | Name string `json:"name"` 97 | Version string `json:"version"` 98 | } 99 | 100 | func buildNodePaths(path string) ([]string, error) { 101 | globalPaths := filepath.Join(path, "node_modules") 102 | localPath := filepath.Join(path, "usr/local/lib/node_modules") 103 | return []string{globalPaths, localPath}, nil 104 | } 105 | 106 | func readPackageJSON(path string) (nodePackage, error) { 107 | var currPackage nodePackage 108 | jsonBytes, err := ioutil.ReadFile(path) 109 | if err != nil { 110 | return currPackage, err 111 | } 112 | err = json.Unmarshal(jsonBytes, &currPackage) 113 | if err != nil { 114 | return currPackage, err 115 | } 116 | return currPackage, err 117 | } 118 | -------------------------------------------------------------------------------- /differs/node_diff_test.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 differs 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 24 | "github.com/GoogleContainerTools/container-diff/util" 25 | ) 26 | 27 | func TestGetNodePackages(t *testing.T) { 28 | testCases := []struct { 29 | descrip string 30 | path string 31 | expected map[string]map[string]util.PackageInfo 32 | err bool 33 | }{ 34 | { 35 | descrip: "no directory", 36 | path: "testDirs/notThere", 37 | expected: map[string]map[string]util.PackageInfo{}, 38 | err: true, 39 | }, 40 | { 41 | descrip: "no packages", 42 | path: "testDirs/noPackages", 43 | expected: map[string]map[string]util.PackageInfo{}, 44 | }, 45 | { 46 | descrip: "all packages in one layer", 47 | path: "testDirs/packageOne", 48 | expected: map[string]map[string]util.PackageInfo{ 49 | "pac1": {"/node_modules/pac1/": {Version: "1.0", Size: 41}}, 50 | "pac2": {"/usr/local/lib/node_modules/pac2/": {Version: "2.0", Size: 41}}, 51 | "pac3": {"/node_modules/pac3/": {Version: "3.0", Size: 41}}}, 52 | }, 53 | { 54 | descrip: "Multi version packages", 55 | path: "testDirs/packageMulti", 56 | expected: map[string]map[string]util.PackageInfo{ 57 | "pac1": {"/node_modules/pac1/": {Version: "1.0", Size: 41}}, 58 | "pac2": {"/node_modules/pac2/": {Version: "2.0", Size: 41}, 59 | "/usr/local/lib/node_modules/pac2/": {Version: "3.0", Size: 41}}}, 60 | }, 61 | } 62 | 63 | for _, test := range testCases { 64 | image := pkgutil.Image{FSPath: test.path} 65 | d := NodeAnalyzer{} 66 | packages, err := d.getPackages(image) 67 | if err != nil && !test.err { 68 | t.Errorf("Got unexpected error: %s", err) 69 | } 70 | if err == nil && test.err { 71 | t.Errorf("Expected error but got none.") 72 | } 73 | if !reflect.DeepEqual(packages, test.expected) { 74 | t.Errorf("Expected: %v but got: %v", test.expected, packages) 75 | } 76 | } 77 | } 78 | func TestReadPackageJSON(t *testing.T) { 79 | testCases := []struct { 80 | descrip string 81 | path string 82 | expected nodePackage 83 | err bool 84 | }{ 85 | { 86 | descrip: "Error on non-existent file", 87 | path: "testDirs/not_real.json", 88 | err: true, 89 | }, 90 | { 91 | descrip: "Parse JSON with exact fields", 92 | path: "testDirs/exact.json", 93 | expected: nodePackage{"La-croix", "Lime"}, 94 | }, 95 | { 96 | descrip: "Parse JSON with additional fields", 97 | path: "testDirs/extra.json", 98 | expected: nodePackage{"La-croix", "Lime"}, 99 | }, 100 | } 101 | for _, test := range testCases { 102 | actual, err := readPackageJSON(test.path) 103 | if err != nil && !test.err { 104 | t.Errorf("Got unexpected error: %s", err) 105 | } 106 | if err == nil && test.err { 107 | t.Error("Expected errorbut got none.") 108 | } 109 | if !reflect.DeepEqual(actual, test.expected) { 110 | t.Errorf("Expected: %s but got: %s", test.expected, actual) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /differs/package_differs.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 differs 18 | 19 | import ( 20 | "errors" 21 | "strings" 22 | 23 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 24 | "github.com/GoogleContainerTools/container-diff/util" 25 | "github.com/sirupsen/logrus" 26 | ) 27 | 28 | type MultiVersionPackageAnalyzer interface { 29 | getPackages(image pkgutil.Image) (map[string]map[string]util.PackageInfo, error) 30 | Name() string 31 | } 32 | 33 | type SingleVersionPackageAnalyzer interface { 34 | getPackages(image pkgutil.Image) (map[string]util.PackageInfo, error) 35 | Name() string 36 | } 37 | 38 | type SingleVersionPackageLayerAnalyzer interface { 39 | getPackages(image pkgutil.Image) ([]map[string]util.PackageInfo, error) 40 | Name() string 41 | } 42 | 43 | func multiVersionDiff(image1, image2 pkgutil.Image, differ MultiVersionPackageAnalyzer) (*util.MultiVersionPackageDiffResult, error) { 44 | pack1, err := differ.getPackages(image1) 45 | if err != nil { 46 | return &util.MultiVersionPackageDiffResult{}, err 47 | } 48 | pack2, err := differ.getPackages(image2) 49 | if err != nil { 50 | return &util.MultiVersionPackageDiffResult{}, err 51 | } 52 | 53 | diff := util.GetMultiVersionMapDiff(pack1, pack2) 54 | return &util.MultiVersionPackageDiffResult{ 55 | Image1: image1.Source, 56 | Image2: image2.Source, 57 | DiffType: strings.TrimSuffix(differ.Name(), "Analyzer"), 58 | Diff: diff, 59 | }, nil 60 | } 61 | 62 | func singleVersionDiff(image1, image2 pkgutil.Image, differ SingleVersionPackageAnalyzer) (*util.SingleVersionPackageDiffResult, error) { 63 | pack1, err := differ.getPackages(image1) 64 | if err != nil { 65 | return &util.SingleVersionPackageDiffResult{}, err 66 | } 67 | pack2, err := differ.getPackages(image2) 68 | if err != nil { 69 | return &util.SingleVersionPackageDiffResult{}, err 70 | } 71 | 72 | diff := util.GetMapDiff(pack1, pack2) 73 | return &util.SingleVersionPackageDiffResult{ 74 | Image1: image1.Source, 75 | Image2: image2.Source, 76 | DiffType: strings.TrimSuffix(differ.Name(), "Analyzer"), 77 | Diff: diff, 78 | }, nil 79 | } 80 | 81 | // singleVersionLayerDiff returns an error as this diff is not supported as 82 | // it is far from obvious to define it in meaningful way 83 | func singleVersionLayerDiff(image1, image2 pkgutil.Image, differ SingleVersionPackageLayerAnalyzer) (*util.SingleVersionPackageLayerDiffResult, error) { 84 | logrus.Warning("'diff' command for packages on layers is not supported, consider using 'analyze' on each image instead") 85 | return &util.SingleVersionPackageLayerDiffResult{}, errors.New("Diff for packages on layers is not supported, only analysis is supported") 86 | } 87 | 88 | func multiVersionAnalysis(image pkgutil.Image, analyzer MultiVersionPackageAnalyzer) (*util.MultiVersionPackageAnalyzeResult, error) { 89 | pack, err := analyzer.getPackages(image) 90 | if err != nil { 91 | return &util.MultiVersionPackageAnalyzeResult{}, err 92 | } 93 | 94 | analysis := util.MultiVersionPackageAnalyzeResult{ 95 | Image: image.Source, 96 | AnalyzeType: strings.TrimSuffix(analyzer.Name(), "Analyzer"), 97 | Analysis: pack, 98 | } 99 | return &analysis, nil 100 | } 101 | 102 | func singleVersionAnalysis(image pkgutil.Image, analyzer SingleVersionPackageAnalyzer) (*util.SingleVersionPackageAnalyzeResult, error) { 103 | pack, err := analyzer.getPackages(image) 104 | if err != nil { 105 | return &util.SingleVersionPackageAnalyzeResult{}, err 106 | } 107 | 108 | analysis := util.SingleVersionPackageAnalyzeResult{ 109 | Image: image.Source, 110 | AnalyzeType: strings.TrimSuffix(analyzer.Name(), "Analyzer"), 111 | Analysis: pack, 112 | } 113 | return &analysis, nil 114 | } 115 | 116 | // singleVersionLayerAnalysis returns the packages included, deleted or 117 | // updated in each layer 118 | func singleVersionLayerAnalysis(image pkgutil.Image, analyzer SingleVersionPackageLayerAnalyzer) (*util.SingleVersionPackageLayerAnalyzeResult, error) { 119 | pack, err := analyzer.getPackages(image) 120 | if err != nil { 121 | return &util.SingleVersionPackageLayerAnalyzeResult{}, err 122 | } 123 | var pkgDiffs []util.PackageDiff 124 | 125 | // Each layer with modified packages includes a complete list of packages 126 | // in its package database. Thus we diff the current layer with the 127 | // previous one that contains a package database. Layers that do not 128 | // include a package database are omitted. 129 | preInd := -1 130 | for i := range pack { 131 | var pkgDiff util.PackageDiff 132 | if preInd < 0 && len(pack[i]) > 0 { 133 | pkgDiff = util.GetMapDiff(make(map[string]util.PackageInfo), pack[i]) 134 | preInd = i 135 | } else if preInd >= 0 && len(pack[i]) > 0 { 136 | pkgDiff = util.GetMapDiff(pack[preInd], pack[i]) 137 | preInd = i 138 | } 139 | 140 | pkgDiffs = append(pkgDiffs, pkgDiff) 141 | } 142 | 143 | return &util.SingleVersionPackageLayerAnalyzeResult{ 144 | Image: image.Source, 145 | AnalyzeType: strings.TrimSuffix(analyzer.Name(), "Analyzer"), 146 | Analysis: util.PackageLayerDiff{ 147 | PackageDiffs: pkgDiffs, 148 | }, 149 | }, nil 150 | } 151 | -------------------------------------------------------------------------------- /differs/rpm_diff_test.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 differs 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | // TestLockUnlock runs some lock-unlock cycles to make sure that close, 24 | // subsequent locking and unlocking doesn't cause any issues. 25 | func TestLockUnlock(t *testing.T) { 26 | for i := 0; i < 20; i++ { 27 | if err := lock(); err != nil { 28 | t.Errorf("[RPMDiff test] lock() %d failed: %s", i, err) 29 | } 30 | if err := unlock(); err != nil { 31 | t.Errorf("[RPMDiff test] unlock() %d failed: %s", i, err) 32 | } 33 | } 34 | } 35 | 36 | // TestGoRoutineRace creates a race condition among goroutines. Race conditions 37 | // among processes cannot be tested here as we can't fork. 38 | func TestGoRoutineRace(t *testing.T) { 39 | wait := make(chan int) 40 | couldLock := false 41 | if err := lock(); err != nil { 42 | t.Errorf("[RPMDiff test] lock() failed: %s", err) 43 | } 44 | 45 | go func() { 46 | wait <- 1 47 | if err := lock(); err != nil { 48 | t.Errorf("[RPMDiff test] lock() failed: %s", err) 49 | } 50 | couldLock = true 51 | if err := unlock(); err != nil { 52 | t.Errorf("[RPMDiff test] unlock() failed: %s", err) 53 | } 54 | wait <- 1 55 | }() 56 | 57 | <-wait 58 | if couldLock { 59 | t.Errorf("Other goroutine locked although lock wasn't released") 60 | } 61 | if err := unlock(); err != nil { 62 | t.Errorf("[RPMDiff test] unlock() failed: %s", err) 63 | } 64 | <-wait 65 | if !couldLock { 66 | t.Errorf("Other goroutine didn't lock although lock was released") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /differs/size_diff.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 differs 18 | 19 | import ( 20 | "strconv" 21 | 22 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 23 | "github.com/GoogleContainerTools/container-diff/util" 24 | ) 25 | 26 | type SizeAnalyzer struct { 27 | } 28 | 29 | func (a SizeAnalyzer) Name() string { 30 | return "SizeAnalyzer" 31 | } 32 | 33 | // SizeDiff diffs two images and compares their size 34 | func (a SizeAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { 35 | diff := []util.SizeDiff{} 36 | size1 := pkgutil.GetSize(image1.FSPath) 37 | size2 := pkgutil.GetSize(image2.FSPath) 38 | 39 | if size1 != size2 { 40 | diff = append(diff, util.SizeDiff{ 41 | Size1: size1, 42 | Size2: size2, 43 | }) 44 | } 45 | 46 | return &util.SizeDiffResult{ 47 | Image1: image1.Source, 48 | Image2: image2.Source, 49 | DiffType: "Size", 50 | Diff: diff, 51 | }, nil 52 | } 53 | 54 | func (a SizeAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { 55 | entries := []util.SizeEntry{ 56 | { 57 | Name: image.Source, 58 | Digest: image.Digest, 59 | Size: pkgutil.GetSize(image.FSPath), 60 | }, 61 | } 62 | 63 | return &util.SizeAnalyzeResult{ 64 | Image: image.Source, 65 | AnalyzeType: "Size", 66 | Analysis: entries, 67 | }, nil 68 | } 69 | 70 | type SizeLayerAnalyzer struct { 71 | } 72 | 73 | func (a SizeLayerAnalyzer) Name() string { 74 | return "SizeLayerAnalyzer" 75 | } 76 | 77 | // SizeLayerDiff diffs the layers of two images and compares their size 78 | func (a SizeLayerAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { 79 | var layerDiffs []util.SizeDiff 80 | 81 | maxLayer := len(image1.Layers) 82 | if len(image2.Layers) > maxLayer { 83 | maxLayer = len(image2.Layers) 84 | } 85 | 86 | for index := 0; index < maxLayer; index++ { 87 | var size1, size2 int64 = -1, -1 88 | if index < len(image1.Layers) { 89 | size1 = pkgutil.GetSize(image1.Layers[index].FSPath) 90 | } 91 | if index < len(image2.Layers) { 92 | size2 = pkgutil.GetSize(image2.Layers[index].FSPath) 93 | } 94 | 95 | if size1 != size2 { 96 | diff := util.SizeDiff{ 97 | Name: strconv.Itoa(index), 98 | Size1: size1, 99 | Size2: size2, 100 | } 101 | layerDiffs = append(layerDiffs, diff) 102 | } 103 | } 104 | 105 | return &util.SizeLayerDiffResult{ 106 | Image1: image1.Source, 107 | Image2: image2.Source, 108 | DiffType: "SizeLayer", 109 | Diff: layerDiffs, 110 | }, nil 111 | } 112 | 113 | func (a SizeLayerAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { 114 | var entries []util.SizeEntry 115 | for index, layer := range image.Layers { 116 | entry := util.SizeEntry{ 117 | Name: strconv.Itoa(index), 118 | Digest: layer.Digest, 119 | Size: pkgutil.GetSize(layer.FSPath), 120 | } 121 | entries = append(entries, entry) 122 | } 123 | 124 | return &util.SizeLayerAnalyzeResult{ 125 | Image: image.Source, 126 | AnalyzeType: "SizeLayer", 127 | Analysis: entries, 128 | }, nil 129 | } 130 | -------------------------------------------------------------------------------- /differs/testDirs/exact.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "La-croix", 3 | "version": "Lime" 4 | } -------------------------------------------------------------------------------- /differs/testDirs/extra.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "sax", 3 | "_id": "sax@1.2.4", 4 | "_inBundle": false, 5 | "_integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", 6 | "_location": "/sax", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "tag", 10 | "registry": true, 11 | "raw": "sax", 12 | "name": "sax", 13 | "escapedName": "sax", 14 | "rawSpec": "", 15 | "saveSpec": null, 16 | "fetchSpec": "latest" 17 | }, 18 | "_requiredBy": [ 19 | "#USER", 20 | "/" 21 | ], 22 | "_resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 23 | "_shasum": "2816234e2378bddc4e5354fab5caa895df7100d9", 24 | "_spec": "sax", 25 | "_where": "/", 26 | "author": { 27 | "name": "Isaac Z. Schlueter", 28 | "email": "i@izs.me", 29 | "url": "http://blog.izs.me/" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/isaacs/sax-js/issues" 33 | }, 34 | "bundleDependencies": false, 35 | "deprecated": false, 36 | "description": "An evented streaming XML parser in JavaScript", 37 | "devDependencies": { 38 | "standard": "^8.6.0", 39 | "tap": "^10.5.1" 40 | }, 41 | "files": [ 42 | "lib/sax.js", 43 | "LICENSE", 44 | "README.md" 45 | ], 46 | "homepage": "https://github.com/isaacs/sax-js#readme", 47 | "license": "ISC", 48 | "main": "lib/sax.js", 49 | "name": "La-croix", 50 | "repository": { 51 | "type": "git", 52 | "url": "git://github.com/isaacs/sax-js.git" 53 | }, 54 | "scripts": { 55 | "postpublish": "git push origin --all; git push origin --tags", 56 | "posttest": "standard -F test/*.js lib/*.js", 57 | "postversion": "npm publish", 58 | "preversion": "npm test", 59 | "test": "tap test/*.js --cov -j4" 60 | }, 61 | "version": "Lime" 62 | } -------------------------------------------------------------------------------- /differs/testDirs/noPackages/not_a_package/file: -------------------------------------------------------------------------------- 1 | non-empty 2 | -------------------------------------------------------------------------------- /differs/testDirs/noPackages/var/db/pkg/dir/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/noPackages/var/db/pkg/dir/file -------------------------------------------------------------------------------- /differs/testDirs/packageEmerge/var/db/pkg/dev-python/pkg1-0.0.1/SIZE: -------------------------------------------------------------------------------- 1 | 167112 -------------------------------------------------------------------------------- /differs/testDirs/packageEmerge/var/db/pkg/dev-python/pkg2-0.0.2/SIZE: -------------------------------------------------------------------------------- 1 | 167112 -------------------------------------------------------------------------------- /differs/testDirs/packageEmerge/var/db/pkg/sys-libs/pkg3-0.0.3/SIZE: -------------------------------------------------------------------------------- 1 | 167112 -------------------------------------------------------------------------------- /differs/testDirs/packageMulti/node_modules/pac1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pac1", 3 | "version": "1.0" 4 | } 5 | -------------------------------------------------------------------------------- /differs/testDirs/packageMulti/node_modules/pac2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pac2", 3 | "version": "2.0" 4 | } 5 | -------------------------------------------------------------------------------- /differs/testDirs/packageMulti/usr/local/lib/node_modules/pac2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pac2", 3 | "version": "3.0" 4 | } 5 | -------------------------------------------------------------------------------- /differs/testDirs/packageMulti/var/lib/dpkg/status: -------------------------------------------------------------------------------- 1 | Package: pac1 2 | info: other stuff 3 | Version: 1.0 4 | 5 | Package: pac2 6 | info: more other stuff 7 | Version: 2.0 8 | 9 | Package: pac3 10 | Version: 3.0 11 | info: again other stuff 12 | -------------------------------------------------------------------------------- /differs/testDirs/packageOne/node_modules/pac1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pac1", 3 | "version": "1.0" 4 | } 5 | -------------------------------------------------------------------------------- /differs/testDirs/packageOne/node_modules/pac3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pac3", 3 | "version": "3.0" 4 | } 5 | -------------------------------------------------------------------------------- /differs/testDirs/packageOne/usr/local/lib/node_modules/pac2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pac2", 3 | "version": "2.0" 4 | } 5 | -------------------------------------------------------------------------------- /differs/testDirs/packageOne/var/lib/dpkg/status: -------------------------------------------------------------------------------- 1 | Package: pac1 2 | info: other stuff 3 | Version: 1.0 4 | 5 | Package: pac2 6 | info: more other stuff 7 | Version: 2.0 8 | 9 | Package: pac3 10 | Version: 3.0 11 | info: again other stuff 12 | -------------------------------------------------------------------------------- /differs/testDirs/pipTests/noPackagesTest/noPackagesInstalled/layer/usr/local/lib/python3.6/site-packages/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/noPackagesTest/noPackagesInstalled/layer/usr/local/lib/python3.6/site-packages/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/noPackagesTest/noSitePackagesFolder/layer/usr/local/lib/python3.6/notSitePackages/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/noPackagesTest/noSitePackagesFolder/layer/usr/local/lib/python3.6/notSitePackages/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python2.7/site-packages/packageone-0.1.1.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python2.7/site-packages/packageone-0.1.1.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python2.7/site-packages/packageone/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python2.7/site-packages/packageone/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python2.7/site-packages/script3-3.0.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python2.7/site-packages/script3-3.0.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python2.7/site-packages/script3.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python2.7/site-packages/script3.py -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packageone-3.6.9.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packageone-3.6.9.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packageone/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packageone/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packagetwo-4.6.2.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packagetwo-4.6.2.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packagetwo/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packagetwo/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script1-1.0.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script1-1.0.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script1.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script1.py -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script2-2.0.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script2-2.0.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script2.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script2.py -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/packageone-3.6.9.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/packageone-3.6.9.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/packageone/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/packageone/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/packagetwo-4.6.2.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/packagetwo-4.6.2.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/packagetwo/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/packagetwo/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/script1-1.0.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/script1-1.0.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/script1.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/script1.py -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/script2-2.0.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/script2-2.0.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/script2.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/packagesSingleVersion/usr/local/lib/python3.6/site-packages/script2.py -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/pythonPath1/packageseven-4.6.2.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/pythonPath1/packageseven-4.6.2.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/pythonPath1/packageseven/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/pythonPath1/packageseven/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/pythonPath1/packagesix-3.6.9.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/pythonPath1/packagesix-3.6.9.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/pythonPath1/packagesix/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/pythonPath1/packagesix/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/pythonPath1/script1.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/pythonPath1/script1.py -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/pythonPath1/script2.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/pythonPath1/script2.py -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/pythonPath2/subdir/packagefive-3.6.9.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/pythonPath2/subdir/packagefive-3.6.9.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/pythonPath2/subdir/packagefive/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/pythonPath2/subdir/packagefive/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/usr/local/lib/python3.6/site-packages/packageone-3.6.9.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/usr/local/lib/python3.6/site-packages/packageone-3.6.9.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/usr/local/lib/python3.6/site-packages/packageone/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/usr/local/lib/python3.6/site-packages/packageone/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/usr/local/lib/python3.6/site-packages/packagetwo-4.6.2.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/usr/local/lib/python3.6/site-packages/packagetwo-4.6.2.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonPathTests/usr/local/lib/python3.6/site-packages/packagetwo/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonPathTests/usr/local/lib/python3.6/site-packages/packagetwo/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonVersionTests/2VersionLayer/usr/local/lib/python2.7/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonVersionTests/2VersionLayer/usr/local/lib/python2.7/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonVersionTests/2VersionLayer/usr/local/lib/python3.6/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonVersionTests/2VersionLayer/usr/local/lib/python3.6/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonVersionTests/noLibLayer/usr/local/notLib/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonVersionTests/noLibLayer/usr/local/notLib/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonVersionTests/noPythonLayer/usr/local/lib/notPython/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonVersionTests/noPythonLayer/usr/local/lib/notPython/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonVersionTests/version2.7Layer/usr/local/lib/python2.7/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonVersionTests/version2.7Layer/usr/local/lib/python2.7/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests/pythonVersionTests/version3.6Layer/usr/local/lib/python3.6/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests/pythonVersionTests/version3.6Layer/usr/local/lib/python3.6/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/noPackagesTest/noPackagesInstalled/usr/local/lib/python3.6/site-packages/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/noPackagesTest/noPackagesInstalled/usr/local/lib/python3.6/site-packages/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/noPackagesTest/noSitePackagesFolder/usr/local/lib/python3.6/notSitePackages/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/noPackagesTest/noSitePackagesFolder/usr/local/lib/python3.6/notSitePackages/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python2.7/site-packages/packageone-0.1.1.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python2.7/site-packages/packageone-0.1.1.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python2.7/site-packages/packageone/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python2.7/site-packages/packageone/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python2.7/site-packages/script3-3.0.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python2.7/site-packages/script3-3.0.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python2.7/site-packages/script3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2018 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 | -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packageone-3.6.9.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packageone-3.6.9.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packageone/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packageone/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packagetwo-4.6.2.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packagetwo-4.6.2.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packagetwo/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/packagetwo/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script1-1.0.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script1-1.0.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2018 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 | -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script2-2.0.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script2-2.0.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesMultiVersion/usr/local/lib/python3.6/site-packages/script2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2018 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 | -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/packageone-3.6.9.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/packageone-3.6.9.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/packageone/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/packageone/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/packagetwo-4.6.2.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/packagetwo-4.6.2.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/packagetwo/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/packagetwo/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/script1-1.0.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/script1-1.0.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/script1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2018 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 | -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/script2-2.0.dist-info/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/script2-2.0.dist-info/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/packagesOneLayer/usr/local/lib/python3.6/site-packages/script2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2018 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 | -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/pythonVersionTests/2VersionLayer/usr/local/lib/python2.7/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/pythonVersionTests/2VersionLayer/usr/local/lib/python2.7/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/pythonVersionTests/2VersionLayer/usr/local/lib/python3.6/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/pythonVersionTests/2VersionLayer/usr/local/lib/python3.6/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/pythonVersionTests/noLibLayer/usr/local/notLib/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/pythonVersionTests/noLibLayer/usr/local/notLib/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/pythonVersionTests/noPythonLayer/usr/local/lib/notPython/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/pythonVersionTests/noPythonLayer/usr/local/lib/notPython/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/pythonVersionTests/version2.7Layer/usr/local/lib/python2.7/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/pythonVersionTests/version2.7Layer/usr/local/lib/python2.7/file -------------------------------------------------------------------------------- /differs/testDirs/pipTests2/pythonVersionTests/version3.6Layer/usr/local/lib/python3.6/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/differs/testDirs/pipTests2/pythonVersionTests/version3.6Layer/usr/local/lib/python3.6/file -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleContainerTools/container-diff 2 | 3 | go 1.21 4 | 5 | require ( 6 | code.cloudfoundry.org/bytefmt v0.0.0-20231017140541-3b893ed0421b 7 | github.com/docker/docker v25.0.3+incompatible 8 | github.com/fsouza/go-dockerclient v1.10.2 9 | github.com/google/go-containerregistry v0.19.0 10 | github.com/google/go-github v17.0.0+incompatible 11 | github.com/mitchellh/go-homedir v1.1.0 12 | github.com/moby/sys/sequential v0.5.0 13 | github.com/nightlyone/lockfile v1.0.0 14 | github.com/pkg/errors v0.9.1 15 | github.com/pkg/profile v1.7.0 16 | github.com/pmezard/go-difflib v1.0.0 17 | github.com/sirupsen/logrus v1.9.3 18 | github.com/spf13/cobra v1.8.0 19 | github.com/spf13/pflag v1.0.5 20 | golang.org/x/oauth2 v0.10.0 21 | ) 22 | 23 | require ( 24 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 25 | github.com/Microsoft/go-winio v0.6.1 // indirect 26 | github.com/containerd/containerd v1.7.13 // indirect 27 | github.com/containerd/log v0.1.0 // indirect 28 | github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect 29 | github.com/distribution/reference v0.5.0 // indirect 30 | github.com/docker/cli v25.0.3+incompatible // indirect 31 | github.com/docker/distribution v2.8.3+incompatible // indirect 32 | github.com/docker/docker-credential-helpers v0.8.1 // indirect 33 | github.com/docker/go-connections v0.5.0 // indirect 34 | github.com/docker/go-units v0.5.0 // indirect 35 | github.com/felixge/fgprof v0.9.3 // indirect 36 | github.com/felixge/httpsnoop v1.0.4 // indirect 37 | github.com/go-logr/logr v1.4.1 // indirect 38 | github.com/go-logr/stdr v1.2.2 // indirect 39 | github.com/gogo/protobuf v1.3.2 // indirect 40 | github.com/golang/protobuf v1.5.3 // indirect 41 | github.com/google/go-querystring v1.0.0 // indirect 42 | github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect 43 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 44 | github.com/klauspost/compress v1.17.7 // indirect 45 | github.com/moby/patternmatcher v0.6.0 // indirect 46 | github.com/moby/sys/user v0.1.0 // indirect 47 | github.com/moby/term v0.5.0 // indirect 48 | github.com/morikuni/aec v1.0.0 // indirect 49 | github.com/opencontainers/go-digest v1.0.0 // indirect 50 | github.com/opencontainers/image-spec v1.1.0 // indirect 51 | github.com/vbatts/tar-split v0.11.5 // indirect 52 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 53 | go.opentelemetry.io/otel v1.24.0 // indirect 54 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 55 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 56 | golang.org/x/mod v0.15.0 // indirect 57 | golang.org/x/net v0.21.0 // indirect 58 | golang.org/x/sync v0.6.0 // indirect 59 | golang.org/x/sys v0.17.0 // indirect 60 | golang.org/x/tools v0.18.0 // indirect 61 | google.golang.org/appengine v1.6.7 // indirect 62 | google.golang.org/protobuf v1.31.0 // indirect 63 | ) 64 | -------------------------------------------------------------------------------- /hack/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | make test integration 3 | -------------------------------------------------------------------------------- /hack/release.sh: -------------------------------------------------------------------------------- 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 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 16 | 17 | # you can pass your github token with --token here if you run out of requests 18 | go run ${DIR}/release_notes/listpullreqs.go 19 | 20 | echo "Huge thank you for this release towards our contributors: " 21 | git log "$(git describe --abbrev=0)".. --format="%aN" --reverse | sort | uniq | awk '{printf "- %s\n", $0 }' 22 | -------------------------------------------------------------------------------- /hack/release_notes/listpullreqs.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 main 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/google/go-github/github" 24 | "github.com/sirupsen/logrus" 25 | "github.com/spf13/cobra" 26 | "golang.org/x/oauth2" 27 | ) 28 | 29 | var ( 30 | token string 31 | fromTag string 32 | toTag string 33 | ) 34 | 35 | var rootCmd = &cobra.Command{ 36 | Use: "listpullreqs fromTag toTag", 37 | Short: "Lists pull requests between two versions in our changelog markdown format", 38 | ArgAliases: []string{"fromTag", "toTag"}, 39 | Run: func(cmd *cobra.Command, args []string) { 40 | printPullRequests() 41 | }, 42 | } 43 | 44 | const org = "GoogleContainerTools" 45 | const repo = "container-diff" 46 | 47 | func main() { 48 | rootCmd.Flags().StringVar(&token, "token", "", "Specify personal Github Token if you are hitting a rate limit anonymously. https://github.com/settings/tokens") 49 | rootCmd.Flags().StringVar(&fromTag, "fromTag", "", "comparison of commits is based on this tag (defaults to the latest tag in the repo)") 50 | rootCmd.Flags().StringVar(&toTag, "toTag", "master", "this is the commit that is compared with fromTag") 51 | if err := rootCmd.Execute(); err != nil { 52 | logrus.Fatal(err) 53 | } 54 | } 55 | 56 | func printPullRequests() { 57 | client := getClient() 58 | 59 | releases, _, _ := client.Repositories.ListReleases(context.Background(), org, repo, &github.ListOptions{}) 60 | lastReleaseTime := *releases[0].PublishedAt 61 | fmt.Println(fmt.Sprintf("Collecting pull request that were merged since the last release: %s (%s)", *releases[0].TagName, lastReleaseTime)) 62 | 63 | listSize := 1 64 | for page := 0; listSize > 0; page++ { 65 | pullRequests, _, _ := client.PullRequests.List(context.Background(), org, repo, &github.PullRequestListOptions{ 66 | State: "closed", 67 | Sort: "updated", 68 | Direction: "desc", 69 | ListOptions: github.ListOptions{ 70 | PerPage: 100, 71 | Page: page, 72 | }, 73 | }) 74 | 75 | for idx := range pullRequests { 76 | pr := pullRequests[idx] 77 | if pr.MergedAt != nil { 78 | if pr.GetMergedAt().After(lastReleaseTime.Time) { 79 | fmt.Printf("* %s [#%d](https://github.com/%s/%s/pull/%d)\n", pr.GetTitle(), *pr.Number, org, repo, *pr.Number) 80 | } 81 | } 82 | } 83 | 84 | listSize = len(pullRequests) 85 | } 86 | } 87 | 88 | func getClient() *github.Client { 89 | if len(token) <= 0 { 90 | return github.NewClient(nil) 91 | } 92 | ctx := context.Background() 93 | ts := oauth2.StaticTokenSource( 94 | &oauth2.Token{AccessToken: token}, 95 | ) 96 | tc := oauth2.NewClient(ctx, ts) 97 | return github.NewClient(tc) 98 | } 99 | -------------------------------------------------------------------------------- /main.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 main 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | "github.com/GoogleContainerTools/container-diff/cmd" 24 | "github.com/pkg/profile" 25 | ) 26 | 27 | const containerDiffEnvPrefix = "CONTAINER_DIFF_ENABLE_PROFILING" 28 | 29 | func main() { 30 | if os.Getenv(containerDiffEnvPrefix) == "1" { 31 | defer profile.Start(profile.TraceProfile).Stop() 32 | } 33 | if err := cmd.RootCmd.Execute(); err != nil { 34 | fmt.Println(err) 35 | os.Exit(1) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/util/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 util 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 | -------------------------------------------------------------------------------- /pkg/util/test_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 util 18 | 19 | import ( 20 | "github.com/google/go-containerregistry/pkg/v1" 21 | "github.com/google/go-containerregistry/pkg/v1/partial" 22 | "github.com/google/go-containerregistry/pkg/v1/types" 23 | ) 24 | 25 | type TestImage struct { 26 | Config *v1.ConfigFile 27 | } 28 | 29 | func (i *TestImage) RawConfigFile() ([]byte, error) { 30 | return partial.RawConfigFile(i) 31 | } 32 | 33 | func (i *TestImage) ConfigFile() (*v1.ConfigFile, error) { 34 | return i.Config, nil 35 | } 36 | 37 | func (i *TestImage) MediaType() (types.MediaType, error) { 38 | return types.DockerManifestSchema2, nil 39 | } 40 | 41 | func (i *TestImage) LayerByDiffID(diffID v1.Hash) (v1.Layer, error) { 42 | return nil, nil 43 | } 44 | 45 | func (i *TestImage) BlobSet() (map[v1.Hash]struct{}, error) { 46 | return nil, nil 47 | } 48 | 49 | func (i *TestImage) ConfigName() (v1.Hash, error) { 50 | return v1.Hash{}, nil 51 | } 52 | 53 | func (i *TestImage) Digest() (v1.Hash, error) { 54 | return v1.Hash{}, nil 55 | } 56 | 57 | func (i *TestImage) Manifest() (*v1.Manifest, error) { 58 | return nil, nil 59 | } 60 | 61 | func (i *TestImage) RawManifest() ([]byte, error) { 62 | return nil, nil 63 | } 64 | 65 | func (i *TestImage) LayerByDigest(v1.Hash) (v1.Layer, error) { 66 | return nil, nil 67 | } 68 | 69 | func (i *TestImage) Layers() ([]v1.Layer, error) { 70 | return nil, nil 71 | } 72 | 73 | func (i *TestImage) Size() (int64, error) { 74 | return 0, nil 75 | } 76 | -------------------------------------------------------------------------------- /pkg/util/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 util 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 | -------------------------------------------------------------------------------- /setup-tests/Dockerfile.diffBase: -------------------------------------------------------------------------------- 1 | FROM gcr.io/gcp-runtimes/centos7:latest 2 | 3 | # docker build . -f Dockerfile.diffBase -t gcr.io/gcp-runtimes/container-diff-tests/diff-base:latest 4 | 5 | RUN yum -q -y install which 6 | RUN mkdir /first && echo "First" > /first/first.txt 7 | RUN mkdir /second && echo "Second" > /second/second.txt 8 | RUN mkdir /third && echo "Third" > /third/third.txt 9 | -------------------------------------------------------------------------------- /setup-tests/Dockerfile.diffLayerBase: -------------------------------------------------------------------------------- 1 | FROM gcr.io/gcp-runtimes/ubuntu_16_0_4:latest 2 | 3 | # docker build . -f Dockerfile.diffLayerBase -t gcr.io/gcp-runtimes/container-diff-tests/diff-layer-base:latest 4 | 5 | 6 | RUN mkdir /first && echo "First" > /first/first.txt 7 | RUN mkdir /second && echo "Second" > /second/second.txt 8 | RUN mkdir /third && echo "Third" > /third/third.txt 9 | -------------------------------------------------------------------------------- /setup-tests/Dockerfile.diffLayerModified: -------------------------------------------------------------------------------- 1 | FROM gcr.io/gcp-runtimes/ubuntu_16_0_4:latest 2 | 3 | # docker build . -f Dockerfile.diffLayerModified -t gcr.io/gcp-runtimes/container-diff-tests/diff-layer-modified:latest 4 | 5 | RUN mkdir /first && echo "First" > /first/first.txt 6 | RUN echo "No Second Layer" 7 | RUN mkdir /modified && echo "Modified" > /modified/modified.txt 8 | -------------------------------------------------------------------------------- /setup-tests/Dockerfile.diffModified: -------------------------------------------------------------------------------- 1 | FROM gcr.io/gcp-runtimes/container-diff-tests/diff-base:latest 2 | 3 | # docker build . -f Dockerfile.diffModified -t gcr.io/gcp-runtimes/container-diff-tests/diff-modified:latest 4 | 5 | RUN rm -rf /second && mkdir /modified && echo "Modified" > /modified/modified.txt 6 | RUN yum -q -y install gcc 7 | -------------------------------------------------------------------------------- /setup-tests/README.md: -------------------------------------------------------------------------------- 1 | These scripts and Dockerfiles are intended to help regenerate and push the 2 | container files required by the integration tests, and then regenerate the 3 | expected output files. 4 | 5 | To run: 6 | cd setup-tests 7 | ./init-tests.sh 8 | -------------------------------------------------------------------------------- /setup-tests/init-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set +x 4 | # Grab the container definitions from the integration test 5 | diffBase=$(cat ../tests/integration_test.go | grep diffBase | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 6 | diffModified=$(cat ../tests/integration_test.go | grep diffModified | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 7 | diffLayerBase=$(cat ../tests/integration_test.go | grep diffLayerBase | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 8 | diffLayerModified=$(cat ../tests/integration_test.go | grep diffLayerModified | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 9 | rpmBase=$(cat ../tests/integration_test.go | grep rpmBase | grep valentinrothberg | sed -e 's/.* = //g' | sed -e 's/"//g') 10 | rpmModified=$(cat ../tests/integration_test.go | grep rpmModified | grep valentinrothberg | sed -e 's/.* = //g' | sed -e 's/"//g') 11 | aptBase=$(cat ../tests/integration_test.go | grep aptBase | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 12 | aptModified=$(cat ../tests/integration_test.go | grep aptModified | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 13 | nodeBase=$(cat ../tests/integration_test.go | grep nodeBase | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 14 | nodeModified=$(cat ../tests/integration_test.go | grep nodeModified | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 15 | multiBase=$(cat ../tests/integration_test.go | grep -w multiBase | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 16 | multiModified=$(cat ../tests/integration_test.go | grep -w multiModified | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 17 | metadataBase=$(cat ../tests/integration_test.go | grep metadataBase | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 18 | metadataModified=$(cat ../tests/integration_test.go | grep metadataModified | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 19 | pipModified=$(cat ../tests/integration_test.go | grep pipModified | grep gcr.io | sed -e 's/.* = //g' | sed -e 's/"//g') 20 | 21 | # Echo the container definitions. 22 | echo diffBase=$diffBase 23 | echo diffModified=$diffModified 24 | echo diffLayerBase=$diffLayerBase 25 | echo diffLayerModified=$diffLayerModified 26 | echo rpmBase=$rpmBase 27 | echo rpmModified=$rpmModified 28 | echo aptBase=$aptBase 29 | echo aptModified=$aptModified 30 | echo nodeBase=$nodeBase 31 | echo nodeModified=$nodeModified 32 | echo multiBase=$multiBase 33 | echo multiModified=$multiModified 34 | echo metadataBase=$metadataBase 35 | echo metadataModified=$metadataModified 36 | echo pipModified=$pipModified 37 | 38 | # Now generate the containers 39 | # XXX TO-DO for now we're only generated diffBase diffModified diffLayerBase and 40 | # diffLayerModified, eventually we should generate all of them 41 | # 42 | 43 | echo 'docker build . -f Dockerfile.diffBase -t $diffBase:latest' 44 | docker build . -f Dockerfile.diffBase -t $diffBase:latest 45 | docker push $diffBase:latest 46 | echo 'docker build . -f Dockerfile.diffLayerBase -t $diffLayerBase:latest' 47 | docker build . -f Dockerfile.diffLayerBase -t $diffLayerBase:latest 48 | docker push $diffLayerBase:latest 49 | echo 'docker build . -f Dockerfile.diffLayerModified -t $diffLayerModified:latest' 50 | docker build . -f Dockerfile.diffLayerModified -t $diffLayerModified:latest 51 | docker push $diffLayerModified:latest 52 | echo 'docker build . -f Dockerfile.diffModified -t $diffModified:latest' 53 | docker build . -f Dockerfile.diffModified -t $diffModified:latest 54 | docker push $diffModified:latest 55 | 56 | #Now generate expected outputs. Do NOT commit these without reviewing them for reasonableness 57 | container-diff diff --no-cache -j --type=file $diffBase $diffModified > ../tests/file_diff_expected.json 58 | container-diff diff --no-cache -j --type=layer $diffLayerBase $diffLayerModified > ../tests/file_layer_diff_expected.json 59 | container-diff diff --no-cache -j --type=size $diffLayerBase $diffLayerModified > ../tests/size_diff_expected.json 60 | container-diff diff --no-cache -j --type=sizelayer $diffLayerBase $diffLayerModified > ../tests/size_layer_diff_expected.json 61 | container-diff diff --no-cache -j --type=apt $aptBase $aptModified > ../tests/apt_diff_expected.json 62 | container-diff diff --no-cache -j --type=node $nodeBase $nodeModified > ../tests/node_diff_order_expected.json 63 | container-diff diff --no-cache -j --type=node --type=pip --type=apt $multiBase $multiModified > ../tests/multi_diff_expected.json 64 | container-diff diff --no-cache -j --type=history $diffBase $diffModified > ../tests/hist_diff_expected.json 65 | container-diff diff --no-cache -j --type=metadata $metadataBase $metadataModified > ../tests/metadata_diff_expected.json 66 | container-diff diff --no-cache -j --type=apt -o $aptBase $aptModified > ../tests/apt_sorted_diff_expected.json 67 | container-diff analyze --no-cache -j --type=apt $aptModified > ../tests/apt_analysis_expected.json 68 | container-diff analyze --no-cache -j --type=file -o $diffModified > ../tests/file_sorted_analysis_expected.json 69 | container-diff analyze --no-cache -j --type=layer $diffLayerBase > ../tests/file_layer_analysis_expected.json 70 | container-diff analyze --no-cache -j --type=size $diffBase > ../tests/size_analysis_expected.json 71 | container-diff analyze --no-cache -j --type=sizelayer $diffLayerBase > ../tests/size_layer_analysis_expected.json 72 | container-diff analyze --no-cache -j --type=pip $pipModified > ../tests/pip_analysis_expected.json 73 | container-diff analyze --no-cache -j --type=node $nodeModified > ../tests/node_analysis_expected.json 74 | 75 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 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 | echo "Running go tests..." 20 | go test -timeout 60s `go list ./... | grep -v vendor` 21 | 22 | echo "Checking gofmt..." 23 | files=$(find . -name "*.go" | grep -v vendor/ | xargs gofmt -l -s) 24 | if [[ $files ]]; then 25 | echo "Gofmt errors in files: $files" 26 | exit 1 27 | fi 28 | 29 | echo "Checking go-addlicense..." 30 | addlicense -check -l apache -ignore \{actions,differs,vendor,setup-tests,out\}/**/* -ignore cloudbuild\*yaml * 31 | -------------------------------------------------------------------------------- /tests/apt_diff_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/apt-base", 4 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/apt-modified", 5 | "DiffType": "Apt", 6 | "Diff": { 7 | "Packages1": [ 8 | { 9 | "Name": "dh-python", 10 | "Version": "2.20170125", 11 | "Size": 411648 12 | }, 13 | { 14 | "Name": "libmpdec2", 15 | "Version": "2.4.2-1", 16 | "Size": 260096 17 | }, 18 | { 19 | "Name": "libpython3-stdlib", 20 | "Version": "3.5.3-1", 21 | "Size": 36864 22 | }, 23 | { 24 | "Name": "libpython3.5-minimal", 25 | "Version": "3.5.3-1", 26 | "Size": 3836928 27 | }, 28 | { 29 | "Name": "libpython3.5-stdlib", 30 | "Version": "3.5.3-1", 31 | "Size": 10133504 32 | }, 33 | { 34 | "Name": "python3", 35 | "Version": "3.5.3-1", 36 | "Size": 68608 37 | }, 38 | { 39 | "Name": "python3-minimal", 40 | "Version": "3.5.3-1", 41 | "Size": 122880 42 | }, 43 | { 44 | "Name": "python3.5", 45 | "Version": "3.5.3-1", 46 | "Size": 326656 47 | }, 48 | { 49 | "Name": "python3.5-minimal", 50 | "Version": "3.5.3-1", 51 | "Size": 9636864 52 | } 53 | ], 54 | "Packages2": [ 55 | { 56 | "Name": "libffi6", 57 | "Version": "3.2.1-6", 58 | "Size": 57344 59 | }, 60 | { 61 | "Name": "libpython-stdlib", 62 | "Version": "2.7.13-2", 63 | "Size": 37888 64 | }, 65 | { 66 | "Name": "libpython2.7-minimal", 67 | "Version": "2.7.13-2", 68 | "Size": 2833408 69 | }, 70 | { 71 | "Name": "libpython2.7-stdlib", 72 | "Version": "2.7.13-2", 73 | "Size": 8755200 74 | }, 75 | { 76 | "Name": "python", 77 | "Version": "2.7.13-2", 78 | "Size": 663552 79 | }, 80 | { 81 | "Name": "python-minimal", 82 | "Version": "2.7.13-2", 83 | "Size": 148480 84 | }, 85 | { 86 | "Name": "python2.7", 87 | "Version": "2.7.13-2", 88 | "Size": 367616 89 | }, 90 | { 91 | "Name": "python2.7-minimal", 92 | "Version": "2.7.13-2", 93 | "Size": 3911680 94 | } 95 | ], 96 | "InfoDiff": [] 97 | } 98 | } 99 | ] -------------------------------------------------------------------------------- /tests/apt_sorted_diff_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/apt-base", 4 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/apt-modified", 5 | "DiffType": "Apt", 6 | "Diff": { 7 | "Packages1": [ 8 | { 9 | "Name": "libpython3.5-stdlib", 10 | "Version": "3.5.3-1", 11 | "Size": 10133504 12 | }, 13 | { 14 | "Name": "python3.5-minimal", 15 | "Version": "3.5.3-1", 16 | "Size": 9636864 17 | }, 18 | { 19 | "Name": "libpython3.5-minimal", 20 | "Version": "3.5.3-1", 21 | "Size": 3836928 22 | }, 23 | { 24 | "Name": "dh-python", 25 | "Version": "2.20170125", 26 | "Size": 411648 27 | }, 28 | { 29 | "Name": "python3.5", 30 | "Version": "3.5.3-1", 31 | "Size": 326656 32 | }, 33 | { 34 | "Name": "libmpdec2", 35 | "Version": "2.4.2-1", 36 | "Size": 260096 37 | }, 38 | { 39 | "Name": "python3-minimal", 40 | "Version": "3.5.3-1", 41 | "Size": 122880 42 | }, 43 | { 44 | "Name": "python3", 45 | "Version": "3.5.3-1", 46 | "Size": 68608 47 | }, 48 | { 49 | "Name": "libpython3-stdlib", 50 | "Version": "3.5.3-1", 51 | "Size": 36864 52 | } 53 | ], 54 | "Packages2": [ 55 | { 56 | "Name": "libpython2.7-stdlib", 57 | "Version": "2.7.13-2", 58 | "Size": 8755200 59 | }, 60 | { 61 | "Name": "python2.7-minimal", 62 | "Version": "2.7.13-2", 63 | "Size": 3911680 64 | }, 65 | { 66 | "Name": "libpython2.7-minimal", 67 | "Version": "2.7.13-2", 68 | "Size": 2833408 69 | }, 70 | { 71 | "Name": "python", 72 | "Version": "2.7.13-2", 73 | "Size": 663552 74 | }, 75 | { 76 | "Name": "python2.7", 77 | "Version": "2.7.13-2", 78 | "Size": 367616 79 | }, 80 | { 81 | "Name": "python-minimal", 82 | "Version": "2.7.13-2", 83 | "Size": 148480 84 | }, 85 | { 86 | "Name": "libffi6", 87 | "Version": "3.2.1-6", 88 | "Size": 57344 89 | }, 90 | { 91 | "Name": "libpython-stdlib", 92 | "Version": "2.7.13-2", 93 | "Size": 37888 94 | } 95 | ], 96 | "InfoDiff": [] 97 | } 98 | } 99 | ] -------------------------------------------------------------------------------- /tests/file_layer_diff_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/diff-layer-base", 4 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/diff-layer-modified", 5 | "DiffType": "FileLayer", 6 | "Diff": { 7 | "DirDiffs": [ 8 | { 9 | "Adds": null, 10 | "Dels": null, 11 | "Mods": null 12 | }, 13 | { 14 | "Adds": null, 15 | "Dels": null, 16 | "Mods": null 17 | }, 18 | { 19 | "Adds": null, 20 | "Dels": null, 21 | "Mods": null 22 | }, 23 | { 24 | "Adds": null, 25 | "Dels": null, 26 | "Mods": null 27 | }, 28 | { 29 | "Adds": [ 30 | { 31 | "Name": "/modified", 32 | "Size": 9 33 | }, 34 | { 35 | "Name": "/modified/.wh..wh..opq", 36 | "Size": 0 37 | }, 38 | { 39 | "Name": "/modified/modified.txt", 40 | "Size": 9 41 | } 42 | ], 43 | "Dels": [ 44 | { 45 | "Name": "/second", 46 | "Size": 7 47 | }, 48 | { 49 | "Name": "/second/.wh..wh..opq", 50 | "Size": 0 51 | }, 52 | { 53 | "Name": "/second/second.txt", 54 | "Size": 7 55 | } 56 | ], 57 | "Mods": null 58 | } 59 | ] 60 | } 61 | } 62 | ] -------------------------------------------------------------------------------- /tests/hist_diff_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/diff-base", 4 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/diff-modified", 5 | "DiffType": "History", 6 | "Diff": { 7 | "Adds": [ 8 | "/bin/sh -c rm -rf /second \u0026\u0026 mkdir /modified \u0026\u0026 echo \"Modified\" \u003e /modified/modified.txt", 9 | "/bin/sh -c yum -q -y install gcc" 10 | ], 11 | "Dels": [] 12 | } 13 | } 14 | ] -------------------------------------------------------------------------------- /tests/metadata_diff_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/metadata-base", 4 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/metadata-modified", 5 | "DiffType": "Metadata", 6 | "Diff": { 7 | "Adds": [ 8 | "ExposedPorts: 1234/tcp:{}", 9 | "Entrypoint: /entrypoint" 10 | ], 11 | "Dels": [ 12 | "ExposedPorts: 1234/tcp:{} 4321/tcp:{}", 13 | "Entrypoint: " 14 | ] 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /tests/multi_diff_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/multi-base", 4 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/multi-modified", 5 | "DiffType": "Apt", 6 | "Diff": { 7 | "Packages1": [], 8 | "Packages2": [ 9 | { 10 | "Name": "dh-python", 11 | "Version": "1.20141111-2", 12 | "Size": 283648 13 | }, 14 | { 15 | "Name": "libmpdec2", 16 | "Version": "2.4.1-1", 17 | "Size": 281600 18 | }, 19 | { 20 | "Name": "libpython3-stdlib", 21 | "Version": "3.4.2-2", 22 | "Size": 28672 23 | }, 24 | { 25 | "Name": "libpython3.4-minimal", 26 | "Version": "3.4.2-1", 27 | "Size": 3389440 28 | }, 29 | { 30 | "Name": "libpython3.4-stdlib", 31 | "Version": "3.4.2-1", 32 | "Size": 9711616 33 | }, 34 | { 35 | "Name": "python3", 36 | "Version": "3.4.2-2", 37 | "Size": 36864 38 | }, 39 | { 40 | "Name": "python3-minimal", 41 | "Version": "3.4.2-2", 42 | "Size": 98304 43 | }, 44 | { 45 | "Name": "python3.4", 46 | "Version": "3.4.2-1", 47 | "Size": 344064 48 | }, 49 | { 50 | "Name": "python3.4-minimal", 51 | "Version": "3.4.2-1", 52 | "Size": 4614144 53 | } 54 | ], 55 | "InfoDiff": [] 56 | } 57 | }, 58 | { 59 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/multi-base", 60 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/multi-modified", 61 | "DiffType": "Node", 62 | "Diff": { 63 | "Packages1": [], 64 | "Packages2": [ 65 | { 66 | "Name": "pax", 67 | "Path": "/node_modules/pax/", 68 | "Version": "0.2.1", 69 | "Size": 11998 70 | } 71 | ], 72 | "InfoDiff": [ 73 | { 74 | "Package": "sax", 75 | "Info1": [ 76 | { 77 | "Version": "1.2.4", 78 | "Size": 56382 79 | } 80 | ], 81 | "Info2": [ 82 | { 83 | "Version": "0.1.1", 84 | "Size": 127107 85 | } 86 | ] 87 | } 88 | ] 89 | } 90 | }, 91 | { 92 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/multi-base", 93 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/multi-modified", 94 | "DiffType": "Pip", 95 | "Diff": { 96 | "Packages1": [ 97 | { 98 | "Name": "pbr", 99 | "Path": "/usr/local/lib/python3.6/site-packages", 100 | "Version": "3.1.1", 101 | "Size": 447110 102 | } 103 | ], 104 | "Packages2": [ 105 | { 106 | "Name": "retrying", 107 | "Path": "/usr/local/lib/python3.6/site-packages", 108 | "Version": "1.3.3", 109 | "Size": 9955 110 | } 111 | ], 112 | "InfoDiff": [ 113 | { 114 | "Package": "mock", 115 | "Info1": [ 116 | { 117 | "Version": "2.0.0", 118 | "Size": 504226 119 | } 120 | ], 121 | "Info2": [ 122 | { 123 | "Version": "0.8.0", 124 | "Size": 73348 125 | } 126 | ] 127 | } 128 | ] 129 | } 130 | } 131 | ] -------------------------------------------------------------------------------- /tests/node_analysis_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image": "gcr.io/gcp-runtimes/container-diff-tests/node-modified", 4 | "AnalyzeType": "Node", 5 | "Analysis": [ 6 | { 7 | "Name": "npm", 8 | "Path": "/usr/local/lib/node_modules/npm/", 9 | "Version": "5.0.3", 10 | "Size": 13830218 11 | }, 12 | { 13 | "Name": "pax", 14 | "Path": "/node_modules/pax/", 15 | "Version": "0.2.1", 16 | "Size": 11365 17 | }, 18 | { 19 | "Name": "sax", 20 | "Path": "/node_modules/sax/", 21 | "Version": "0.1.1", 22 | "Size": 127390 23 | } 24 | ] 25 | } 26 | ] -------------------------------------------------------------------------------- /tests/node_diff_order_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/node-modified:2.0", 4 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/node-modified", 5 | "DiffType": "Node", 6 | "Diff": { 7 | "Packages1": [], 8 | "Packages2": [ 9 | { 10 | "Name": "pax", 11 | "Path": "/node_modules/pax/", 12 | "Version": "0.2.1", 13 | "Size": 11365 14 | } 15 | ], 16 | "InfoDiff": [] 17 | } 18 | } 19 | ] -------------------------------------------------------------------------------- /tests/pip_analysis_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image": "gcr.io/gcp-runtimes/container-diff-tests/pip-modified", 4 | "AnalyzeType": "Pip", 5 | "Analysis": [ 6 | { 7 | "Name": "argparse", 8 | "Path": "/usr/lib/python2.7", 9 | "Version": "1.2.1", 10 | "Size": 89124 11 | }, 12 | { 13 | "Name": "bzr", 14 | "Path": "/usr/lib/python2.7/dist-packages", 15 | "Version": "2.7.0dev1", 16 | "Size": 13063022 17 | }, 18 | { 19 | "Name": "configobj", 20 | "Path": "/usr/lib/python2.7/dist-packages", 21 | "Version": "5.0.6", 22 | "Size": 136871 23 | }, 24 | { 25 | "Name": "mercurial", 26 | "Path": "/usr/lib/python2.7/dist-packages", 27 | "Version": "3.1.2", 28 | "Size": 4073713 29 | }, 30 | { 31 | "Name": "mock", 32 | "Path": "/usr/local/lib/python3.6/site-packages", 33 | "Version": "2.0.0", 34 | "Size": 504226 35 | }, 36 | { 37 | "Name": "pbr", 38 | "Path": "/usr/local/lib/python3.6/site-packages", 39 | "Version": "3.1.1", 40 | "Size": 447110 41 | }, 42 | { 43 | "Name": "pip", 44 | "Path": "/usr/local/lib/python3.6/site-packages", 45 | "Version": "9.0.1", 46 | "Size": 5289421 47 | }, 48 | { 49 | "Name": "setuptools", 50 | "Path": "/usr/local/lib/python3.6/site-packages", 51 | "Version": "36.0.1", 52 | "Size": 1282800 53 | }, 54 | { 55 | "Name": "six", 56 | "Path": "/usr/local/lib/python3.6/site-packages", 57 | "Version": "1.10.0", 58 | "Size": 30098 59 | }, 60 | { 61 | "Name": "six", 62 | "Path": "/usr/lib/python2.7/dist-packages", 63 | "Version": "1.8.0", 64 | "Size": 27344 65 | }, 66 | { 67 | "Name": "wheel", 68 | "Path": "/usr/local/lib/python3.6/site-packages", 69 | "Version": "0.29.0", 70 | "Size": 103509 71 | }, 72 | { 73 | "Name": "wsgiref", 74 | "Path": "/usr/lib/python2.7", 75 | "Version": "0.1.2", 76 | "Size": 101007 77 | } 78 | ] 79 | } 80 | ] -------------------------------------------------------------------------------- /tests/rpm_diff_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/diff-base:latest", 4 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/diff-modified:latest", 5 | "DiffType": "RPM", 6 | "Diff": { 7 | "Packages1": [], 8 | "Packages2": [ 9 | { 10 | "Name": "binutils", 11 | "Version": "2.27-34.base.el7", 12 | "Size": 25150000 13 | }, 14 | { 15 | "Name": "cpp", 16 | "Version": "4.8.5-36.el7_6.2", 17 | "Size": 15636661 18 | }, 19 | { 20 | "Name": "gcc", 21 | "Version": "4.8.5-36.el7_6.2", 22 | "Size": 39230745 23 | }, 24 | { 25 | "Name": "glibc-devel", 26 | "Version": "2.17-260.el7_6.6", 27 | "Size": 1066078 28 | }, 29 | { 30 | "Name": "glibc-headers", 31 | "Version": "2.17-260.el7_6.6", 32 | "Size": 2338328 33 | }, 34 | { 35 | "Name": "kernel-headers", 36 | "Version": "3.10.0-957.27.2.el7", 37 | "Size": 3824341 38 | }, 39 | { 40 | "Name": "libgomp", 41 | "Version": "4.8.5-36.el7_6.2", 42 | "Size": 212184 43 | }, 44 | { 45 | "Name": "libmpc", 46 | "Version": "1.0.1-3.el7", 47 | "Size": 113833 48 | }, 49 | { 50 | "Name": "mpfr", 51 | "Version": "3.1.1-4.el7", 52 | "Size": 554279 53 | } 54 | ], 55 | "InfoDiff": [] 56 | } 57 | } 58 | ] -------------------------------------------------------------------------------- /tests/size_analysis_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image": "gcr.io/gcp-runtimes/container-diff-tests/diff-base", 4 | "AnalyzeType": "Size", 5 | "Analysis": [ 6 | { 7 | "Name": "gcr.io/gcp-runtimes/container-diff-tests/diff-base", 8 | "Digest": "sha256:b48cce984ba2ddfbc56e1b44cfe3080145ab8c432309cab7010cb6ea725b7e0e", 9 | "Size": 295151156 10 | } 11 | ] 12 | } 13 | ] -------------------------------------------------------------------------------- /tests/size_diff_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/diff-layer-base", 4 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/diff-layer-modified", 5 | "DiffType": "Size", 6 | "Diff": [ 7 | { 8 | "Name": "", 9 | "Size1": 143747263, 10 | "Size2": 143747259 11 | } 12 | ] 13 | } 14 | ] -------------------------------------------------------------------------------- /tests/size_layer_analysis_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image": "gcr.io/gcp-runtimes/container-diff-tests/diff-layer-base", 4 | "AnalyzeType": "SizeLayer", 5 | "Analysis": [ 6 | { 7 | "Name": "0", 8 | "Digest": "sha256:ea92ce749ad4f3ac86c3d674c027c4fd81875a765a43ae78b6ee1ac9c0296967", 9 | "Size": 120852579 10 | }, 11 | { 12 | "Name": "1", 13 | "Digest": "sha256:e4e5d1283791b38034a3d21092953b595fab077f46b33971ec2b2d70a83b2468", 14 | "Size": 23439251 15 | }, 16 | { 17 | "Name": "2", 18 | "Digest": "sha256:3c2cba919283a210665e480bcbf943eaaf4ed87a83f02e81bb286b8bdead0e75", 19 | "Size": 0 20 | }, 21 | { 22 | "Name": "3", 23 | "Digest": "sha256:f81e220940ad2f723a83d57e1e8551f996eaf14275fe4f15c09e844210e3e544", 24 | "Size": 6 25 | }, 26 | { 27 | "Name": "4", 28 | "Digest": "sha256:fed95766b55ca840f9d31c412056a529483a57afc652e795a5ebf1b067e93498", 29 | "Size": 7 30 | }, 31 | { 32 | "Name": "5", 33 | "Digest": "sha256:b307cf1c717a06b5d7a35987ca627073e1052db774cadf90904dd2641424634a", 34 | "Size": 6 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /tests/size_layer_diff_expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image1": "gcr.io/gcp-runtimes/container-diff-tests/diff-layer-base", 4 | "Image2": "gcr.io/gcp-runtimes/container-diff-tests/diff-layer-modified", 5 | "DiffType": "SizeLayer", 6 | "Diff": [ 7 | { 8 | "Name": "4", 9 | "Size1": 7, 10 | "Size2": 9 11 | }, 12 | { 13 | "Name": "5", 14 | "Size1": 6, 15 | "Size2": -1 16 | } 17 | ] 18 | } 19 | ] -------------------------------------------------------------------------------- /util/format_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 util 18 | 19 | import ( 20 | "bufio" 21 | "encoding/json" 22 | "errors" 23 | "io" 24 | "strings" 25 | "text/tabwriter" 26 | "text/template" 27 | 28 | "github.com/sirupsen/logrus" 29 | ) 30 | 31 | var templates = map[string]string{ 32 | "SingleVersionPackageDiff": SingleVersionDiffOutput, 33 | "MultiVersionPackageDiff": MultiVersionDiffOutput, 34 | "HistDiff": HistoryDiffOutput, 35 | "MetadataDiff": MetadataDiffOutput, 36 | "DirDiff": FSDiffOutput, 37 | "MultipleDirDiff": FSLayerDiffOutput, 38 | "FilenameDiff": FilenameDiffOutput, 39 | "ListAnalyze": ListAnalysisOutput, 40 | "FileAnalyze": FileAnalysisOutput, 41 | "FileLayerAnalyze": FileLayerAnalysisOutput, 42 | "SizeAnalyze": SizeAnalysisOutput, 43 | "SizeLayerAnalyze": SizeLayerAnalysisOutput, 44 | "SizeDiff": SizeDiffOutput, 45 | "SizeLayerDiff": SizeLayerDiffOutput, 46 | "MultiVersionPackageAnalyze": MultiVersionPackageOutput, 47 | "SingleVersionPackageAnalyze": SingleVersionPackageOutput, 48 | "SingleVersionPackageLayerAnalyze": SingleVersionPackageLayerOutput, 49 | } 50 | 51 | func JSONify(writer io.Writer, diff interface{}) error { 52 | diffBytes, err := json.MarshalIndent(diff, "", " ") 53 | if err != nil { 54 | return err 55 | } 56 | f := bufio.NewWriter(writer) 57 | defer f.Flush() 58 | f.Write(diffBytes) 59 | return nil 60 | } 61 | 62 | func getTemplate(templateType string) (string, error) { 63 | if template, ok := templates[templateType]; ok { 64 | return template, nil 65 | } 66 | return "", errors.New("No available template") 67 | } 68 | 69 | func TemplateOutput(writer io.Writer, diff interface{}, templateType string) error { 70 | outputTmpl, err := getTemplate(templateType) 71 | if err != nil { 72 | logrus.Error(err) 73 | } 74 | funcs := template.FuncMap{"join": strings.Join} 75 | tmpl, err := template.New("tmpl").Funcs(funcs).Parse(outputTmpl) 76 | if err != nil { 77 | logrus.Error(err) 78 | return err 79 | } 80 | w := tabwriter.NewWriter(writer, 8, 8, 8, ' ', 0) 81 | err = tmpl.Execute(w, diff) 82 | if err != nil { 83 | logrus.Error(err) 84 | return err 85 | } 86 | w.Flush() 87 | return nil 88 | } 89 | 90 | func TemplateOutputFromFormat(writer io.Writer, diff interface{}, templateType string, format string) error { 91 | if format == "" { 92 | return TemplateOutput(writer, diff, templateType) 93 | } 94 | funcs := template.FuncMap{"join": strings.Join} 95 | tmpl, err := template.New("tmpl").Funcs(funcs).Parse(format) 96 | if err != nil { 97 | logrus.Warningf("User specified format resulted in error, printing default output.") 98 | logrus.Error(err) 99 | return TemplateOutput(writer, diff, templateType) 100 | } 101 | w := tabwriter.NewWriter(writer, 8, 8, 8, ' ', 0) 102 | err = tmpl.Execute(w, diff) 103 | if err != nil { 104 | return err 105 | } 106 | w.Flush() 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /util/image_utils_test.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 util 18 | 19 | import ( 20 | "testing" 21 | 22 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 23 | ) 24 | 25 | func TestImageTags(t *testing.T) { 26 | tests := []struct { 27 | image string 28 | hasTag bool 29 | }{ 30 | { 31 | image: "gcr.io/test_image/foo:latest", 32 | hasTag: true, 33 | }, 34 | { 35 | image: "gcr.io/test_image/foo:", 36 | hasTag: false, 37 | }, 38 | { 39 | image: "daemon://gcr.io/test_image/foo:test", 40 | hasTag: true, 41 | }, 42 | { 43 | image: "remote://gcr.io/test_image_foo", 44 | hasTag: false, 45 | }, 46 | } 47 | 48 | for _, test := range tests { 49 | if pkgutil.HasTag(test.image) != test.hasTag { 50 | t.Errorf("Error checking tag on image %s", test.image) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /util/output_sort_utils_test.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 util 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 24 | ) 25 | 26 | var packageTests = [][]PackageOutput{ 27 | { 28 | {Name: "a", Version: "1.2", Size: 10}, 29 | {Name: "b", Version: "1.5", Size: 12}, 30 | {Name: "c", Version: "1.4", Size: 20}, 31 | }, 32 | { 33 | {Name: "a", Version: "1.2", Size: 10}, 34 | {Name: "b", Version: "1.5", Size: 12}, 35 | {Name: "c", Version: "1.4", Size: 12}, 36 | }, 37 | { 38 | {Name: "a", Version: "1.2", Size: 10}, 39 | {Name: "a", Version: "1.4", Size: 20}, 40 | {Name: "a", Version: "1.2", Size: 15}, 41 | }, 42 | } 43 | 44 | func TestSortPackageOutput(t *testing.T) { 45 | for _, test := range []struct { 46 | input []PackageOutput 47 | sortBy func(a, b *PackageOutput) bool 48 | expected []PackageOutput 49 | }{ 50 | { 51 | input: packageTests[0], 52 | sortBy: packageSizeSort, 53 | expected: []PackageOutput{ 54 | {Name: "c", Version: "1.4", Size: 20}, 55 | {Name: "b", Version: "1.5", Size: 12}, 56 | {Name: "a", Version: "1.2", Size: 10}, 57 | }, 58 | }, 59 | { 60 | input: packageTests[0], 61 | sortBy: packageNameSort, 62 | expected: []PackageOutput{ 63 | {Name: "a", Version: "1.2", Size: 10}, 64 | {Name: "b", Version: "1.5", Size: 12}, 65 | {Name: "c", Version: "1.4", Size: 20}, 66 | }, 67 | }, 68 | { 69 | input: packageTests[1], 70 | sortBy: packageSizeSort, 71 | expected: []PackageOutput{ 72 | {Name: "b", Version: "1.5", Size: 12}, 73 | {Name: "c", Version: "1.4", Size: 12}, 74 | {Name: "a", Version: "1.2", Size: 10}, 75 | }, 76 | }, 77 | { 78 | input: packageTests[2], 79 | sortBy: packageNameSort, 80 | expected: []PackageOutput{ 81 | {Name: "a", Version: "1.2", Size: 15}, 82 | {Name: "a", Version: "1.2", Size: 10}, 83 | {Name: "a", Version: "1.4", Size: 20}, 84 | }, 85 | }, 86 | } { 87 | actual := test.input 88 | packageBy(test.sortBy).Sort(actual) 89 | if !reflect.DeepEqual(actual, test.expected) { 90 | t.Errorf("\nExpected: %v\nGot: %v", test.expected, actual) 91 | } 92 | } 93 | } 94 | 95 | var directoryTests = [][]pkgutil.DirectoryEntry{ 96 | { 97 | {Name: "a", Size: 10}, 98 | {Name: "b", Size: 12}, 99 | {Name: "c", Size: 20}, 100 | }, 101 | { 102 | {Name: "a", Size: 10}, 103 | {Name: "b", Size: 12}, 104 | {Name: "c", Size: 12}, 105 | }, 106 | } 107 | 108 | func TestSortDirectoryEntries(t *testing.T) { 109 | for _, test := range []struct { 110 | input []pkgutil.DirectoryEntry 111 | sortBy func(a, b *pkgutil.DirectoryEntry) bool 112 | expected []pkgutil.DirectoryEntry 113 | }{ 114 | { 115 | input: directoryTests[0], 116 | sortBy: directorySizeSort, 117 | expected: []pkgutil.DirectoryEntry{ 118 | {Name: "c", Size: 20}, 119 | {Name: "b", Size: 12}, 120 | {Name: "a", Size: 10}, 121 | }, 122 | }, 123 | { 124 | input: directoryTests[0], 125 | sortBy: directoryNameSort, 126 | expected: []pkgutil.DirectoryEntry{ 127 | {Name: "a", Size: 10}, 128 | {Name: "b", Size: 12}, 129 | {Name: "c", Size: 20}, 130 | }, 131 | }, 132 | { 133 | input: directoryTests[1], 134 | sortBy: directorySizeSort, 135 | expected: []pkgutil.DirectoryEntry{ 136 | {Name: "b", Size: 12}, 137 | {Name: "c", Size: 12}, 138 | {Name: "a", Size: 10}, 139 | }, 140 | }, 141 | } { 142 | actual := test.input 143 | directoryBy(test.sortBy).Sort(actual) 144 | if !reflect.DeepEqual(actual, test.expected) { 145 | t.Errorf("\nExpected: %v\nGot: %v", test.expected, actual) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /util/output_text_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 util 18 | 19 | import ( 20 | "code.cloudfoundry.org/bytefmt" 21 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 22 | ) 23 | 24 | type StrPackageOutput struct { 25 | Name string 26 | Path string 27 | Version string 28 | Size string 29 | } 30 | 31 | func stringifySize(size int64) string { 32 | strSize := "unknown" 33 | if size != -1 { 34 | strSize = bytefmt.ByteSize(uint64(size)) 35 | } 36 | return strSize 37 | } 38 | 39 | func stringifyPackages(packages []PackageOutput) []StrPackageOutput { 40 | strPackages := []StrPackageOutput{} 41 | for _, pack := range packages { 42 | strSize := stringifySize(pack.Size) 43 | strPackages = append(strPackages, StrPackageOutput{pack.Name, pack.Path, pack.Version, strSize}) 44 | } 45 | return strPackages 46 | } 47 | 48 | type StrMultiVersionInfo struct { 49 | Package string 50 | Info1 []StrPackageInfo 51 | Info2 []StrPackageInfo 52 | } 53 | 54 | type StrPackageInfo struct { 55 | Version string 56 | Size string 57 | } 58 | 59 | func stringifyPackageInfo(info PackageInfo) StrPackageInfo { 60 | return StrPackageInfo{Version: info.Version, Size: stringifySize(info.Size)} 61 | } 62 | 63 | type StrInfo struct { 64 | Package string 65 | Info1 StrPackageInfo 66 | Info2 StrPackageInfo 67 | } 68 | 69 | func stringifyPackageDiff(infoDiff []Info) (strInfoDiff []StrInfo) { 70 | for _, diff := range infoDiff { 71 | strInfo1 := stringifyPackageInfo(diff.Info1) 72 | strInfo2 := stringifyPackageInfo(diff.Info2) 73 | 74 | strDiff := StrInfo{Package: diff.Package, Info1: strInfo1, Info2: strInfo2} 75 | strInfoDiff = append(strInfoDiff, strDiff) 76 | } 77 | return 78 | } 79 | 80 | func stringifyMultiVersionPackageDiff(infoDiff []MultiVersionInfo) (strInfoDiff []StrMultiVersionInfo) { 81 | for _, diff := range infoDiff { 82 | strInfos1 := []StrPackageInfo{} 83 | for _, info := range diff.Info1 { 84 | strInfos1 = append(strInfos1, stringifyPackageInfo(info)) 85 | } 86 | 87 | strInfos2 := []StrPackageInfo{} 88 | for _, info := range diff.Info2 { 89 | strInfos2 = append(strInfos2, stringifyPackageInfo(info)) 90 | } 91 | 92 | strDiff := StrMultiVersionInfo{Package: diff.Package, Info1: strInfos1, Info2: strInfos2} 93 | strInfoDiff = append(strInfoDiff, strDiff) 94 | } 95 | return 96 | } 97 | 98 | type StrDirectoryEntry struct { 99 | Name string 100 | Size string 101 | } 102 | 103 | func stringifyDirectoryEntries(entries []pkgutil.DirectoryEntry) (strEntries []StrDirectoryEntry) { 104 | for _, entry := range entries { 105 | strEntry := StrDirectoryEntry{Name: entry.Name, Size: stringifySize(entry.Size)} 106 | strEntries = append(strEntries, strEntry) 107 | } 108 | return 109 | } 110 | 111 | type StrEntryDiff struct { 112 | Name string 113 | Size1 string 114 | Size2 string 115 | } 116 | 117 | func stringifyEntryDiffs(entries []EntryDiff) (strEntries []StrEntryDiff) { 118 | for _, entry := range entries { 119 | strEntry := StrEntryDiff{Name: entry.Name, Size1: stringifySize(entry.Size1), Size2: stringifySize(entry.Size2)} 120 | strEntries = append(strEntries, strEntry) 121 | } 122 | return 123 | } 124 | 125 | type StrSizeEntry struct { 126 | Name string 127 | Digest string 128 | Size string 129 | } 130 | 131 | func stringifySizeEntries(entries []SizeEntry) (strEntries []StrSizeEntry) { 132 | for _, entry := range entries { 133 | strEntry := StrSizeEntry{Name: entry.Name, Digest: entry.Digest.String(), Size: stringifySize(entry.Size)} 134 | strEntries = append(strEntries, strEntry) 135 | } 136 | return 137 | } 138 | 139 | type StrSizeDiff struct { 140 | Name string 141 | Size1 string 142 | Size2 string 143 | } 144 | 145 | func stringifySizeDiffs(entries []SizeDiff) (strEntries []StrSizeDiff) { 146 | for _, entry := range entries { 147 | strEntry := StrSizeDiff{Name: entry.Name, Size1: stringifySize(entry.Size1), Size2: stringifySize(entry.Size2)} 148 | strEntries = append(strEntries, strEntry) 149 | } 150 | return 151 | } 152 | -------------------------------------------------------------------------------- /util/size_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 util 18 | 19 | import "github.com/google/go-containerregistry/pkg/v1" 20 | 21 | type SizeEntry struct { 22 | Name string 23 | Digest v1.Hash 24 | Size int64 25 | } 26 | 27 | type SizeDiff struct { 28 | Name string 29 | Size1 int64 30 | Size2 int64 31 | } 32 | -------------------------------------------------------------------------------- /util/tar_utils_test.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 util 18 | 19 | import ( 20 | "testing" 21 | 22 | pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" 23 | ) 24 | 25 | func TestIsTar(t *testing.T) { 26 | testCases := []struct { 27 | input string 28 | expected bool 29 | }{ 30 | {input: "/testTar/la-croix1.tar", expected: true}, 31 | {input: "/testTar/la-croix1-actual", expected: false}, 32 | } 33 | for _, test := range testCases { 34 | actual := pkgutil.IsTar(test.input) 35 | if test.expected != actual { 36 | t.Errorf("Expected: %t but got: %t", test.expected, actual) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /util/testTars/la-croix-dir-update-actual/lime.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-dir-update-actual/lime.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-dir-update-actual/nest/f1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-dir-update-actual/nest/f1.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-dir-update-actual/nest/f2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-dir-update-actual/nest/f2.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-dir-update-actual/passionfruit.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-dir-update-actual/passionfruit.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-dir-update-actual/peach-pear.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-dir-update-actual/peach-pear.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-dir-update.tar: -------------------------------------------------------------------------------- 1 | PaxHeader/nest000750 777777 013202 00000000051 13122533542 015235 xustar00atisdalenonconf000000 000000 14 uid=391207 2 | 27 atime=1498081640.631133 3 | nest/000750 1374047013202 00000000000 13122533542 013342 5ustar00atisdalenonconf000000 000000 nest/PaxHeader/f2.txt000640 777777 013202 00000000051 13122533542 016360 xustar00atisdalenonconf000000 000000 14 uid=391207 4 | 27 atime=1498081640.631133 5 | nest/f2.txt000640 1374047013202 00000000000 13122533542 014401 0ustar00atisdalenonconf000000 000000 -------------------------------------------------------------------------------- /util/testTars/la-croix-starter/lime.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-starter/lime.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-starter/nest/f1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-starter/nest/f1.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-starter/passionfruit.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-starter/passionfruit.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-starter/peach-pear.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-starter/peach-pear.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-update-actual/lime.txt: -------------------------------------------------------------------------------- 1 | updated text 2 | -------------------------------------------------------------------------------- /util/testTars/la-croix-update-actual/nest/f1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-update-actual/nest/f1.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-update-actual/passionfruit.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-update-actual/passionfruit.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-update-actual/peach-pear.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-update-actual/peach-pear.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-update.tar: -------------------------------------------------------------------------------- 1 | PaxHeader/lime.txt000640 777777 013202 00000000104 13142114310 016013 xustar00atisdalenonconf000000 000000 14 uid=391207 2 | 27 mtime=1502124232.168352 3 | 27 atime=1502124232.160352 4 | lime.txt000640 1374047013202 00000000015 13142114310 014043 0ustar00atisdalenonconf000000 000000 updated text 5 | -------------------------------------------------------------------------------- /util/testTars/la-croix-wh-actual/nest2/hello: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-wh-actual/nest2/hello -------------------------------------------------------------------------------- /util/testTars/la-croix-wh-actual/passionfruit.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-wh-actual/passionfruit.txt -------------------------------------------------------------------------------- /util/testTars/la-croix-wh-actual/peach-pear.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix-wh-actual/peach-pear.txt -------------------------------------------------------------------------------- /util/testTars/la-croix1-actual/lime.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix1-actual/lime.txt -------------------------------------------------------------------------------- /util/testTars/la-croix1-actual/passionfruit.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix1-actual/passionfruit.txt -------------------------------------------------------------------------------- /util/testTars/la-croix1-actual/peach-pear.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix1-actual/peach-pear.txt -------------------------------------------------------------------------------- /util/testTars/la-croix1.tar: -------------------------------------------------------------------------------- 1 | PaxHeader/passionfruit.txt000640 777777 013202 00000000051 13122533463 017630 xustar00atisdalenonconf000000 000000 14 uid=391207 2 | 27 atime=1498070311.983759 3 | passionfruit.txt000640 1374047013202 00000000000 13122533463 015651 0ustar00atisdalenonconf000000 000000 PaxHeader/lime.txt000640 777777 013202 00000000051 13122533445 016030 xustar00atisdalenonconf000000 000000 14 uid=391207 4 | 27 atime=1498070311.975759 5 | lime.txt000640 1374047013202 00000000000 13122533445 014051 0ustar00atisdalenonconf000000 000000 PaxHeader/peach-pear.txt000640 777777 013202 00000000051 13122533456 017111 xustar00atisdalenonconf000000 000000 14 uid=391207 6 | 27 atime=1498070311.983759 7 | peach-pear.txt000640 1374047013202 00000000000 13122533456 015132 0ustar00atisdalenonconf000000 000000 -------------------------------------------------------------------------------- /util/testTars/la-croix2-actual/lime.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix2-actual/lime.txt -------------------------------------------------------------------------------- /util/testTars/la-croix2-actual/nest/f1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix2-actual/nest/f1.txt -------------------------------------------------------------------------------- /util/testTars/la-croix2-actual/passionfruit.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix2-actual/passionfruit.txt -------------------------------------------------------------------------------- /util/testTars/la-croix2-actual/peach-pear.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix2-actual/peach-pear.txt -------------------------------------------------------------------------------- /util/testTars/la-croix3-actual/lime.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix3-actual/lime.txt -------------------------------------------------------------------------------- /util/testTars/la-croix3-actual/nest/f1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix3-actual/nest/f1.txt -------------------------------------------------------------------------------- /util/testTars/la-croix3-actual/nested-dir.tar: -------------------------------------------------------------------------------- 1 | PaxHeader/f2.txt000640 777777 013202 00000000051 13122533632 015407 xustar00atisdalenonconf000000 000000 14 uid=391207 2 | 27 atime=1498086485.636409 3 | f2.txt000640 1374047013202 00000000000 13122533632 013430 0ustar00atisdalenonconf000000 000000 -------------------------------------------------------------------------------- /util/testTars/la-croix3-actual/passionfruit.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix3-actual/passionfruit.txt -------------------------------------------------------------------------------- /util/testTars/la-croix3-actual/peach-pear.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix3-actual/peach-pear.txt -------------------------------------------------------------------------------- /util/testTars/la-croix3-full/lime.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix3-full/lime.txt -------------------------------------------------------------------------------- /util/testTars/la-croix3-full/nest/f1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix3-full/nest/f1.txt -------------------------------------------------------------------------------- /util/testTars/la-croix3-full/nested-dir/f2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix3-full/nested-dir/f2.txt -------------------------------------------------------------------------------- /util/testTars/la-croix3-full/passionfruit.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix3-full/passionfruit.txt -------------------------------------------------------------------------------- /util/testTars/la-croix3-full/peach-pear.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/container-diff/b90c99c638e3ab0185d264c448ca22d349d93972/util/testTars/la-croix3-full/peach-pear.txt -------------------------------------------------------------------------------- /util/test_files/dir1/file1: -------------------------------------------------------------------------------- 1 | This is a file. 2 | -------------------------------------------------------------------------------- /util/test_files/dir1/file2: -------------------------------------------------------------------------------- 1 | This is a file. 2 | -------------------------------------------------------------------------------- /util/test_files/dir1/file3: -------------------------------------------------------------------------------- 1 | This is a file. 2 | -------------------------------------------------------------------------------- /util/test_files/dir1_copy/file1: -------------------------------------------------------------------------------- 1 | This is a file. 2 | -------------------------------------------------------------------------------- /util/test_files/dir1_copy/file2: -------------------------------------------------------------------------------- 1 | This is a file. 2 | -------------------------------------------------------------------------------- /util/test_files/dir1_copy/file3: -------------------------------------------------------------------------------- 1 | This is a file. 2 | -------------------------------------------------------------------------------- /util/test_files/dir2/file1: -------------------------------------------------------------------------------- 1 | This is a file. 2 | -------------------------------------------------------------------------------- /util/test_files/dir2/file2: -------------------------------------------------------------------------------- 1 | This is a changed file. 2 | -------------------------------------------------------------------------------- /util/test_files/dir2/file4: -------------------------------------------------------------------------------- 1 | This is a file. 2 | -------------------------------------------------------------------------------- /util/test_files/dir2_modified/file1: -------------------------------------------------------------------------------- 1 | This is a modified file. 2 | -------------------------------------------------------------------------------- /util/test_files/dir2_modified/file2: -------------------------------------------------------------------------------- 1 | This is a modified file. 2 | -------------------------------------------------------------------------------- /util/test_files/dir2_modified/file4: -------------------------------------------------------------------------------- 1 | This is a modified file. 2 | -------------------------------------------------------------------------------- /util/test_files/file1: -------------------------------------------------------------------------------- 1 | This is a file. 2 | -------------------------------------------------------------------------------- /util/test_files/file1_copy: -------------------------------------------------------------------------------- 1 | This is a file. 2 | -------------------------------------------------------------------------------- /util/test_files/file2: -------------------------------------------------------------------------------- 1 | This is a different file. 2 | -------------------------------------------------------------------------------- /version/version.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 version 18 | 19 | import "fmt" 20 | 21 | // Bump this on release 22 | var version = "v0.19.0" 23 | 24 | // When built using `make` this is overridden via -ldflags 25 | var gitVersion = "(unknown)" 26 | 27 | // returns just the vX.Y.Z version suitable for `make release` 28 | func GetShortVersion() string { 29 | return version 30 | } 31 | 32 | func GetVersion() string { 33 | return fmt.Sprintf("%s built from git %s", version, gitVersion) 34 | } 35 | --------------------------------------------------------------------------------