├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── lint-pr-title.yml │ └── release-please.yml ├── .gitignore ├── .golangci.yml ├── .release-please-manifest.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── command ├── command.go ├── get.go ├── remove.go └── set.go ├── copyright_header ├── fenv └── get.go ├── ffs ├── create.go └── get.go ├── fnet ├── dial.go └── get.go ├── go.mod ├── go.sum ├── option ├── modifier.go ├── nerdctl.go ├── option.go └── option_test.go ├── release-please-config.json ├── run └── run_test.go ├── staticcheck.conf └── tests ├── build.go ├── builder_prune.go ├── compose_build.go ├── compose_down.go ├── compose_kill.go ├── compose_logs.go ├── compose_ps.go ├── compose_pull.go ├── cp.go ├── create.go ├── events.go ├── exec.go ├── image_history.go ├── image_inspect.go ├── image_prune.go ├── images.go ├── info.go ├── inspect.go ├── kill.go ├── load.go ├── login.go ├── logout.go ├── logs.go ├── network_create.go ├── network_inspect.go ├── network_ls.go ├── network_rm.go ├── port.go ├── ps.go ├── pull.go ├── push.go ├── restart.go ├── rm.go ├── rmi.go ├── run.go ├── save.go ├── start.go ├── stats.go ├── stop.go ├── tag.go ├── tests.go ├── volume_create.go ├── volume_inspect.go ├── volume_ls.go ├── volume_prune.go └── volume_rm.go /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Issue #, if available: 2 | 3 | *Description of changes:* 4 | 5 | *Testing done:* 6 | 7 | 8 | 9 | - [ ] I've reviewed the guidance in CONTRIBUTING.md 10 | 11 | 12 | #### License Acceptance 13 | 14 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | commit-message: 8 | # When a dependency is updated, 9 | # we want release-please to treat the corresponding commit as a releasable unit 10 | # because it may contain a security fix. 11 | # 12 | # Re. how that is achieved, see `changelog-types` in workflows/release-please.yml. 13 | prefix: "build" 14 | include: "scope" 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: "daily" 19 | commit-message: 20 | prefix: "ci" 21 | include: "scope" 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | go-linter: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 15 | with: 16 | go-version-file: go.mod 17 | cache: true 18 | - name: golangci-lint 19 | uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6.5.2 20 | with: 21 | # Pin the version in case all the builds start to fail at the same time. 22 | # There may not be an automatic way (e.g., dependabot) to update a specific parameter of a Github Action, 23 | # so we will just update it manually whenever it makes sense (e.g., a feature that we want is added). 24 | version: v1.62.2 25 | args: --fix=false 26 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR Title" 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - reopened 9 | - synchronize 10 | 11 | jobs: 12 | main: 13 | name: conventional-commit 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 # v4.2.0 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.idea 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # The sections in this file are ordered in the order presented in https://golangci-lint.run/usage/configuration/. 2 | # The nested fields are ordered alphabetically. 3 | 4 | linters-settings: 5 | goheader: 6 | template-path: copyright_header 7 | goimports: 8 | local-prefixes: github.com/runfinch/common-tests 9 | gosec: 10 | config: 11 | G306: "0o644" 12 | lll: 13 | # 145 is just a lax value as we don't want this to be too strict. 14 | line-length: 145 15 | tab-width: 4 16 | makezero: 17 | always: true 18 | nolintlint: 19 | require-explanation: true 20 | require-specific: true 21 | stylecheck: 22 | # ST1003 is left out because it is a bit opinionated. 23 | checks: ["all", "-ST1003"] 24 | linters: 25 | enable: 26 | - copyloopvar 27 | - errname 28 | - errorlint 29 | - forcetypeassert 30 | - gocritic 31 | - godot 32 | - gofumpt 33 | - goheader 34 | - goimports 35 | - gosec 36 | - lll 37 | - makezero 38 | - misspell 39 | - nilerr 40 | - nilnil 41 | - nolintlint 42 | - nosprintfhostport 43 | - paralleltest 44 | - predeclared 45 | - reassign 46 | - revive 47 | - testableexamples 48 | - unconvert 49 | - unparam 50 | - usestdlibvars 51 | - wastedassign 52 | - whitespace 53 | - stylecheck 54 | issues: 55 | # Some checks enabled in the stylecheck setting are disabled by default 56 | # (e.g., https://golangci-lint.run/usage/false-positives/#exc0013), 57 | # so we need to enable them explicitly here. 58 | exclude-use-default: false 59 | fix: true 60 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "0.9.4" 3 | } 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Note that Finch CLI may not be an option here, or there will be circular dependency. 5 | # To be specific, when we update e2e tests in the future, 6 | # it is possible that the functionality has not been implemented in Finch CLI yet, 7 | # so the test logs won't be the expected ones. 8 | # 9 | # For example, if the test is "pulls an alpine image", 10 | # then the test subject should pull an alpine image successfully and the corresponding logs should be printed. 11 | # If the functionality is not in the test subject yet, the developer will see some failing logs instead. 12 | # 13 | # The assumption is that when adding/updating an e2e test, 14 | # it is likely that the publicly available Finch CLI does not have the corresponding functionality 15 | # (i.e., the "pulls an alpine image" in the example above) yet. 16 | # It is because that the functionality cannot be added to Finch CLI until the corresponding test is in place. 17 | # In other words, the test has to be merged into Finch Test and used by Finch CLI, then 18 | # the CI of Finch CLI can run with the updated test and the added functionality. 19 | # 20 | # As a result, we may want to use another reference implementation (i.e., nerdctl here) when testing the tests themselves. 21 | SUBJECT ?= nerdctl 22 | 23 | VERBOSE ?= true 24 | VERBOSE_FLAGS = 25 | ifeq ($(VERBOSE),true) 26 | # https://github.com/onsi/ginkgo/issues/381 27 | VERBOSE_FLAGS = -test.v -ginkgo.v 28 | endif 29 | 30 | .PHONY: run 31 | run: 32 | go test -timeout 30m ./run/... $(VERBOSE_FLAGS) -args --subject="$(SUBJECT)" 33 | 34 | .PHONY: lint 35 | # To run golangci-lint locally: https://golangci-lint.run/usage/install/#local-installation 36 | lint: 37 | golangci-lint run 38 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Finch Common End-to-end Tests 2 | 3 | This repository contains common e2e tests that can be imported as Go functions (The Functions). 4 | 5 | The scope of this repository is limited to container development commands (i.e., no tests for VM lifecycle). 6 | 7 | To enable users of this repository to freely extend the set of tests to be run, this repository is not in charge of the lifecycle of the testing environment (e.g., spin up or tear down the testing VM): 8 | 9 | - When The Functions are called, it is assumed that the environment for the specified test subject is already set up (e.g., assume that `finch` is the test subject here, the underlying VM is up, and sanity-checking command like `finch version` can return successfully). 10 | - The Functions may remove any existing objects in the testing environment (e.g., a container that is currently running may be stopped and removed by The Functions). 11 | - The Functions will remove any objects generated by it (e.g., an image pulled during the testing). 12 | 13 | Note that The Functions may create temporary files under the home directory of the current user, so make sure that the home directory is writable by the "subject" (see `New` in [`option/option.go`](option/option.go) for more details), or the tests may fail. For more details, see [`ffs/create.go`](ffs/create.go). 14 | 15 | ## Development Flow 16 | 17 | 1. Loop 18 | 1. Update the tests as you wish. 19 | 1. To save time, use the [`Focus`](https://onsi.github.io/ginkgo/#focused-specs) decorator to only run the tests you're developing. 20 | 1. Run `make run` (you may need to tune the Makefile variables according to your setup). If the output makes sense to you, exit the loop. If not, go back to the step 1. 21 | 1. Remove the `Focus` decorators added during development. 22 | 1. Create a pull request. 23 | 1. After the pull request is merged, cut a release if necessary. Please refer to the [versioning section](#versioning) if you need to cut a release. 24 | 25 | ## Versioning 26 | 27 | We use [semantic versioning](https://github.com/semver/semver/blob/master/semver.md). There are some concrete examples in the sections below. They are meant to be extended when new interesting cases are discovered so that in the future, we can speed up the process of deciding which version to bump by having some references here. 28 | 29 | ### Major 30 | 31 | - Change the signature of `option.New` to accept more arguments in a backward-incompatible manner. 32 | 33 | ### Minor 34 | 35 | - Add a new test (i.e., add an exported function to the `common-tests` package). 36 | 37 | ### Patch 38 | 39 | - Change the image that is pulled in `tests.Pull` from `alpine` to a smaller image. 40 | 41 | ## Sync Between Tests and Code 42 | 43 | This section explains the workflow to keep tests up to date along with code changes. 44 | 45 | Take Finch CLI as an example, when adding/updating an e2e test, it is likely that the publicly available Finch CLI does not have the corresponding functionality yet. It is because the functionality cannot be added to Finch CLI until the corresponding test is in place. In other words, the test has to be merged into Finch Test, and a release will be cut to include the newly added test, then the test can be used by Finch CLI via referencing the newly created release in its `go.mod`. Last, the CI of Finch CLI can run with the updated test and the added functionality. 46 | 47 | ## Security 48 | 49 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 50 | 51 | ## License 52 | 53 | This project is licensed under the Apache-2.0 License. 54 | -------------------------------------------------------------------------------- /command/command.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package command invokes external commands. 5 | // 6 | // It is designed in a way that the users of the `tests` package can also utilize this package when writing their own tests. 7 | package command 8 | 9 | import ( 10 | "bufio" 11 | "io" 12 | "strings" 13 | "time" 14 | 15 | ginkgo "github.com/onsi/ginkgo/v2" 16 | "github.com/onsi/gomega" 17 | "github.com/onsi/gomega/gexec" 18 | 19 | "github.com/runfinch/common-tests/option" 20 | ) 21 | 22 | // Command represents a to-be-executed shell command. 23 | type Command struct { 24 | opt *option.Option 25 | args []string 26 | stdout io.Writer 27 | stdin io.Reader 28 | timeout time.Duration 29 | shouldWait bool 30 | shouldCheckExitCode bool 31 | shouldSucceed bool 32 | } 33 | 34 | // New creates a command with the default configuration. 35 | // Invoke the WithXXX methods to configure it. 36 | // After it's configured, invoke Run to run the command. 37 | // 38 | // Also check the RunXXX and StdXXX wrapper functions in this package for simple use cases. 39 | // As a rule of thumb, please don't create a wrapper function for a use case that involves more than one thing. 40 | // For example, 41 | // 42 | // command.RunWithoutSuccessfulExit(o, arg).Err.Contents() 43 | // 44 | // may be more readable than 45 | // 46 | // command.RunWithoutSuccessfulExitAndReturnStderr(o, arg) 47 | func New(opt *option.Option, args ...string) *Command { 48 | return &Command{ 49 | opt: opt, 50 | args: args, 51 | stdout: ginkgo.GinkgoWriter, 52 | stdin: nil, 53 | timeout: 10 * time.Second, 54 | shouldWait: true, 55 | shouldCheckExitCode: true, 56 | shouldSucceed: true, 57 | } 58 | } 59 | 60 | // WithTimeout updates the timeout for the session. 61 | func (c *Command) WithTimeout(timeout time.Duration) *Command { 62 | c.timeout = timeout 63 | return c 64 | } 65 | 66 | // WithTimeoutInSeconds updates the timeout (in seconds) for the session. 67 | func (c *Command) WithTimeoutInSeconds(timeout time.Duration) *Command { 68 | return c.WithTimeout(timeout * time.Second) 69 | } 70 | 71 | // WithoutSuccessfulExit ensures that the exit code of the command is not 0. 72 | func (c *Command) WithoutSuccessfulExit() *Command { 73 | c.shouldSucceed = false 74 | c.shouldCheckExitCode = true 75 | c.shouldWait = true 76 | return c 77 | } 78 | 79 | // WithoutWait disables waiting for a session to finish. 80 | func (c *Command) WithoutWait() *Command { 81 | c.shouldWait = false 82 | return c 83 | } 84 | 85 | // WithoutCheckingExitCode disables exit code checking after the session ends. 86 | func (c *Command) WithoutCheckingExitCode() *Command { 87 | c.shouldCheckExitCode = false 88 | c.shouldWait = true 89 | return c 90 | } 91 | 92 | // WithStdout specifies the output writer for gexec.Start. 93 | func (c *Command) WithStdout(stdout io.Writer) *Command { 94 | c.stdout = stdout 95 | return c 96 | } 97 | 98 | // WithStdin specifies the input reader for gexec.Start. 99 | func (c *Command) WithStdin(stdin io.Reader) *Command { 100 | c.stdin = stdin 101 | return c 102 | } 103 | 104 | // Run starts a session and waits for it to finish. 105 | // It's behavior can be modified by using other Command methods. 106 | // It returns the ended session for further assertions. 107 | func (c *Command) Run() *gexec.Session { 108 | cmd := c.opt.NewCmd(c.args...) 109 | cmd.Stdin = c.stdin 110 | session, err := gexec.Start(cmd, c.stdout, ginkgo.GinkgoWriter) 111 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 112 | if !c.shouldWait { 113 | return session 114 | } 115 | session.Wait(c.timeout) 116 | 117 | if !c.shouldCheckExitCode { 118 | return session 119 | } 120 | 121 | if c.shouldSucceed { 122 | gomega.Expect(session).Should(gexec.Exit(0)) 123 | } else { 124 | gomega.Expect(session).ShouldNot(gexec.Exit(0)) 125 | } 126 | 127 | return session 128 | } 129 | 130 | // Run starts a session, waits for it to finish, ensures the exit code to be 0, 131 | // and returns the ended session to be used for assertions. 132 | func Run(o *option.Option, args ...string) *gexec.Session { 133 | return New(o, args...).Run() 134 | } 135 | 136 | // RunWithoutSuccessfulExit starts a session, waits for it to finish, ensures the exit code not to be 0, 137 | // and returns the ended session to be used for assertions. 138 | func RunWithoutSuccessfulExit(o *option.Option, args ...string) *gexec.Session { 139 | return New(o, args...).WithoutSuccessfulExit().Run() 140 | } 141 | 142 | // Stdout invokes Run and returns the stdout. 143 | func Stdout(o *option.Option, args ...string) []byte { 144 | return Run(o, args...).Out.Contents() 145 | } 146 | 147 | // StdoutStr invokes Run and returns the output in string format. 148 | func StdoutStr(o *option.Option, args ...string) string { 149 | return strings.TrimSpace(string(Stdout(o, args...))) 150 | } 151 | 152 | // StdoutAsLines invokes Run and returns the stdout as lines. 153 | func StdoutAsLines(o *option.Option, args ...string) []string { 154 | return toLines(Run(o, args...).Out) 155 | } 156 | 157 | // Stderr invokes Run and returns the stderr. 158 | func Stderr(o *option.Option, args ...string) []byte { 159 | return Run(o, args...).Err.Contents() 160 | } 161 | 162 | // StderrAsLines invokes Run and returns the stderr as lines. 163 | func StderrAsLines(o *option.Option, args ...string) []string { 164 | return toLines(Run(o, args...).Err) 165 | } 166 | 167 | // StderrStr invokes Run and returns the output in string format. 168 | func StderrStr(o *option.Option, args ...string) string { 169 | return string(Run(o, args...).Err.Contents()) 170 | } 171 | 172 | // RunWithoutWait starts a session without waiting for it to finish. 173 | func RunWithoutWait(o *option.Option, args ...string) *gexec.Session { 174 | return New(o, args...).WithoutWait().Run() 175 | } 176 | 177 | func toLines(r io.Reader) []string { 178 | var lines []string 179 | 180 | scanner := bufio.NewScanner(r) 181 | for scanner.Scan() { 182 | lines = append(lines, scanner.Text()) 183 | } 184 | gomega.Expect(scanner.Err()).ShouldNot(gomega.HaveOccurred()) 185 | 186 | return lines 187 | } 188 | -------------------------------------------------------------------------------- /command/get.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "github.com/runfinch/common-tests/option" 8 | ) 9 | 10 | // GetAllContainerIDs returns all container IDs. 11 | func GetAllContainerIDs(o *option.Option) []string { 12 | return StdoutAsLines(o, "ps", "--all", "--quiet", "--no-trunc") 13 | } 14 | 15 | // GetAllImageNames returns all image names. 16 | func GetAllImageNames(o *option.Option) []string { 17 | return StdoutAsLines(o, "images", "--all", "--format", "{{.Repository}}:{{.Tag}}") 18 | } 19 | 20 | // GetAllVolumeNames returns all volume names. 21 | func GetAllVolumeNames(o *option.Option) []string { 22 | return StdoutAsLines(o, "volume", "ls", "--quiet") 23 | } 24 | 25 | // GetAllNetworkNames returns all network names. 26 | func GetAllNetworkNames(o *option.Option) []string { 27 | return StdoutAsLines(o, "network", "ls", "--format", "{{.Name}}") 28 | } 29 | 30 | // GetAllImageIDs returns all image IDs. 31 | func GetAllImageIDs(o *option.Option) []string { 32 | return StdoutAsLines(o, "images", "--all", "--quiet") 33 | } 34 | -------------------------------------------------------------------------------- /command/remove.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "github.com/onsi/ginkgo/v2" 8 | 9 | "github.com/runfinch/common-tests/option" 10 | ) 11 | 12 | // RemoveAll removes all containers and images in the testing environment specified by o. 13 | func RemoveAll(o *option.Option) { 14 | RemoveContainers(o) 15 | RemoveImages(o) 16 | RemoveVolumes(o) 17 | RemoveNetworks(o) 18 | } 19 | 20 | // RemoveContainers removes all containers in the testing environment specified by o. 21 | func RemoveContainers(o *option.Option) { 22 | allIDs := GetAllContainerIDs(o) 23 | var ids []string 24 | for _, id := range allIDs { 25 | if id != localRegistryContainerID { 26 | ids = append(ids, id) 27 | } 28 | } 29 | if len(ids) == 0 { 30 | ginkgo.GinkgoWriter.Println("No containers to be removed") 31 | return 32 | } 33 | 34 | args := append([]string{"rm", "--force"}, ids...) 35 | Run(o, args...) 36 | } 37 | 38 | // RemoveVolumes removes all unused local volumes in the testing environment specified by o. 39 | func RemoveVolumes(o *option.Option) { 40 | volumes := GetAllVolumeNames(o) 41 | if len(volumes) == 0 { 42 | ginkgo.GinkgoWriter.Println("No volumes to be removed") 43 | return 44 | } 45 | Run(o, "volume", "prune", "--force", "--all") 46 | } 47 | 48 | // RemoveImages removes all container images in the testing environment specified by o. 49 | func RemoveImages(o *option.Option) { 50 | allIDs := GetAllImageIDs(o) 51 | var ids []string 52 | for _, id := range allIDs { 53 | if id != localRegistryImageID { 54 | ids = append(ids, id) 55 | } 56 | } 57 | if removedAllImages(ids) { 58 | return 59 | } 60 | args := append([]string{"rmi", "--force"}, ids...) 61 | Run(o, args...) 62 | 63 | allNames := GetAllImageNames(o) 64 | var names []string 65 | for _, name := range allNames { 66 | if name != localRegistryImageName { 67 | names = append(names, name) 68 | } 69 | } 70 | if removedAllImages(names) { 71 | return 72 | } 73 | args = append([]string{"rmi", "--force"}, names...) 74 | Run(o, args...) 75 | } 76 | 77 | // RemoveNetworks removes all networks in the testing environment specified by o. 78 | // TODO: use "network prune" after upgrading nerdctl to v0.23. 79 | func RemoveNetworks(o *option.Option) { 80 | defaultNetworks := []string{"bridge", "host", "none"} 81 | networks := GetAllNetworkNames(o) 82 | var customNetworks []string 83 | for _, n := range networks { 84 | if !contains(defaultNetworks, n) { 85 | customNetworks = append(customNetworks, n) 86 | } 87 | } 88 | 89 | if len(customNetworks) == 0 { 90 | ginkgo.GinkgoWriter.Println("No networks to be removed") 91 | return 92 | } 93 | 94 | args := append([]string{"network", "rm"}, customNetworks...) 95 | Run(o, args...) 96 | } 97 | 98 | func contains(strs []string, target string) bool { 99 | for _, str := range strs { 100 | if str == target { 101 | return true 102 | } 103 | } 104 | return false 105 | } 106 | 107 | func removedAllImages(images []string) bool { 108 | if len(images) == 0 { 109 | ginkgo.GinkgoWriter.Println("No images to be removed") 110 | return true 111 | } 112 | return false 113 | } 114 | -------------------------------------------------------------------------------- /command/set.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package command 5 | 6 | var ( 7 | localRegistryImageID string 8 | localRegistryContainerID string 9 | localRegistryImageName string 10 | ) 11 | 12 | // SetLocalRegistryContainerID sets the ID for the local registry. Usually you don't need to invoke this function yourself. 13 | // For more details, see tests.SetupLocalRegistry. 14 | func SetLocalRegistryContainerID(id string) { 15 | localRegistryContainerID = id 16 | } 17 | 18 | // SetLocalRegistryImageID sets the ID for local registry image. Usually you don't need to invoke this function yourself. 19 | // For more details, see tests.SetupLocalRegistry. 20 | func SetLocalRegistryImageID(id string) { 21 | localRegistryImageID = id 22 | } 23 | 24 | // SetLocalRegistryImageName sets the local registry image name. Usually you don't need to invoke this function yourself. 25 | // For more details, see tests.SetupLocalRegistry. 26 | func SetLocalRegistryImageName(name string) { 27 | localRegistryImageName = name 28 | } 29 | -------------------------------------------------------------------------------- /copyright_header: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- /fenv/get.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package fenv Package path implements utility routines for working with environment variables 5 | package fenv 6 | 7 | import ( 8 | "os" 9 | ) 10 | 11 | // GetEnv retrieves the value of an environment variable. 12 | // It returns an empty string if the variable is not set. 13 | func GetEnv(key string) string { 14 | return os.Getenv(key) 15 | } 16 | -------------------------------------------------------------------------------- /ffs/create.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package ffs contains functions that manipulate file system 5 | package ffs 6 | 7 | import ( 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/onsi/gomega" 12 | ) 13 | 14 | // CreateBuildContext creates a directory which contains a Dockerfile with the specified content and returns the path to the directory. 15 | // It is the caller's responsibility to remove the directory when it is no longer needed. 16 | func CreateBuildContext(dockerfile string) string { 17 | return filepath.Dir(CreateTempFile("Dockerfile", dockerfile)) 18 | } 19 | 20 | // WriteFile writs data to the file specified by name. The file will be created if not existing. 21 | func WriteFile(name string, data string) { 22 | err := os.WriteFile(name, []byte(data), 0o644) 23 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 24 | } 25 | 26 | // CreateTarFilePath creates a directory and a tar file path appended to the directory and returns the tar file path. 27 | // It is the caller's responsibility to remove the directory when it is no longer needed. 28 | // TODO: replace with CreateTempDir. 29 | func CreateTarFilePath() string { 30 | tempDir := CreateTempDir("finch-test-save") 31 | tarFilePath := filepath.Join(tempDir, "test.tar") 32 | return tarFilePath 33 | } 34 | 35 | // CreateComposeYmlContext creates a temp directory along with a docker-compose.yml file. 36 | // It is the caller's responsibility to remove the directory when it is no longer needed. 37 | func CreateComposeYmlContext(composeYmlContent string) (string, string) { 38 | composeFileName := "docker-compose.yml" 39 | tempDir := CreateTempDir("finch-compose") 40 | composeFilePath := filepath.Join(tempDir, composeFileName) 41 | WriteFile(composeFilePath, composeYmlContent) 42 | return tempDir, composeFilePath 43 | } 44 | 45 | // CreateTempFile creates a temp directory which contains a temp file and returns the path to the temp file. 46 | // It is the caller's responsibility to remove the directory when it is no longer needed. 47 | func CreateTempFile(filename string, content string) string { 48 | tempDir := CreateTempDir("finch-test") 49 | filepath := filepath.Join(tempDir, filename) 50 | WriteFile(filepath, content) 51 | return filepath 52 | } 53 | 54 | // CreateTempDir creates a temp directory and returns the path of the created directory. 55 | // It is the caller's responsibility to remove the directory when it is no longer needed. 56 | func CreateTempDir(directoryPrefix string) string { 57 | homeDir, err := os.UserHomeDir() 58 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 59 | tempDir, err := os.MkdirTemp(homeDir, directoryPrefix) 60 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 61 | return tempDir 62 | } 63 | 64 | // CreateNestedDir creates a nested directory and returns the path of the created directory. 65 | // It is the caller's responsibility to remove the directory when it is no longer needed. 66 | func CreateNestedDir(dirPath string) string { 67 | homeDir, err := os.UserHomeDir() 68 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 69 | fullPath := filepath.Join(homeDir, dirPath) 70 | err = os.MkdirAll(fullPath, 0o740) 71 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 72 | return fullPath 73 | } 74 | 75 | // CreateFilePathInHome creates a filepath and returns the path. 76 | func CreateFilePathInHome(dirPath string) string { 77 | homeDir, err := os.UserHomeDir() 78 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 79 | fullPath := filepath.Join(homeDir, dirPath) 80 | return fullPath 81 | } 82 | 83 | // DeleteDirectory deletes the directory including nested directories. 84 | func DeleteDirectory(directoryPath string) { 85 | err := os.RemoveAll(directoryPath) 86 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 87 | } 88 | -------------------------------------------------------------------------------- /ffs/get.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package ffs 5 | 6 | import ( 7 | "os" 8 | ) 9 | 10 | // CheckIfFileExists checks if a file exists in the filesystem. 11 | func CheckIfFileExists(fileName string) bool { 12 | if _, err := os.Stat(fileName); err == nil { 13 | return true 14 | } 15 | return false 16 | } 17 | -------------------------------------------------------------------------------- /fnet/dial.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package fnet 5 | 6 | import ( 7 | "net/http" 8 | "time" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | ) 13 | 14 | // HTTPGetAndAssert sends an HTTP GET request to the specified URL, asserts the response status code against want, and closes the response body. 15 | func HTTPGetAndAssert(url string, want int, maxRetry int, retryInterval time.Duration) { 16 | var ( 17 | err error 18 | resp *http.Response 19 | ) 20 | client := http.Client{ 21 | Timeout: 5 * time.Second, 22 | } 23 | 24 | for i := 0; i < maxRetry; i++ { 25 | // #nosec G107 // it does not matter if url is not a constant for testing. 26 | resp, err = client.Get(url) 27 | if err != nil { 28 | time.Sleep(retryInterval) 29 | continue 30 | } 31 | defer func() { gomega.Expect(resp.Body.Close()).To(gomega.Succeed()) }() 32 | gomega.Expect(resp.StatusCode).To(gomega.Equal(want)) 33 | return 34 | } 35 | ginkgo.Fail(err.Error()) 36 | } 37 | -------------------------------------------------------------------------------- /fnet/get.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package fnet contains functions for network operations. 5 | package fnet 6 | 7 | import ( 8 | "net" 9 | 10 | "github.com/onsi/gomega" 11 | ) 12 | 13 | // GetFreePort returns a free port. 14 | func GetFreePort() int { 15 | l, err := net.Listen("tcp", "localhost:0") 16 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 17 | defer func() { 18 | gomega.Expect(l.Close()).To(gomega.Succeed()) 19 | }() 20 | 21 | tcpAddr, ok := l.Addr().(*net.TCPAddr) 22 | gomega.Expect(ok).To(gomega.BeTrue()) 23 | return tcpAddr.Port 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/runfinch/common-tests 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/onsi/ginkgo/v2 v2.23.4 7 | github.com/onsi/gomega v1.37.0 8 | ) 9 | 10 | require ( 11 | github.com/go-logr/logr v1.4.2 // indirect 12 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 13 | github.com/google/go-cmp v0.7.0 // indirect 14 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 15 | go.uber.org/automaxprocs v1.6.0 // indirect 16 | golang.org/x/net v0.38.0 // indirect 17 | golang.org/x/sys v0.32.0 // indirect 18 | golang.org/x/text v0.23.0 // indirect 19 | golang.org/x/tools v0.31.0 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 4 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 5 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 6 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 7 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 8 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 9 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 10 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 11 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 12 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= 16 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= 17 | github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 18 | github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 22 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 23 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 24 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 25 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 26 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 27 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 28 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 29 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 30 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 31 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 32 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 33 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 34 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 35 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 36 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 39 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 41 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | -------------------------------------------------------------------------------- /option/modifier.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package option 5 | 6 | // Modifier modifies an Option. 7 | // 8 | // It is not intended to be implemented by code outside this package. 9 | // It is created to provide the flexibility to pass more things to option.New in the future 10 | // without the need to update its signature. 11 | type Modifier interface { 12 | modify(*Option) 13 | } 14 | 15 | type funcModifier struct { 16 | f func(*Option) 17 | } 18 | 19 | func newFuncModifier(f func(*Option)) *funcModifier { 20 | return &funcModifier{f: f} 21 | } 22 | 23 | func (fm *funcModifier) modify(o *Option) { 24 | fm.f(o) 25 | } 26 | 27 | // Env specifies the environment variables to be used during testing. It has the same format as Cmd.Env in os/exec. 28 | func Env(env []string) Modifier { 29 | return newFuncModifier(func(o *Option) { 30 | o.env = env 31 | }) 32 | } 33 | 34 | // WithNoEnvironmentVariablePassthrough denotes the option does not support environment variable passthrough. 35 | // 36 | // This is useful for disabling tests that require this feature. 37 | func WithNoEnvironmentVariablePassthrough() Modifier { 38 | return newFuncModifier(func(o *Option) { 39 | delete(o.features, environmentVariablePassthrough) 40 | }) 41 | } 42 | 43 | // WithNerdctlVersion denotes the underlying nerdctl version. 44 | // 45 | // This is useful for tests whose expectations change based on 46 | // the underlying nerdctl version. 47 | func WithNerdctlVersion(version string) Modifier { 48 | return newFuncModifier(func(o *Option) { 49 | o.features[nerdctlVersion] = version 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /option/nerdctl.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package option 5 | 6 | import "regexp" 7 | 8 | const ( 9 | nerdctl1xx = "1.x.x" 10 | nerdctl2xx = "2.x.x" 11 | defaultNerdctlVersion = nerdctl2xx 12 | ) 13 | 14 | var ( 15 | nerdctl1xxRegex = regexp.MustCompile(`^1\.[x0-9]+\.[x0-9]+`) 16 | nerdctl2xxRegex = regexp.MustCompile(`^2\.[x0-9]+\.[x0-9]+`) 17 | ) 18 | 19 | func isNerdctl1xx(version string) bool { 20 | return nerdctl1xxRegex.MatchString(version) 21 | } 22 | 23 | func isNerdctl2xx(version string) bool { 24 | return nerdctl2xxRegex.MatchString(version) 25 | } 26 | -------------------------------------------------------------------------------- /option/option.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package option customizes how tests are run. 5 | package option 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | ) 14 | 15 | type feature int 16 | 17 | const ( 18 | environmentVariablePassthrough feature = iota 19 | nerdctlVersion feature = iota 20 | ) 21 | 22 | // Option customizes how tests are run. 23 | // 24 | // If a testing function needs special customizations other than the ones specified in Option, 25 | // we can use composition to extend it. 26 | // For example, to test login functionality, 27 | // we may create a struct named LoginOption that embeds Option and contains additional fields like Username and Password. 28 | type Option struct { 29 | subject []string 30 | env []string 31 | features map[feature]any 32 | } 33 | 34 | // New does some sanity checks on the arguments before initializing an Option. 35 | // 36 | // subject specifies the subject to be tested. 37 | // It is intentionally not designed as an (optional) Modifier because it must contain at least one element. 38 | // Essentially it is used as a prefix when invoking all the binaries during testing. 39 | // 40 | // For example, if subject is ["foo", "bar"], then to test pulling a image, the command name would be "foo", 41 | // and the command args would be something like ["bar", "pull", "alpine"]. 42 | func New(subject []string, modifiers ...Modifier) (*Option, error) { 43 | if len(subject) == 0 { 44 | return nil, errors.New("missing subject") 45 | } 46 | 47 | o := &Option{ 48 | subject: subject, 49 | features: map[feature]any{ 50 | environmentVariablePassthrough: true, 51 | nerdctlVersion: nerdctl2xx, 52 | }, 53 | } 54 | for _, modifier := range modifiers { 55 | modifier.modify(o) 56 | } 57 | 58 | return o, nil 59 | } 60 | 61 | // NewCmd creates a command using the stored option and the provided args. 62 | func (o *Option) NewCmd(args ...string) *exec.Cmd { 63 | cmdName := o.subject[0] 64 | cmdArgs := append(o.subject[1:], args...) //nolint:gocritic // appendAssign does not apply to our case. 65 | cmd := exec.Command(cmdName, cmdArgs...) //nolint:gosec // G204 is not an issue because cmdName is fully controlled by the user. 66 | cmd.Env = append(os.Environ(), o.env...) 67 | return cmd 68 | } 69 | 70 | // UpdateEnv updates the environment variable for the key name of the input. 71 | func (o *Option) UpdateEnv(envKey, envValue string) { 72 | env := fmt.Sprintf("%s=%s", envKey, envValue) 73 | if i, exists := containsEnv(o.env, envKey); exists { 74 | o.env[i] = env 75 | } else { 76 | o.env = append(o.env, env) 77 | } 78 | } 79 | 80 | // DeleteEnv deletes the environment variable for the key name of the input. 81 | func (o *Option) DeleteEnv(envKey string) { 82 | if i, exists := containsEnv(o.env, envKey); exists { 83 | o.env = append(o.env[:i], o.env[i+1:]...) 84 | } 85 | } 86 | 87 | // containsEnv determines whether an environment variable exists. 88 | func containsEnv(envs []string, targetEnvKey string) (int, bool) { 89 | for i, env := range envs { 90 | if strings.Split(env, "=")[0] == targetEnvKey { 91 | return i, true 92 | } 93 | } 94 | 95 | return -1, false 96 | } 97 | 98 | // SupportsEnvVarPassthrough is used by tests to check if the option 99 | // supports [feature.environmentVariablePassthrough]. 100 | func (o *Option) SupportsEnvVarPassthrough() bool { 101 | if value, exists := o.features[environmentVariablePassthrough]; exists { 102 | if boolValue, ok := value.(bool); ok { 103 | return boolValue 104 | } 105 | } 106 | return false 107 | } 108 | 109 | // IsNerdctlV1 is used by tests to check if the option supports [feature.nerdctlVersion] == nerdctl1xx. 110 | func (o *Option) IsNerdctlV1() bool { 111 | return o.isNerdctlVersion(isNerdctl1xx) 112 | } 113 | 114 | // IsNerdctlV2 is used by tests to check if the option supports [feature.nerdctlVersion] == nerdctl2xx. 115 | func (o *Option) IsNerdctlV2() bool { 116 | return o.isNerdctlVersion(isNerdctl2xx) 117 | } 118 | 119 | func (o *Option) isNerdctlVersion(cmp func(string) bool) bool { 120 | var version string 121 | 122 | if value, exists := o.features[nerdctlVersion]; !exists { 123 | version = defaultNerdctlVersion 124 | } else if value, ok := value.(string); ok { 125 | version = value 126 | } 127 | 128 | return cmp(version) 129 | } 130 | -------------------------------------------------------------------------------- /option/option_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package option 5 | 6 | import "testing" 7 | 8 | func TestSupportsEnvVarPassthrough(t *testing.T) { 9 | t.Parallel() 10 | 11 | tests := []struct { 12 | name string 13 | mods []Modifier 14 | assert func(*testing.T, *Option) 15 | }{ 16 | { 17 | name: "IsEnvVarPassthroughByDefault", 18 | mods: []Modifier{}, 19 | assert: func(t *testing.T, uut *Option) { 20 | if !uut.SupportsEnvVarPassthrough() { 21 | t.Fatal("expected SupportsEnvVarPassthrough to be true") 22 | } 23 | }, 24 | }, 25 | { 26 | name: "IsNotEnvVarPassthrough", 27 | mods: []Modifier{ 28 | WithNoEnvironmentVariablePassthrough(), 29 | }, 30 | assert: func(t *testing.T, uut *Option) { 31 | if uut.SupportsEnvVarPassthrough() { 32 | t.Fatal("expected SupportsEnvVarPassthrough to be false") 33 | } 34 | }, 35 | }, 36 | } 37 | 38 | for _, test := range tests { 39 | t.Run(test.name, func(t *testing.T) { 40 | t.Parallel() 41 | 42 | uut, err := New([]string{"nerdctl"}, test.mods...) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | test.assert(t, uut) 48 | }) 49 | } 50 | } 51 | 52 | func TestNerdctlVersion(t *testing.T) { 53 | t.Parallel() 54 | 55 | tests := []struct { 56 | name string 57 | mods []Modifier 58 | assert func(*testing.T, *Option) 59 | }{ 60 | { 61 | name: "IsNerdctlV2ByDefault", 62 | mods: []Modifier{}, 63 | assert: func(t *testing.T, uut *Option) { 64 | if !uut.IsNerdctlV2() { 65 | t.Fatal("expected IsNerdctlV2 to be true") 66 | } 67 | }, 68 | }, 69 | { 70 | name: "IsNerdctlV1", 71 | mods: []Modifier{ 72 | WithNerdctlVersion("1.7.7"), 73 | }, 74 | assert: func(t *testing.T, uut *Option) { 75 | if !uut.IsNerdctlV1() { 76 | t.Fatal("expected IsNerdctlV1 to be true") 77 | } 78 | }, 79 | }, 80 | { 81 | name: "IsNerdctlV2", 82 | mods: []Modifier{ 83 | WithNerdctlVersion("2.0.2"), 84 | }, 85 | assert: func(t *testing.T, uut *Option) { 86 | if !uut.IsNerdctlV2() { 87 | t.Fatal("expected IsNerdctlV2 to be true") 88 | } 89 | }, 90 | }, 91 | { 92 | name: "IsPatchedNerdctlV2", 93 | mods: []Modifier{ 94 | WithNerdctlVersion("2.0.2.m"), 95 | }, 96 | assert: func(t *testing.T, uut *Option) { 97 | if !uut.IsNerdctlV2() { 98 | t.Fatal("expected IsNerdctlV2 to be true") 99 | } 100 | }, 101 | }, 102 | } 103 | 104 | for _, test := range tests { 105 | t.Run(test.name, func(t *testing.T) { 106 | t.Parallel() 107 | 108 | uut, err := New([]string{"nerdctl"}, test.mods...) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | test.assert(t, uut) 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "release-type": "go", 4 | "bump-minor-pre-major": true, 5 | "changelog-sections": [ 6 | { 7 | "type": "build", 8 | "section": "Build System or External Dependencies", 9 | "hidden": false 10 | }, 11 | { 12 | "type": "exp", 13 | "section": "Experimental", 14 | "hidden": false 15 | }, 16 | { 17 | "type": "feat", 18 | "section": "Features", 19 | "hidden": false 20 | }, 21 | { 22 | "type": "fix", 23 | "section": "Bug Fixes", 24 | "hidden": false 25 | }, 26 | { 27 | "type": "revert", 28 | "section": "Reverts", 29 | "hidden": false 30 | }, 31 | { 32 | "type": "chore", 33 | "section": "Miscellaneous Chores", 34 | "hidden": true 35 | }, 36 | { 37 | "type": "docs", 38 | "section": "Documentation", 39 | "hidden": true 40 | }, 41 | { 42 | "type": "refactor", 43 | "section": "Code Refactoring", 44 | "hidden": true 45 | }, 46 | { 47 | "type": "test", 48 | "section": "Tests", 49 | "hidden": true 50 | }, 51 | { 52 | "type": "ci", 53 | "section": "Continuous Integration", 54 | "hidden": true 55 | } 56 | ], 57 | "packages": { 58 | ".": {} 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /run/run_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package run 5 | 6 | import ( 7 | "flag" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/onsi/ginkgo/v2" 12 | "github.com/onsi/gomega" 13 | 14 | "github.com/runfinch/common-tests/option" 15 | "github.com/runfinch/common-tests/tests" 16 | ) 17 | 18 | // From https://pkg.go.dev/testing#hdr-Main: 19 | // "Command line flags are always parsed by the time test or benchmark functions run." 20 | // As a result, we need to define the custom flags below as global variables so that 21 | // when `flag.Parse` is invoked by the `testing` package, they can also be parsed. 22 | var subject = flag.String("subject", "", "the subject to be tested, potentially containing spaces") 23 | 24 | //nolint:paralleltest // TestRun is like TestMain for the e2e tests. 25 | func TestRun(t *testing.T) { 26 | o, err := option.New(strings.Split(*subject, " ")) 27 | if err != nil { 28 | t.Fatalf("failed to initialize a testing option: %v", err) 29 | } 30 | 31 | ginkgo.SynchronizedBeforeSuite(func() []byte { 32 | tests.SetupLocalRegistry(o) 33 | return nil 34 | }, func(_ []byte) {}) 35 | 36 | ginkgo.SynchronizedAfterSuite(func() { 37 | tests.CleanupLocalRegistry(o) 38 | }, func() {}) 39 | 40 | const description = "Finch Shared E2E Tests" 41 | const defaultHostGatewayIP = "192.168.5.2" 42 | ginkgo.Describe(description, func() { 43 | // Every test should be listed here. 44 | // TODO: add tests for "system prune" and "network prune" after upgrading nerdctl to v0.23 45 | tests.Pull(o) 46 | tests.Rm(o) 47 | tests.Rmi(o) 48 | tests.Run(&tests.RunOption{BaseOpt: o, CGMode: tests.Unified, DefaultHostGatewayIP: defaultHostGatewayIP}) 49 | tests.Start(o) 50 | tests.Stop(o) 51 | tests.Cp(o) 52 | tests.Tag(o) 53 | tests.Save(o) 54 | tests.Load(o) 55 | tests.Build(o) 56 | tests.Push(o) 57 | tests.Images(o) 58 | tests.ComposeBuild(o) 59 | tests.ComposeDown(o) 60 | tests.ComposeKill(o) 61 | tests.ComposePs(o) 62 | tests.ComposePull(o) 63 | tests.ComposeLogs(o) 64 | tests.Create(o) 65 | tests.Port(o) 66 | tests.Kill(o) 67 | tests.Restart(o) 68 | tests.Stats(o) 69 | tests.BuilderPrune(o) 70 | tests.Exec(o) 71 | tests.Logs(o) 72 | tests.Login(o) 73 | tests.Logout(o) 74 | tests.VolumeCreate(o) 75 | tests.VolumeInspect(o) 76 | tests.VolumeLs(o) 77 | tests.VolumeRm(o) 78 | tests.VolumePrune(o) 79 | tests.ImageHistory(o) 80 | tests.ImageInspect(o) 81 | tests.ImagePrune(o) 82 | tests.Info(o) 83 | tests.Events(o) 84 | tests.Inspect(o) 85 | tests.NetworkCreate(o) 86 | tests.NetworkInspect(o) 87 | tests.NetworkLs(o) 88 | tests.NetworkRm(o) 89 | tests.Ps(o) 90 | }) 91 | 92 | gomega.RegisterFailHandler(ginkgo.Fail) 93 | ginkgo.RunSpecs(t, description) 94 | } 95 | -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | # https://staticcheck.io/docs/configuration/#configuration-format 2 | checks = ["inherit", "ST1000", "ST1016", "ST1020", "ST1021", "ST1022"] 3 | -------------------------------------------------------------------------------- /tests/build.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | 13 | "github.com/onsi/ginkgo/v2" 14 | "github.com/onsi/gomega" 15 | 16 | "github.com/runfinch/common-tests/command" 17 | "github.com/runfinch/common-tests/ffs" 18 | "github.com/runfinch/common-tests/option" 19 | ) 20 | 21 | // Build command building an image. 22 | // 23 | // TODO: --no-cache, syntax check for docker files 24 | // --no-cache flag is added to tests asserting the output from `RUN` command. 25 | // [Discussion]: https://github.com/runfinch/common-tests/pull/4#discussion_r971338825 26 | func Build(o *option.Option) { 27 | ginkgo.Describe("Build container image", func() { 28 | ginkgo.Context("Build container image using default image", func() { 29 | var buildContext string 30 | ginkgo.BeforeEach(func() { 31 | buildContext = ffs.CreateBuildContext(fmt.Sprintf(`FROM %s 32 | CMD ["echo", "finch-test-dummy-output"] 33 | `, localImages[defaultImage])) 34 | ginkgo.DeferCleanup(os.RemoveAll, buildContext) 35 | command.RemoveAll(o) 36 | }) 37 | 38 | ginkgo.AfterEach(func() { 39 | command.RemoveAll(o) 40 | }) 41 | 42 | for _, tag := range []string{"-t", "--tag"} { 43 | ginkgo.It(fmt.Sprintf("build basic alpine image with %s option", tag), func() { 44 | command.Run(o, "build", tag, testImageName, buildContext) 45 | imageShouldExist(o, testImageName) 46 | }) 47 | } 48 | 49 | ginkgo.Context("Build an image with file option", func() { 50 | var dockerFilePath string 51 | ginkgo.BeforeEach(func() { 52 | dockerFilePath = filepath.Join(buildContext, "AnotherDockerfile") 53 | ffs.WriteFile(dockerFilePath, fmt.Sprintf(`FROM %s 54 | RUN ["echo", "built from AnotherDockerfile"] 55 | `, localImages[defaultImage])) 56 | }) 57 | 58 | for _, file := range []string{"-f", "--file"} { 59 | ginkgo.It(fmt.Sprintf("build an image with %s option", file), func() { 60 | stdErr := command.Stderr(o, "build", "--no-cache", file, dockerFilePath, buildContext) 61 | gomega.Expect(stdErr).Should(gomega.ContainSubstring("built from AnotherDockerfile")) 62 | }) 63 | } 64 | }) 65 | 66 | ginkgo.It("build image with --secret option", func() { 67 | containerWithSecret := fmt.Sprintf(`FROM %s 68 | RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret 69 | `, localImages[defaultImage]) 70 | dockerFilePath := filepath.Join(buildContext, "Dockerfile.with-secret") 71 | ffs.WriteFile(dockerFilePath, containerWithSecret) 72 | secretFile := filepath.Join(buildContext, "secret.txt") 73 | ffs.WriteFile(secretFile, "somesecret") 74 | secret := fmt.Sprintf("id=mysecret,src=%s", secretFile) 75 | stdErr := command.Stderr(o, "build", "--progress=plain", "--no-cache", "-f", dockerFilePath, "--secret", secret, buildContext) 76 | gomega.Expect(stdErr).Should(gomega.ContainSubstring("somesecret")) 77 | }) 78 | 79 | ginkgo.It("build image with --target option", func() { 80 | containerWithTarget := fmt.Sprintf(`FROM %s AS build_env 81 | RUN echo output from build_env 82 | FROM %s AS prod_env 83 | RUN echo "output from prod_env 84 | `, localImages[defaultImage], localImages[defaultImage]) 85 | dockerFilePath := filepath.Join(buildContext, "Dockerfile.with-target") 86 | ffs.WriteFile(dockerFilePath, containerWithTarget) 87 | stdEr := command.Stderr(o, "build", "--progress=plain", "--no-cache", 88 | "-f", dockerFilePath, "--target", "build_env", buildContext) 89 | gomega.Expect(stdEr).Should(gomega.ContainSubstring("output from build_env")) 90 | gomega.Expect(stdEr).ShouldNot(gomega.ContainSubstring("output from prod_env")) 91 | }) 92 | 93 | // "--output=type=docker" is intentional for the imageId to show up 94 | ginkgo.It("build image with --quiet option", func() { 95 | commandOut := command.StdoutStr(o, "build", "--output=type=docker", "--quiet", buildContext) 96 | gomega.Expect(len(strings.Split(commandOut, "\n"))).To(gomega.Equal(1)) 97 | }) 98 | 99 | ginkgo.It("build image with --build-arg option", func() { 100 | containerWithBuildArg := "ARG VERSION=latest \n FROM public.ecr.aws/docker/library/alpine:${VERSION}" 101 | dockerFilePath := filepath.Join(buildContext, "Dockerfile.with-build-arg") 102 | ffs.WriteFile(dockerFilePath, containerWithBuildArg) 103 | stdErr := command.Stderr(o, "build", "-f", dockerFilePath, "--no-cache", "--progress=plain", 104 | "--build-arg", "VERSION=3.13", buildContext) 105 | gomega.Expect(stdErr).Should(gomega.ContainSubstring("public.ecr.aws/docker/library/alpine:3.13")) 106 | }) 107 | 108 | ginkgo.It("build image with --progress=plain", func() { 109 | dockerFile := fmt.Sprintf(`FROM %s 110 | RUN echo "progress flag set:$((1 + 1))" 111 | `, localImages[defaultImage]) 112 | dockerFilePath := filepath.Join(buildContext, "Dockerfile.progress") 113 | ffs.WriteFile(dockerFilePath, dockerFile) 114 | stdErr := command.Stderr(o, "build", "-f", dockerFilePath, "--no-cache", "--progress=plain", buildContext) 115 | gomega.Expect(stdErr).Should(gomega.ContainSubstring("progress flag set:2")) 116 | }) 117 | 118 | // TODO: Test if we can `import` the tar ball after `nerdctl import` is supported. 119 | ginkgo.It("build image with --output flag", func() { 120 | outputFilePath := filepath.Join(buildContext, "out.tar") 121 | dest := fmt.Sprintf("type=tar,dest=%s", outputFilePath) 122 | command.Run(o, "build", "-t", "output:tag", "--output", dest, buildContext) 123 | // When --output flag is enabled build artifacts exported as files and not as a local image. 124 | imageShouldNotExist(o, "output:tag") 125 | gomega.Expect(ffs.CheckIfFileExists(outputFilePath)).To(gomega.Equal(true)) 126 | }) 127 | 128 | ginkgo.It("Build an image with --ssh option", func() { 129 | if runtime.GOOS == "windows" { 130 | ginkgo.Skip("non-functional on Windows, see https://github.com/runfinch/finch/issues/750") 131 | } 132 | containerWithSSH := fmt.Sprintf(`FROM %s 133 | RUN ["echo", "built from Dockerfile.with-ssh"] 134 | `, localImages[defaultImage]) 135 | dockerFilePath := filepath.Join(buildContext, "Dockerfile.with-ssh") 136 | ffs.WriteFile(dockerFilePath, containerWithSSH) 137 | stdErr := command.Stderr(o, "build", "--ssh", "default", "-f", dockerFilePath, buildContext) 138 | gomega.Expect(stdErr).Should(gomega.ContainSubstring("built from Dockerfile.with-ssh")) 139 | }) 140 | 141 | ginkgo.Context("Docker file syntax tests", func() { 142 | negativeTests := []struct { 143 | test string 144 | fileName string 145 | instructions string 146 | errorMessage string 147 | }{ 148 | { 149 | test: "Empty Dockerfile", 150 | fileName: "Dockerfile.Empty", 151 | instructions: "", 152 | errorMessage: "Dockerfile cannot be empty", 153 | }, 154 | { 155 | test: "Env no value", 156 | fileName: "Dockerfile.NoEnv", 157 | instructions: fmt.Sprintf(`FROM %s 158 | ENV PATH 159 | `, localImages[defaultImage]), 160 | errorMessage: "ENV must have two arguments", 161 | }, 162 | { 163 | test: "Only comments", 164 | fileName: "Dockerfile.OnlyComments", 165 | instructions: "# Hello\n# These are just comments", 166 | errorMessage: "file with no instructions", 167 | }, 168 | } 169 | 170 | for _, test := range negativeTests { 171 | ginkgo.It("should not successfully build a container", func() { 172 | dockerFilePath := filepath.Join(buildContext, test.fileName) 173 | ffs.WriteFile(dockerFilePath, test.instructions) 174 | stdErr := command.RunWithoutSuccessfulExit(o, "build", "-f", dockerFilePath, buildContext).Err.Contents() 175 | gomega.Expect(stdErr).Should(gomega.ContainSubstring(test.errorMessage)) 176 | }) 177 | } 178 | }) 179 | }) 180 | 181 | ginkgo.Context("Build container image using alpine image", func() { 182 | var buildContext string 183 | ginkgo.BeforeEach(func() { 184 | buildContext = ffs.CreateBuildContext(fmt.Sprintf(`FROM %s 185 | CMD ["echo", "finch-test-dummy-output"] 186 | `, alpineImage)) 187 | ginkgo.DeferCleanup(os.RemoveAll, buildContext) 188 | command.RemoveAll(o) 189 | }) 190 | 191 | ginkgo.AfterEach(func() { 192 | command.RemoveAll(o) 193 | }) 194 | // If SetupLocalRegistry is invoked before this test case, 195 | // then localImages[defaultImage] will point to the image in the local registry, 196 | // and there will be only one platform (i.e., the platform of the running machine) available for that image in the local registry. 197 | // As a result, to make this test case not flaky even when SetupLocalRegistry is used, 198 | // we need to pull alpineImage instead of localImages[defaultImage] 199 | // because we can be sure that the registry associated with the former provides the image with the platform specified below. 200 | ginkgo.It("build basic alpine image with --platform option", func() { 201 | command.Run(o, "build", "-t", testImageName, "--platform=amd64", buildContext) 202 | platform := command.StdoutStr(o, "images", testImageName, "--format", "{{.Platform}}") 203 | gomega.Expect(platform).Should(gomega.Equal("linux/amd64")) 204 | }) 205 | }) 206 | }) 207 | } 208 | -------------------------------------------------------------------------------- /tests/builder_prune.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | 12 | "github.com/runfinch/common-tests/command" 13 | "github.com/runfinch/common-tests/ffs" 14 | "github.com/runfinch/common-tests/option" 15 | ) 16 | 17 | // BuilderPrune tests the "builder prune" command that prunes the builder cache. 18 | func BuilderPrune(o *option.Option) { 19 | ginkgo.Describe("prune the builder cache", func() { 20 | var buildContext string 21 | ginkgo.BeforeEach(func() { 22 | buildContext = ffs.CreateBuildContext(fmt.Sprintf(`FROM %s 23 | CMD ["echo", "finch-test-dummy-output"] 24 | `, localImages[defaultImage])) 25 | ginkgo.DeferCleanup(os.RemoveAll, buildContext) 26 | command.RemoveAll(o) 27 | }) 28 | ginkgo.AfterEach(func() { 29 | command.RemoveAll(o) 30 | }) 31 | ginkgo.It("should prune the builder cache successfully", func() { 32 | command.Run(o, "build", "--output=type=docker", buildContext) 33 | // There is no interface to validate the current builder cache size. 34 | // To validate in Buildkit, run `buildctl du -v`. 35 | args := []string{"builder", "prune"} 36 | 37 | // TODO(macedonv): remove after nerdctlv2 is supported across all platforms. 38 | if o.IsNerdctlV2() { 39 | // Do not prompt for user response during automated testing. 40 | args = append(args, "--force") 41 | } 42 | 43 | command.Run(o, args...) 44 | }) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /tests/compose_build.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/onsi/ginkgo/v2" 12 | "github.com/onsi/gomega" 13 | 14 | "github.com/runfinch/common-tests/command" 15 | "github.com/runfinch/common-tests/ffs" 16 | "github.com/runfinch/common-tests/option" 17 | ) 18 | 19 | // ComposeBuild tests functionality of `compose build` command. 20 | func ComposeBuild(o *option.Option) { 21 | services := []string{"svc1_build_cmd", "svc2_build_cmd"} 22 | imageSuffix := []string{"alpine:latest", "-svc2_build_cmd:latest"} 23 | ginkgo.Describe("Compose build command", func() { 24 | var composeContext string 25 | var composeFilePath string 26 | ginkgo.BeforeEach(func() { 27 | command.RemoveAll(o) 28 | composeContext, composeFilePath = createComposeYmlForBuildCmd(services) 29 | ginkgo.DeferCleanup(os.RemoveAll, composeContext) 30 | }) 31 | 32 | ginkgo.AfterEach(func() { 33 | command.RemoveAll(o) 34 | }) 35 | 36 | ginkgo.It("should build services defined in the compose file", func() { 37 | command.Run(o, "compose", "build", "--file", composeFilePath) 38 | 39 | imageList := command.GetAllImageNames(o) 40 | gomega.Expect(imageList).Should(gomega.ContainElement(gomega.HaveSuffix(imageSuffix[0]))) 41 | gomega.Expect(imageList).Should(gomega.ContainElement(gomega.HaveSuffix(imageSuffix[1]))) 42 | // The built image should print 'Compose build test' when run. 43 | output := command.StdoutStr(o, "run", localImages[defaultImage]) 44 | gomega.Expect(output).Should(gomega.Equal("Compose build test")) 45 | }) 46 | 47 | ginkgo.It("should build services defined in the compose file specified by the COMPOSE_FILE environment variable", func() { 48 | if !o.SupportsEnvVarPassthrough() { 49 | ginkgo.Skip("Test requires option environment variable passthrough") 50 | } 51 | 52 | envKey := "COMPOSE_FILE" 53 | o.UpdateEnv(envKey, composeFilePath) 54 | 55 | command.Run(o, "compose", "build") 56 | 57 | imageList := command.GetAllImageNames(o) 58 | gomega.Expect(imageList).Should(gomega.ContainElement(gomega.HaveSuffix(imageSuffix[0]))) 59 | gomega.Expect(imageList).Should(gomega.ContainElement(gomega.HaveSuffix(imageSuffix[1]))) 60 | // The built image should print 'Compose build test' when run. 61 | output := command.StdoutStr(o, "run", localImages[defaultImage]) 62 | gomega.Expect(output).Should(gomega.Equal("Compose build test")) 63 | 64 | o.DeleteEnv(envKey) 65 | }) 66 | 67 | ginkgo.It("should output progress in plain text format", func() { 68 | composeBuildOutput := command.StderrStr(o, "compose", "build", "--progress", 69 | "plain", "--no-cache", "--file", composeFilePath) 70 | // The docker file contains following command. 71 | // RUN printf 'should only see the final answer when "--progress" is set to be "plain": %d\n' $(expr 1 + 1) 72 | // where the expression "$(expr 1 + 1)" will be evaluated to 2 only for "--progress plain" output. 73 | gomega.Expect(composeBuildOutput).Should(gomega.ContainSubstring( 74 | `should only see the final answer when '--progress' is set to be 'plain': 2`)) 75 | 76 | imageList := command.GetAllImageNames(o) 77 | gomega.Expect(imageList).Should(gomega.ContainElement(gomega.HaveSuffix(imageSuffix[0]))) 78 | gomega.Expect(imageList).Should(gomega.ContainElement(gomega.HaveSuffix(imageSuffix[1]))) 79 | }) 80 | 81 | ginkgo.It("should build services defined in the compose file with --build-args", func() { 82 | command.Run(o, "compose", "build", "--build-arg", 83 | `CMD_MSG=Compose build with --build-arg`, "--file", composeFilePath) 84 | output := command.StdoutStr(o, "compose", "up", "--file", composeFilePath) 85 | gomega.Expect(output).Should(gomega.ContainSubstring("Compose build with --build-arg")) 86 | command.Run(o, "compose", "down", "--file", composeFilePath) 87 | }) 88 | ginkgo.It("should build services defined in the compose file without --build-args", func() { 89 | command.Run(o, "compose", "build", "--file", composeFilePath) 90 | 91 | // The built image should print default value of the build-arg which is 'Compose build test'. 92 | output := command.StdoutStr(o, "compose", "up", "--file", composeFilePath) 93 | gomega.Expect(output).Should(gomega.ContainSubstring("Compose build test")) 94 | command.Run(o, "compose", "down", "--file", composeFilePath) 95 | }) 96 | // TODO: --no-cache does not have any effect on the build output. 97 | ginkgo.It("should build services defined in the compose file without using cache", func() { 98 | command.Run(o, "compose", "build", "--no-cache", "--file", composeFilePath) 99 | imageList := command.GetAllImageNames(o) 100 | gomega.Expect(imageList).Should(gomega.ContainElement(gomega.HaveSuffix(imageSuffix[0]))) 101 | gomega.Expect(imageList).Should(gomega.ContainElement(gomega.HaveSuffix(imageSuffix[1]))) 102 | }) 103 | // TODO: add functional test for --ipfs 104 | }) 105 | } 106 | 107 | func createComposeYmlForBuildCmd(serviceNames []string) (string, string) { 108 | gomega.Expect(serviceNames).Should(gomega.HaveLen(2)) 109 | 110 | dockerFileContent := fmt.Sprintf(` 111 | FROM %s 112 | ARG CMD_MSG="Compose build test" 113 | RUN printf "should only see the final answer when '--progress' is set to be 'plain': %%d\n" $(expr 1 + 1) 114 | ENV ENV_CMD_MSG=${CMD_MSG} 115 | CMD echo ${ENV_CMD_MSG} 116 | `, localImages[defaultImage]) 117 | 118 | composeYmlContent := fmt.Sprintf( 119 | ` 120 | services: 121 | %[1]s: 122 | build: 123 | context: . 124 | dockerfile: Dockerfile 125 | image: %[3]s 126 | %[2]s: 127 | build: 128 | context: . 129 | dockerfile: Dockerfile 130 | `, serviceNames[0], serviceNames[1], localImages[defaultImage]) 131 | 132 | composeDir, composeFilePath := ffs.CreateComposeYmlContext(composeYmlContent) 133 | ffs.WriteFile(filepath.Join(composeDir, "Dockerfile"), dockerFileContent) 134 | return composeDir, composeFilePath 135 | } 136 | -------------------------------------------------------------------------------- /tests/compose_down.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "regexp" 10 | "time" 11 | 12 | "github.com/onsi/ginkgo/v2" 13 | "github.com/onsi/gomega" 14 | 15 | "github.com/runfinch/common-tests/command" 16 | "github.com/runfinch/common-tests/ffs" 17 | "github.com/runfinch/common-tests/option" 18 | ) 19 | 20 | // ComposeDown tests functionality of `compose down` command. 21 | func ComposeDown(o *option.Option) { 22 | services := []string{"svc1_compose_down", "svc2_compose_down"} 23 | containerNames := []string{"container1_compose_down", "container2_compose_down"} 24 | 25 | ginkgo.Describe("Compose down command", func() { 26 | var composeContext string 27 | var composeFilePath string 28 | ginkgo.BeforeEach(func() { 29 | command.RemoveAll(o) 30 | composeContext, composeFilePath = createComposeYmlForDownCmd(services, containerNames) 31 | ginkgo.DeferCleanup(os.RemoveAll, composeContext) 32 | command.Run(o, "compose", "up", "-d", "--file", composeFilePath) 33 | containerShouldExist(o, containerNames...) 34 | }) 35 | 36 | ginkgo.AfterEach(func() { 37 | command.RemoveAll(o) 38 | }) 39 | ginkgo.It("should stop services defined in compose file without deleting volumes", func() { 40 | command.Run(o, "compose", "down", "--file", composeFilePath) 41 | // Container removing is asynchronous in compose down. 42 | // https://github.com/containerd/nerdctl/blob/242c6b92bcb6a6d1522104dc7206d2886b7e9cc8/pkg/composer/rm.go#L89. 43 | gomega.Eventually(func() error { 44 | return containerShouldNotExist(o, containerNames...) 45 | }, 10*time.Second, 5*time.Second).Should(gomega.BeNil()) 46 | volumeShouldExist(o, "compose_data_volume") 47 | }) 48 | 49 | for _, volumes := range []string{"-v", "--volumes"} { 50 | ginkgo.It(fmt.Sprintf("should stop compose services and delete volumes by specifying %s flag", volumes), func() { 51 | volumes := volumes 52 | output := command.StdoutStr(o, "compose", "down", volumes, "--file", composeFilePath) 53 | gomega.Eventually(func() error { 54 | return containerShouldNotExist(o, containerNames...) 55 | }, 10*time.Second, 5*time.Second).Should(gomega.BeNil()) 56 | if !isVolumeInUse(output) { 57 | volumeShouldNotExist(o, "compose_data_volume") 58 | } 59 | }) 60 | } 61 | }) 62 | } 63 | 64 | // sometimes nerdctl fails to delete the volume due to concurrent usage of the volume by the container. 65 | // For more details - https://github.com/runfinch/finch/issues/261 66 | func isVolumeInUse(output string) bool { 67 | if len(output) < 1 { 68 | return false 69 | } 70 | // Error msg is generated from nerdctl volume rm cmd. 71 | // see: https://github.com/containerd/nerdctl/blob/main/pkg/cmd/volume/rm.go#L52 72 | re := regexp.MustCompile(`volume.*in use`) 73 | return re.MatchString(output) 74 | } 75 | 76 | func createComposeYmlForDownCmd(serviceNames []string, containerNames []string) (string, string) { 77 | gomega.Expect(serviceNames).Should(gomega.HaveLen(2)) 78 | gomega.Expect(containerNames).Should(gomega.HaveLen(2)) 79 | 80 | // Service commands should have SIGTERM handlers so graceful shutdown is quick. 81 | composeYmlContent := fmt.Sprintf( 82 | ` 83 | services: 84 | %[1]s: 85 | image: "%[3]s" 86 | container_name: "%[4]s" 87 | command: | 88 | sh -c " 89 | trap 'echo shutting down; exit 0' SIGTERM 90 | sleep infinity & 91 | wait 92 | " 93 | volumes: 94 | - compose_data_volume:/usr/local/data 95 | %[2]s: 96 | image: "%[3]s" 97 | container_name: "%[5]s" 98 | command: | 99 | sh -c " 100 | trap 'echo shutting down; exit 0' SIGTERM 101 | sleep infinity & 102 | wait 103 | " 104 | volumes: 105 | - compose_data_volume:/usr/local/data 106 | volumes: 107 | compose_data_volume: 108 | `, serviceNames[0], serviceNames[1], localImages[defaultImage], containerNames[0], containerNames[1]) 109 | return ffs.CreateComposeYmlContext(composeYmlContent) 110 | } 111 | -------------------------------------------------------------------------------- /tests/compose_kill.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | 13 | "github.com/runfinch/common-tests/command" 14 | "github.com/runfinch/common-tests/ffs" 15 | "github.com/runfinch/common-tests/option" 16 | ) 17 | 18 | // ComposeKill tests functionality of `compose` command. 19 | func ComposeKill(o *option.Option) { 20 | services := []string{"svc1_compose_kill", "svc2_compose_kill"} 21 | containerNames := []string{"container1_compose_kill", "container2_compose_kill"} 22 | 23 | ginkgo.Describe("Compose kill command", func() { 24 | var composeContext string 25 | var composeFilePath string 26 | ginkgo.BeforeEach(func() { 27 | command.RemoveAll(o) 28 | composeContext, composeFilePath = createComposeYmlForKillCmd(services, containerNames) 29 | ginkgo.DeferCleanup(os.RemoveAll, composeContext) 30 | command.Run(o, "compose", "up", "-d", "--file", composeFilePath) 31 | 32 | containerShouldExist(o, containerNames...) 33 | }) 34 | 35 | ginkgo.AfterEach(func() { 36 | command.Run(o, "compose", "down", "--file", composeFilePath) 37 | command.RemoveAll(o) 38 | }) 39 | ginkgo.It("should kill all service", func() { 40 | command.Run(o, "compose", "kill", "--file", composeFilePath) 41 | containerShouldNotBeRunning(o, containerNames...) 42 | }) 43 | 44 | for _, signal := range []string{"-s", "--signal"} { 45 | for _, term := range []string{"SIGTERM", "TERM"} { 46 | ginkgo.It(fmt.Sprintf("should send %s to containers when using %s", term, signal), func() { 47 | command.Run(o, "compose", "kill", signal, term, "--file", composeFilePath) 48 | containerShouldNotBeRunning(o, containerNames...) 49 | }) 50 | } 51 | } 52 | }) 53 | } 54 | 55 | func createComposeYmlForKillCmd(serviceNames []string, containerNames []string) (string, string) { 56 | gomega.Expect(serviceNames).Should(gomega.HaveLen(2)) 57 | gomega.Expect(containerNames).Should(gomega.HaveLen(2)) 58 | 59 | // Service commands implement SIGTERM handler to test compose kill 60 | // can send non-default signals. 61 | // 62 | // With PID=1, `sleep infinity` would only exit when receiving SIGKILL. 63 | // https://stackoverflow.com/questions/45148381/why-cant-i-ctrl-c-a-sleep-infinity-in-docker-when-it-runs-as-pid-1 64 | composeYmlContent := fmt.Sprintf( 65 | ` 66 | services: 67 | %[1]s: 68 | image: "%[3]s" 69 | container_name: "%[4]s" 70 | command: | 71 | sh -c " 72 | trap 'echo shutting down; exit 0' SIGTERM 73 | sleep infinity & 74 | wait 75 | " 76 | %[2]s: 77 | image: "%[3]s" 78 | container_name: "%[5]s" 79 | command: | 80 | sh -c " 81 | trap 'echo shutting down; exit 0' SIGTERM 82 | sleep infinity & 83 | wait 84 | " 85 | `, serviceNames[0], serviceNames[1], localImages[defaultImage], containerNames[0], containerNames[1]) 86 | return ffs.CreateComposeYmlContext(composeYmlContent) 87 | } 88 | -------------------------------------------------------------------------------- /tests/compose_logs.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | 13 | "github.com/runfinch/common-tests/command" 14 | "github.com/runfinch/common-tests/ffs" 15 | "github.com/runfinch/common-tests/option" 16 | ) 17 | 18 | // ComposeLogs tests functionality of `compose logs` command. 19 | func ComposeLogs(o *option.Option) { 20 | services := []string{"svc1_compose_logs", "svc2_compose_logs"} 21 | containerNames := []string{"container1_compose_logs", "container2_compose_logs"} 22 | 23 | ginkgo.Describe("Compose logs command", func() { 24 | var buildContext string 25 | var composeFilePath string 26 | var imageNames []string 27 | 28 | ginkgo.BeforeEach(func() { 29 | imageNames = []string{localImages[defaultImage], localImages[defaultImage]} 30 | command.RemoveAll(o) 31 | buildContext, composeFilePath = createComposeYmlForLogsCmd(services, imageNames, containerNames) 32 | ginkgo.DeferCleanup(os.RemoveAll, buildContext) 33 | command.Run(o, "compose", "up", "-d", "--file", composeFilePath) 34 | containerShouldExist(o, containerNames...) 35 | }) 36 | 37 | ginkgo.AfterEach(func() { 38 | command.Run(o, "compose", "down", "--file", composeFilePath) 39 | command.RemoveAll(o) 40 | }) 41 | ginkgo.It("should show the logs with prefixes", func() { 42 | output := command.StdoutAsLines(o, "compose", "logs", "--file", composeFilePath) 43 | // Log format: container_name |log_msg 44 | // example: container1_composelogs |hello from service 1 45 | gomega.Expect(output).Should(gomega.ContainElements( 46 | gomega.HavePrefix(containerNames[0]), 47 | gomega.HavePrefix(containerNames[1]))) 48 | }) 49 | ginkgo.It("should show the logs without prefixes", func() { 50 | output := command.StdoutAsLines(o, "compose", "logs", "--no-log-prefix", "--file", composeFilePath) 51 | // Log format: log_msg 52 | // example: hello from service 1 53 | gomega.Expect(output).ShouldNot(gomega.ContainElements( 54 | gomega.HavePrefix(containerNames[0]), 55 | gomega.HavePrefix(containerNames[1]))) 56 | }) 57 | ginkgo.It("should show the logs with no color", func() { 58 | output := command.StdoutStr(o, "compose", "logs", "--no-color", "--file", composeFilePath) 59 | // The asci color code has prefix \x1b[3 e.g. Black: \u001b[30m, Red: \u001b[31m 60 | gomega.Expect(output).ShouldNot(gomega.ContainSubstring("\x1b[3")) 61 | }) 62 | ginkgo.It("should show the last line of the logs", func() { 63 | output := command.StdoutAsLines(o, "compose", "logs", services[0], "--tail", "1", "--file", composeFilePath) 64 | gomega.Expect(output).Should(gomega.HaveLen(1)) 65 | }) 66 | 67 | for _, tFlag := range []string{"-t", "--timestamps"} { 68 | ginkgo.It(fmt.Sprintf("should show the logs with timestamp with no prefixes and no color [flag %s]", tFlag), func() { 69 | // Log format: YYYY-MM-DDThh:mm:ss.000000000Z LOG MSG 70 | timestampMatcher := gomega.MatchRegexp("^[0-9]{1,4}-[0-9]{1,2}-[0-9]{1,2}T[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}.*") 71 | output := command.StdoutAsLines(o, "compose", "logs", tFlag, "--no-log-prefix", "--no-color", "--file", composeFilePath) 72 | gomega.Expect(output).Should(gomega.HaveEach(timestampMatcher)) 73 | }) 74 | } 75 | }) 76 | } 77 | 78 | func createComposeYmlForLogsCmd(serviceNames []string, imageNames []string, containerNames []string) (string, string) { 79 | gomega.Expect(serviceNames).Should(gomega.HaveLen(2)) 80 | gomega.Expect(imageNames).Should(gomega.HaveLen(2)) 81 | gomega.Expect(containerNames).Should(gomega.HaveLen(2)) 82 | 83 | // Service commands should have SIGTERM handlers so graceful shutdown is quick. 84 | composeYmlContent := fmt.Sprintf( 85 | ` 86 | services: 87 | %[1]s: 88 | image: "%[3]s" 89 | container_name: "%[5]s" 90 | command: | 91 | sh -c " 92 | trap 'echo shutting down; exit 0' SIGTERM 93 | echo 'hello from service 2' 94 | echo 'again hello' 95 | sleep infinity & 96 | wait 97 | " 98 | %[2]s: 99 | image: "%[4]s" 100 | container_name: "%[6]s" 101 | command: | 102 | sh -c " 103 | trap 'echo shutting down; exit 0' SIGTERM 104 | echo 'hello from service 2' 105 | echo 'again hello' 106 | sleep infinity & 107 | wait 108 | " 109 | `, serviceNames[0], serviceNames[1], imageNames[0], imageNames[1], containerNames[0], containerNames[1]) 110 | return ffs.CreateComposeYmlContext(composeYmlContent) 111 | } 112 | -------------------------------------------------------------------------------- /tests/compose_ps.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | 13 | "github.com/runfinch/common-tests/command" 14 | "github.com/runfinch/common-tests/ffs" 15 | "github.com/runfinch/common-tests/option" 16 | ) 17 | 18 | // ComposePs tests functionality of `compose ps` command. 19 | func ComposePs(o *option.Option) { 20 | services := []string{"svc1_compose_ps", "svc2_compose_ps"} 21 | containerNames := []string{"container1_compose_ps", "container2_compose_ps"} 22 | 23 | ginkgo.Describe("Compose ps command", func() { 24 | var composeContext string 25 | var composeFilePath string 26 | var imageNames []string 27 | 28 | ginkgo.BeforeEach(func() { 29 | imageNames = []string{localImages[defaultImage], localImages[defaultImage]} 30 | command.RemoveAll(o) 31 | composeContext, composeFilePath = createComposeYmlForPsCmd(services, imageNames, containerNames) 32 | ginkgo.DeferCleanup(os.RemoveAll, composeContext) 33 | command.Run(o, "compose", "up", "-d", "--file", composeFilePath) 34 | containerShouldExist(o, containerNames...) 35 | }) 36 | 37 | ginkgo.AfterEach(func() { 38 | command.RemoveAll(o) 39 | }) 40 | ginkgo.It("should list services defined in compose file", func() { 41 | psOutput := command.StdoutAsLines(o, "compose", "ps", "--file", composeFilePath) 42 | gomega.Expect(psOutput).Should(gomega.ContainElements( 43 | gomega.ContainSubstring(services[0]), 44 | gomega.ContainSubstring(services[1]))) 45 | gomega.Expect(psOutput).Should(gomega.ContainElements( 46 | gomega.ContainSubstring(containerNames[0]), 47 | gomega.ContainSubstring(containerNames[1]))) 48 | gomega.Expect(psOutput).Should(gomega.ContainElement(gomega.ContainSubstring("sleep infinity"))) 49 | gomega.Expect(psOutput).Should(gomega.ContainElement(gomega.ContainSubstring("8080->8080/tcp"))) 50 | gomega.Expect(psOutput).Should(gomega.ContainElement(gomega.ContainSubstring("running"))) 51 | }) 52 | }) 53 | } 54 | 55 | func createComposeYmlForPsCmd(serviceNames []string, imageNames []string, containerNames []string) (string, string) { 56 | gomega.Expect(serviceNames).Should(gomega.HaveLen(2)) 57 | gomega.Expect(imageNames).Should(gomega.HaveLen(2)) 58 | gomega.Expect(containerNames).Should(gomega.HaveLen(2)) 59 | 60 | composeYmlContent := fmt.Sprintf( 61 | ` 62 | services: 63 | %[1]s: 64 | image: "%[3]s" 65 | container_name: "%[5]s" 66 | command: sleep infinity 67 | ports: 68 | - 8080:8080 69 | %[2]s: 70 | image: "%[4]s" 71 | container_name: "%[6]s" 72 | command: sleep infinity 73 | `, serviceNames[0], serviceNames[1], imageNames[0], imageNames[1], containerNames[0], containerNames[1]) 74 | return ffs.CreateComposeYmlContext(composeYmlContent) 75 | } 76 | -------------------------------------------------------------------------------- /tests/compose_pull.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | 13 | "github.com/runfinch/common-tests/command" 14 | "github.com/runfinch/common-tests/ffs" 15 | "github.com/runfinch/common-tests/option" 16 | ) 17 | 18 | // ComposePull tests functionality of `compose pull` command. 19 | func ComposePull(o *option.Option) { 20 | services := []string{"svc1_compose_pull", "svc2_compose_pull"} 21 | ginkgo.Describe("Compose pull command", func() { 22 | var composeContext string 23 | var composeFilePath string 24 | var imageNames []string 25 | 26 | ginkgo.BeforeEach(func() { 27 | imageNames = []string{localImages[defaultImage], localImages[olderAlpineImage]} 28 | command.RemoveAll(o) 29 | composeContext, composeFilePath = createComposeYmlForPullCmd(services, imageNames) 30 | ginkgo.DeferCleanup(os.RemoveAll, composeContext) 31 | }) 32 | 33 | ginkgo.AfterEach(func() { 34 | command.RemoveAll(o) 35 | }) 36 | ginkgo.It("should pull images for all services", func() { 37 | command.Run(o, "compose", "pull", "--file", composeFilePath) 38 | imageList := command.GetAllImageNames(o) 39 | gomega.Expect(imageList).Should(gomega.ContainElements(imageNames)) 40 | }) 41 | 42 | ginkgo.It("should pull the image for the first service only", func() { 43 | command.Run(o, "compose", "pull", services[0], "--file", composeFilePath) 44 | imageList := command.GetAllImageNames(o) 45 | gomega.Expect(imageList).Should(gomega.ContainElement(imageNames[0])) 46 | gomega.Expect(imageList).ShouldNot(gomega.ContainElement(imageNames[1])) 47 | }) 48 | 49 | for _, qFlag := range []string{"-q", "--quiet"} { 50 | ginkgo.It(fmt.Sprintf("should pull the images without printing progress information with %s flag", qFlag), func() { 51 | qFlag := qFlag 52 | command.Run(o, "compose", "pull", qFlag, "--file", composeFilePath) 53 | imageList := command.GetAllImageNames(o) 54 | gomega.Expect(imageList).Should(gomega.ContainElements(imageNames)) 55 | }) 56 | } 57 | }) 58 | } 59 | 60 | func createComposeYmlForPullCmd(serviceNames []string, imageNames []string) (string, string) { 61 | gomega.Expect(serviceNames).Should(gomega.HaveLen(2)) 62 | gomega.Expect(imageNames).Should(gomega.HaveLen(2)) 63 | 64 | composeYmlContent := fmt.Sprintf( 65 | ` 66 | services: 67 | %[1]s: 68 | image: "%[3]s" 69 | %[2]s: 70 | image: "%[4]s" 71 | `, serviceNames[0], serviceNames[1], imageNames[0], imageNames[1]) 72 | return ffs.CreateComposeYmlContext(composeYmlContent) 73 | } 74 | -------------------------------------------------------------------------------- /tests/cp.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/onsi/ginkgo/v2" 12 | "github.com/onsi/gomega" 13 | 14 | "github.com/runfinch/common-tests/command" 15 | "github.com/runfinch/common-tests/ffs" 16 | "github.com/runfinch/common-tests/option" 17 | ) 18 | 19 | // Cp tests `finch cp` command to copy files between container and host filesystems. 20 | func Cp(o *option.Option) { 21 | filename := "test-file" 22 | content := "test-content" 23 | containerFilepath := filepath.ToSlash(filepath.Join("/tmp", filename)) 24 | containerResource := fmt.Sprintf("%s:%s", testContainerName, containerFilepath) 25 | 26 | ginkgo.Describe("copy from container to host and vice versa", func() { 27 | ginkgo.BeforeEach(func() { 28 | command.RemoveAll(o) 29 | }) 30 | ginkgo.AfterEach(func() { 31 | command.RemoveAll(o) 32 | }) 33 | 34 | ginkgo.When("the container is running", func() { 35 | ginkgo.BeforeEach(func() { 36 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "sleep", "infinity") 37 | }) 38 | 39 | ginkgo.It("should be able to copy file from host to container", func() { 40 | path := ffs.CreateTempFile(filename, content) 41 | ginkgo.DeferCleanup(os.RemoveAll, filepath.Dir(path)) 42 | 43 | command.Run(o, "cp", path, containerResource) 44 | fileShouldExistInContainer(o, testContainerName, containerFilepath, content) 45 | }) 46 | 47 | ginkgo.It("should be able to copy file from container to host", func() { 48 | cmd := fmt.Sprintf("echo -n %s > %s", content, containerFilepath) 49 | command.Run(o, "exec", testContainerName, "sh", "-c", cmd) 50 | fileDir := ffs.CreateTempDir("finch-test") 51 | path := filepath.Join(fileDir, filename) 52 | ginkgo.DeferCleanup(os.RemoveAll, fileDir) 53 | 54 | command.Run(o, "cp", containerResource, path) 55 | fileShouldExist(path, content) 56 | }) 57 | 58 | for _, link := range []string{"-L", "--follow-link"} { 59 | ginkgo.It(fmt.Sprintf("with %s flag, should be able to copy file from host to container and follow symbolic link", 60 | link), func() { 61 | path := ffs.CreateTempFile(filename, content) 62 | fileDir := filepath.Dir(path) 63 | ginkgo.DeferCleanup(os.RemoveAll, fileDir) 64 | symlink := filepath.Join(fileDir, "symlink") 65 | err := os.Symlink(path, symlink) 66 | gomega.Expect(err).ToNot(gomega.HaveOccurred()) 67 | 68 | command.Run(o, "cp", link, symlink, containerResource) 69 | fileShouldExistInContainer(o, testContainerName, containerFilepath, content) 70 | }) 71 | 72 | ginkgo.It(fmt.Sprintf("with %s flag, should be able to copy file from container to host and follow symbolic link", 73 | link), func() { 74 | cmd := fmt.Sprintf("echo -n %s > %s", content, containerFilepath) 75 | command.Run(o, "exec", testContainerName, "sh", "-c", cmd) 76 | containerSymlink := filepath.Join("/tmp", "symlink") 77 | command.Run(o, "exec", testContainerName, "ln", "-s", containerFilepath, containerSymlink) 78 | fileDir := ffs.CreateTempDir("finch-test") 79 | path := filepath.Join(fileDir, filename) 80 | ginkgo.DeferCleanup(os.RemoveAll, fileDir) 81 | 82 | command.Run(o, "cp", link, fmt.Sprintf("%s:%s", testContainerName, containerSymlink), path) 83 | fileShouldExist(path, content) 84 | }) 85 | } 86 | 87 | ginkgo.It("should not be able to copy nonexistent file from host to container", func() { 88 | fileDir := ffs.CreateTempDir("finch-test") 89 | ginkgo.DeferCleanup(os.RemoveAll, fileDir) 90 | 91 | command.RunWithoutSuccessfulExit(o, "cp", filepath.Join(fileDir, filename), containerResource) 92 | fileShouldNotExistInContainer(o, testContainerName, containerFilepath) 93 | }) 94 | 95 | ginkgo.It("should not be able to copy nonexistent file from container to host", func() { 96 | fileDir := ffs.CreateTempDir("finch-test") 97 | path := filepath.Join(fileDir, filename) 98 | ginkgo.DeferCleanup(os.RemoveAll, fileDir) 99 | 100 | command.RunWithoutSuccessfulExit(o, "cp", containerResource, path) 101 | fileShouldNotExist(path) 102 | }) 103 | }) 104 | 105 | ginkgo.When("the container is not running", func() { 106 | ginkgo.It("should be able to copy file from host to container", func() { 107 | command.Run(o, "run", "--name", testContainerName, localImages[defaultImage], "sleep", "5") 108 | command.Run(o, "stop", testContainerName) 109 | path := ffs.CreateTempFile(filename, content) 110 | ginkgo.DeferCleanup(os.RemoveAll, filepath.Dir(path)) 111 | command.Run(o, "cp", path, containerResource) 112 | 113 | // Need to run container to cat file, can't exec in stopped container. 114 | // Start here will sleep 1s again so we can check file in container. 115 | command.Run(o, "container", "start", testContainerName) 116 | fileShouldExistInContainer(o, testContainerName, containerFilepath, content) 117 | }) 118 | 119 | ginkgo.It("should be able to copy file from container to host", func() { 120 | cmd := fmt.Sprintf("echo -n %s > %s", content, containerFilepath) 121 | command.Run(o, "run", "--name", testContainerName, localImages[defaultImage], "sh", "-c", cmd) 122 | fileDir := ffs.CreateTempDir("finch-test") 123 | path := filepath.Join(fileDir, filename) 124 | ginkgo.DeferCleanup(os.RemoveAll, fileDir) 125 | command.Run(o, "cp", containerResource, path) 126 | fileShouldExist(path, content) 127 | }) 128 | }) 129 | }) 130 | } 131 | -------------------------------------------------------------------------------- /tests/create.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega" 11 | 12 | "github.com/runfinch/common-tests/command" 13 | "github.com/runfinch/common-tests/option" 14 | ) 15 | 16 | // Create tests creating a container. 17 | func Create(o *option.Option) { 18 | ginkgo.Describe("create a container", func() { 19 | ginkgo.BeforeEach(func() { 20 | command.RemoveAll(o) 21 | }) 22 | ginkgo.AfterEach(func() { 23 | command.RemoveAll(o) 24 | }) 25 | 26 | ginkgo.It("should create a container and able to start the container", func() { 27 | command.Run(o, "create", "--name", testContainerName, localImages[defaultImage], "sleep", "infinity") 28 | status := command.StdoutStr(o, "ps", "-a", "--filter", fmt.Sprintf("name=%s", testContainerName), "--format", "{{.Status}}") 29 | gomega.Expect(status).Should(gomega.Equal("Created")) 30 | 31 | command.Run(o, "start", testContainerName) 32 | containerShouldBeRunning(o, testContainerName) 33 | }) 34 | 35 | ginkgo.It("should not create a container if the image doesn't exist", func() { 36 | command.RunWithoutSuccessfulExit(o, "create", nonexistentImageName) 37 | }) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /tests/events.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "strings" 8 | "time" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gexec" 13 | 14 | "github.com/runfinch/common-tests/command" 15 | "github.com/runfinch/common-tests/option" 16 | ) 17 | 18 | // Events tests "events" command that gets real time events from server, synonyms to "system events" command. 19 | func Events(o *option.Option) { 20 | ginkgo.Describe("get real time events from the server", func() { 21 | ginkgo.BeforeEach(func() { 22 | command.RemoveAll(o) 23 | }) 24 | ginkgo.AfterEach(func() { 25 | command.RemoveAll(o) 26 | }) 27 | 28 | ginkgo.It("should get real time events from command", func() { 29 | session := command.RunWithoutWait(o, "system", "events") 30 | defer session.Kill() 31 | 32 | // Give time for the system events to be running and monitoring before pull is called. 33 | time.Sleep(5 * time.Second) 34 | 35 | gomega.Expect(session.Out.Contents()).Should(gomega.BeEmpty()) 36 | command.Run(o, "pull", localImages[defaultImage]) 37 | 38 | // allow propagation time 39 | gomega.Eventually(func(session *gexec.Session) string { 40 | return strings.TrimSpace(string(session.Out.Contents())) 41 | }).WithArguments(session). 42 | WithTimeout(15 * time.Second). 43 | WithPolling(1 * time.Second). 44 | Should(gomega.ContainSubstring(localImages[defaultImage])) 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /tests/exec.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/onsi/ginkgo/v2" 12 | "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gbytes" 14 | 15 | "github.com/runfinch/common-tests/command" 16 | "github.com/runfinch/common-tests/ffs" 17 | "github.com/runfinch/common-tests/option" 18 | ) 19 | 20 | // Exec tests executing a command in a running container. 21 | func Exec(o *option.Option) { 22 | ginkgo.Describe("execute command in a container", func() { 23 | ginkgo.BeforeEach(func() { 24 | command.RemoveAll(o) 25 | }) 26 | ginkgo.AfterEach(func() { 27 | command.RemoveAll(o) 28 | }) 29 | // TODO: specifying -t flag will have error in test -> panic: provided file is not a console 30 | ginkgo.When("then container is running", func() { 31 | ginkgo.BeforeEach(func() { 32 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "sleep", "infinity") 33 | }) 34 | 35 | ginkgo.It("should execute a command in a running container", func() { 36 | strEchoed := "hello" 37 | output := command.StdoutStr(o, "exec", testContainerName, "echo", strEchoed) 38 | gomega.Expect(output).Should(gomega.Equal(strEchoed)) 39 | }) 40 | 41 | for _, interactive := range []string{"-i", "--interactive", "-i=true", "--interactive=true"} { 42 | ginkgo.It(fmt.Sprintf("should output string by piping if %s flag keeps STDIN open", interactive), func() { 43 | want := []byte("hello") 44 | got := command.New(o, "exec", interactive, testContainerName, "cat"). 45 | WithStdin(gbytes.BufferWithBytes(want)).Run().Out.Contents() 46 | gomega.Expect(got).Should(gomega.Equal(want)) 47 | }) 48 | } 49 | 50 | for _, detach := range []string{"-d", "--detach", "-d=true", "--detach=true"} { 51 | ginkgo.It(fmt.Sprintf("should execute command in detached mode with %s flag", detach), func() { 52 | command.Run(o, "exec", detach, testContainerName, "nc", "-l") 53 | processes := command.StdoutStr(o, "exec", testContainerName, "ps", "aux") 54 | gomega.Expect(processes).Should(gomega.ContainSubstring("nc -l")) 55 | }) 56 | } 57 | 58 | for _, workDir := range []string{"-w", "--workdir"} { 59 | ginkgo.It(fmt.Sprintf("should execute command under directory specified by %s flag", workDir), func() { 60 | dir := "/tmp" 61 | output := command.StdoutStr(o, "exec", workDir, dir, testContainerName, "pwd") 62 | gomega.Expect(output).Should(gomega.Equal(dir)) 63 | }) 64 | } 65 | 66 | for _, env := range []string{"-e", "--env"} { 67 | ginkgo.It(fmt.Sprintf("should set the environment variable with %s flag", env), func() { 68 | const envPair = "ENV=1" 69 | lines := command.StdoutAsLines(o, "exec", env, envPair, testContainerName, "env") 70 | gomega.Expect(lines).Should(gomega.ContainElement(envPair)) 71 | }) 72 | } 73 | 74 | ginkgo.It("should set environment variables from file with --env-file flag", func() { 75 | const envPair = "ENV=1" 76 | envPath := ffs.CreateTempFile("env", envPair) 77 | ginkgo.DeferCleanup(os.RemoveAll, filepath.Dir(envPath)) 78 | 79 | envOutput := command.StdoutAsLines(o, "exec", "--env-file", envPath, testContainerName, "env") 80 | gomega.Expect(envOutput).Should(gomega.ContainElement(envPair)) 81 | }) 82 | 83 | for _, privilegedFlag := range []string{"--privileged", "--privileged=true"} { 84 | ginkgo.It(fmt.Sprintf("should execute command in privileged mode with %s flag", privilegedFlag), func() { 85 | command.RunWithoutSuccessfulExit(o, "exec", testContainerName, "ip", "link", "add", "dummy1", "type", "dummy") 86 | command.Run(o, "exec", privilegedFlag, testContainerName, "ip", "link", "add", "dummy1", "type", "dummy") 87 | output := command.StdoutStr(o, "exec", privilegedFlag, testContainerName, "ip", "link") 88 | gomega.Expect(output).Should(gomega.ContainSubstring("dummy1")) 89 | }) 90 | } 91 | 92 | for _, user := range []string{"-u", "--user"} { 93 | ginkgo.It(fmt.Sprintf("should output user id according to user name specified by %s flag", user), func() { 94 | testCases := map[string][]string{ 95 | "1000": {"uid=1000 gid=0(root)", "uid=1000 gid=0(root) groups=0(root)"}, 96 | "1000:users": {"uid=1000 gid=100(users)", "uid=1000 gid=100(users) groups=100(users)"}, 97 | } 98 | 99 | for name, want := range testCases { 100 | output := command.StdoutStr(o, "exec", user, name, testContainerName, "id") 101 | // TODO: Remove the Or operator after upgrading the nerdctl dependency to 1.2.1 to only match want[1] 102 | gomega.Expect(output).Should(gomega.Or(gomega.Equal(want[0]), gomega.Equal(want[1]))) 103 | } 104 | }) 105 | } 106 | }) 107 | 108 | ginkgo.It("should not execute a command when the container is not running", func() { 109 | command.Run(o, "run", "--name", testContainerName, localImages[defaultImage]) 110 | command.RunWithoutSuccessfulExit(o, "exec", testContainerName) 111 | }) 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /tests/image_history.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/onsi/ginkgo/v2" 12 | "github.com/onsi/gomega" 13 | 14 | "github.com/runfinch/common-tests/ffs" 15 | 16 | "github.com/runfinch/common-tests/command" 17 | "github.com/runfinch/common-tests/option" 18 | ) 19 | 20 | // ImageHistory tests "image history" command that shows the history of an image. 21 | func ImageHistory(o *option.Option) { 22 | ginkgo.Describe("show the history of an image", func() { 23 | ginkgo.BeforeEach(func() { 24 | command.RemoveAll(o) 25 | pullImage(o, localImages[defaultImage]) 26 | }) 27 | 28 | ginkgo.AfterEach(func() { 29 | command.RemoveAll(o) 30 | }) 31 | 32 | ginkgo.It("should display image history", func() { 33 | gomega.Expect(command.StdoutStr(o, "image", "history", localImages[defaultImage])).ShouldNot(gomega.BeEmpty()) 34 | }) 35 | 36 | for _, quiet := range []string{"-q", "--quiet"} { 37 | ginkgo.It(fmt.Sprintf("should only display snapshot ID with %s flag", quiet), func() { 38 | output := command.StdoutAsLines(o, "image", "history", quiet, "--no-trunc", localImages[defaultImage]) 39 | ids := removeMissingID(output) 40 | gomega.Expect(ids).Should(gomega.HaveEach(gomega.MatchRegexp(sha256RegexFull))) 41 | }) 42 | } 43 | 44 | ginkgo.It("should only display snapshot ID with --format flag", func() { 45 | output := command.StdoutAsLines(o, "image", "history", "--no-trunc", localImages[defaultImage], "--format", "{{.Snapshot}}") 46 | ids := removeMissingID(output) 47 | gomega.Expect(ids).Should(gomega.HaveEach(gomega.MatchRegexp(sha256RegexFull))) 48 | }) 49 | 50 | ginkgo.It("should display image history with --no-trunc flag", func() { 51 | const text = "a very very very very long test phrase that only serves for testing purpose" 52 | buildContext := ffs.CreateBuildContext(fmt.Sprintf(`FROM %s 53 | CMD ["echo", %s] 54 | `, localImages[defaultImage], text)) 55 | ginkgo.DeferCleanup(os.RemoveAll, buildContext) 56 | 57 | command.Run(o, "build", "-t", testImageName, buildContext) 58 | gomega.Expect(command.StdoutStr(o, "image", "history", testImageName)).ShouldNot(gomega.ContainSubstring(text)) 59 | gomega.Expect(command.StdoutStr(o, "image", "history", "--no-trunc", testImageName)).Should(gomega.ContainSubstring(text)) 60 | }) 61 | }) 62 | } 63 | 64 | func removeMissingID(ids []string) []string { 65 | var res []string 66 | for _, id := range ids { 67 | if !strings.Contains(id, "missing") { 68 | res = append(res, id) 69 | } 70 | } 71 | return res 72 | } 73 | -------------------------------------------------------------------------------- /tests/image_inspect.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/onsi/ginkgo/v2" 8 | "github.com/onsi/gomega" 9 | 10 | "github.com/runfinch/common-tests/command" 11 | "github.com/runfinch/common-tests/option" 12 | ) 13 | 14 | // ImageInspect tests "image inspect" command that displays detailed information on one or more images. 15 | func ImageInspect(o *option.Option) { 16 | ginkgo.Describe("display detailed information on one or more images", func() { 17 | ginkgo.BeforeEach(func() { 18 | command.RemoveAll(o) 19 | pullImage(o, localImages[defaultImage]) 20 | }) 21 | ginkgo.AfterEach(func() { 22 | command.RemoveAll(o) 23 | }) 24 | 25 | ginkgo.It("should display detailed information on an image", func() { 26 | gomega.Expect(command.StdoutStr(o, "image", "inspect", localImages[defaultImage])).ShouldNot(gomega.BeEmpty()) 27 | }) 28 | 29 | ginkgo.It("should display image RepoTags with --format flag", func() { 30 | image := command.StdoutStr(o, "image", "inspect", localImages[defaultImage], "--format", "{{(index .RepoTags 0)}}") 31 | gomega.Expect(image).Should(gomega.Equal(localImages[defaultImage])) 32 | }) 33 | 34 | ginkgo.It("should display multiple image RepoTags with --format flag", func() { 35 | pullImage(o, localImages[olderAlpineImage]) 36 | lines := command.StdoutAsLines( 37 | o, 38 | "image", 39 | "inspect", 40 | localImages[defaultImage], 41 | localImages[olderAlpineImage], 42 | "--format", 43 | "{{(index .RepoTags 0)}}", 44 | ) 45 | gomega.Expect(lines).Should(gomega.ConsistOf(localImages[defaultImage], localImages[olderAlpineImage])) 46 | }) 47 | 48 | ginkgo.It("should not display information if image doesn't exist", func() { 49 | command.RunWithoutSuccessfulExit(o, "image", "inspect", nonexistentImageName) 50 | }) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /tests/image_prune.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega/gbytes" 11 | 12 | "github.com/runfinch/common-tests/command" 13 | "github.com/runfinch/common-tests/option" 14 | ) 15 | 16 | // ImagePrune tests "image prune" command that removes unused images. 17 | func ImagePrune(o *option.Option) { 18 | // Currently, nerdctl image prune requires --all to be specified. 19 | // REF - https://github.com/containerd/nerdctl#whale-nerdctl-image-prune 20 | // TODO: Add a test case to only prune dangling images after `--all` is not required for `image prune`. 21 | ginkgo.Describe("Remove unused images", func() { 22 | ginkgo.BeforeEach(func() { 23 | command.RemoveAll(o) 24 | pullImage(o, localImages[defaultImage]) 25 | }) 26 | ginkgo.AfterEach(func() { 27 | command.RemoveAll(o) 28 | }) 29 | 30 | ginkgo.It("should remove all unused images with inputting y in prompt confirmation", func() { 31 | imageShouldExist(o, localImages[defaultImage]) 32 | command.New(o, "image", "prune", "-a").WithStdin(gbytes.BufferWithBytes([]byte("y"))).Run() 33 | imageShouldNotExist(o, localImages[defaultImage]) 34 | }) 35 | 36 | ginkgo.It("should not remove any unused image with inputting n in prompt confirmation", func() { 37 | imageShouldExist(o, localImages[defaultImage]) 38 | command.New(o, "image", "prune", "-a").WithStdin(gbytes.BufferWithBytes([]byte("n"))).Run() 39 | imageShouldExist(o, localImages[defaultImage]) 40 | }) 41 | 42 | for _, force := range []string{"-f", "--force"} { 43 | ginkgo.It(fmt.Sprintf("with %s flag, should remove unused images without prompting a confirmation", force), func() { 44 | imageShouldExist(o, localImages[defaultImage]) 45 | command.Run(o, "image", "prune", "-a", "-f") 46 | imageShouldNotExist(o, localImages[defaultImage]) 47 | }) 48 | } 49 | 50 | ginkgo.It("should not remove an image if it's used by a dead container", func() { 51 | command.Run(o, "run", localImages[defaultImage]) 52 | imageShouldExist(o, localImages[defaultImage]) 53 | command.Run(o, "image", "prune", "-a", "-f") 54 | imageShouldExist(o, localImages[defaultImage]) 55 | }) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /tests/images.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/runfinch/common-tests/command" 8 | "github.com/runfinch/common-tests/option" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | ) 13 | 14 | // Images tests functionality of `images` command that lists container images. 15 | func Images(o *option.Option) { 16 | const sha256RegexTruncated = `^[a-f0-9]{12}$` 17 | ginkgo.Describe("list container images", ginkgo.Ordered, func() { 18 | testImageName := "fn-test-images-cmd:latest" 19 | ginkgo.BeforeAll(func() { 20 | pullImage(o, localImages[defaultImage]) 21 | buildImage(o, testImageName) 22 | }) 23 | 24 | ginkgo.AfterAll(func() { 25 | removeImage(o, testImageName) 26 | removeImage(o, localImages[defaultImage]) 27 | }) 28 | 29 | ginkgo.It("should list all the images in a tabular format", func() { 30 | images := command.StdoutAsLines(o, "images") 31 | header, images := images[0], images[1:] 32 | gomega.Expect(images).ShouldNot(gomega.BeEmpty()) 33 | gomega.Expect(header).Should(gomega.MatchRegexp( 34 | "REPOSITORY[\t ]+TAG[\t ]+IMAGE ID[\t ]+CREATED[\t ]+PLATFORM[\t ]+SIZE[\t ]+BLOB SIZE")) 35 | gomega.Expect(images).Should(gomega.HaveEach((gomega.MatchRegexp(`^(.+[\t ]+){6}.+$`)))) 36 | // TODO: add more strict validation using output matcher. 37 | }) 38 | ginkgo.It("should list all the images with image names in a tabular format ", func() { 39 | images := command.StdoutAsLines(o, "images", "--names") 40 | header, images := images[0], images[1:] 41 | gomega.Expect(images).ShouldNot(gomega.BeEmpty()) 42 | gomega.Expect(header).Should(gomega.MatchRegexp("NAME[\t ]+IMAGE ID[\t ]+CREATED[\t ]+PLATFORM[\t ]+SIZE[\t ]+BLOB SIZE")) 43 | gomega.Expect(images).Should(gomega.HaveEach((gomega.MatchRegexp(`^(.+[\t ]+){5}.+$`)))) 44 | // TODO: add more strict validation using output matcher. 45 | }) 46 | ginkgo.It("should list all the images", func() { 47 | images := command.StdoutAsLines(o, "images", "--format", "{{.Repository}}:{{.Tag}}") 48 | gomega.Expect(images).ShouldNot(gomega.BeEmpty()) 49 | gomega.Expect(images).Should(gomega.ContainElements(testImageName)) 50 | gomega.Expect(images).Should(gomega.ContainElements(localImages[defaultImage])) 51 | }) 52 | ginkgo.It("should list truncated IMAGE IDs", func() { 53 | images := command.StdoutAsLines(o, "images", "--quiet") 54 | gomega.Expect(images).ShouldNot(gomega.BeEmpty()) 55 | // TODO: Remove the Or operator after upgrading the nerdctl dependency to 1.2.1 to only match sha256RegexFull 56 | gomega.Expect(images).To(gomega.Or(gomega.HaveEach(gomega.MatchRegexp(sha256RegexFull)), 57 | gomega.HaveEach(gomega.MatchRegexp(sha256RegexTruncated)))) 58 | }) 59 | ginkgo.It("should list full IMAGE IDs", func() { 60 | images := command.StdoutAsLines(o, "images", "--quiet", "--no-trunc") 61 | gomega.Expect(images).ShouldNot(gomega.BeEmpty()) 62 | gomega.Expect(images).Should(gomega.HaveEach(gomega.MatchRegexp(sha256RegexFull))) 63 | }) 64 | ginkgo.It("should list IMAGE digests", func() { 65 | images := command.StdoutAsLines(o, "images", "--digests", "--format", "{{.Digest}}") 66 | gomega.Expect(images).ShouldNot(gomega.BeEmpty()) 67 | gomega.Expect(images).Should(gomega.HaveEach(gomega.MatchRegexp(sha256RegexFull))) 68 | }) 69 | // TODO: need to implement --filter functional test once we upgrade to nerdctl 0.23. 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /tests/info.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/onsi/ginkgo/v2" 8 | "github.com/onsi/gomega" 9 | 10 | "github.com/runfinch/common-tests/command" 11 | "github.com/runfinch/common-tests/option" 12 | ) 13 | 14 | // Info tests "info" command that displays system-wide information, synonyms to "system info" command. 15 | func Info(o *option.Option) { 16 | ginkgo.Describe("display system-wide information", func() { 17 | ginkgo.BeforeEach(func() { 18 | command.RemoveAll(o) 19 | }) 20 | ginkgo.AfterEach(func() { 21 | command.RemoveAll(o) 22 | }) 23 | 24 | ginkgo.It("should display system-wide information", func() { 25 | gomega.Expect(command.StdoutStr(o, "system", "info", "--format", "{{.OSType}}")).ShouldNot(gomega.BeEmpty()) 26 | gomega.Expect(command.StdoutStr(o, "system", "info", "--format", "{{.Architecture}}")).ShouldNot(gomega.BeEmpty()) 27 | }) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /tests/inspect.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/runfinch/common-tests/command" 8 | "github.com/runfinch/common-tests/option" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | ) 13 | 14 | // Inspect tests displaying the detailed information of image or container. 15 | func Inspect(o *option.Option) { 16 | ginkgo.Describe("inspect a container", func() { 17 | ginkgo.BeforeEach(func() { 18 | command.RemoveAll(o) 19 | }) 20 | ginkgo.AfterEach(func() { 21 | command.RemoveAll(o) 22 | }) 23 | 24 | ginkgo.It("should display the detailed information of a container", func() { 25 | command.Run(o, "run", "--name", testContainerName, localImages[defaultImage]) 26 | image := command.StdoutStr(o, "inspect", "--format", "{{.Image}}", testContainerName) 27 | gomega.Expect(image).To(gomega.Equal(localImages[defaultImage])) 28 | containerName := command.StdoutStr(o, "inspect", "--format", "{{.Name}}", testContainerName) 29 | gomega.Expect(containerName).To(gomega.Equal(testContainerName)) 30 | gomega.Expect(command.StdoutStr(o, "inspect", "--format", "{{.State.Status}}", testContainerName)).To(gomega.Equal("exited")) 31 | gomega.Expect(command.StdoutStr(o, "inspect", "--format", "{{.State.Error}}", testContainerName)).To(gomega.Equal("")) 32 | }) 33 | 34 | ginkgo.It("should display multiple container image with --format flag", func() { 35 | const oldContainerName = "ctr-old" 36 | command.Run(o, "run", "--name", testContainerName, localImages[defaultImage]) 37 | command.Run(o, "run", "--name", oldContainerName, localImages[olderAlpineImage]) 38 | images := command.StdoutAsLines(o, "inspect", "--format", "{{.Image}}", testContainerName, oldContainerName) 39 | gomega.Expect(images).Should(gomega.ConsistOf(localImages[defaultImage], localImages[olderAlpineImage])) 40 | }) 41 | 42 | ginkgo.It("should have an error if inspect a non-existing container", func() { 43 | command.RunWithoutSuccessfulExit(o, "inspect", nonexistentContainerName) 44 | }) 45 | 46 | ginkgo.It("should show the information of a container with --type=container flag", func() { 47 | command.Run(o, "run", "--name", testContainerName, localImages[defaultImage]) 48 | image := command.StdoutStr(o, "inspect", "--type", "container", testContainerName, "--format", "{{.Image}}") 49 | gomega.Expect(image).Should(gomega.Equal(localImages[defaultImage])) 50 | containerName := command.StdoutStr(o, "inspect", "--format", "{{.Name}}", testContainerName) 51 | gomega.Expect(containerName).Should(gomega.Equal(testContainerName)) 52 | }) 53 | 54 | ginkgo.It("should show the information of an image with --type=image flag", func() { 55 | pullImage(o, localImages[defaultImage]) 56 | image := command.StdoutStr(o, "inspect", "--type", "image", localImages[defaultImage], "--format", "{{(index .RepoTags 0)}}") 57 | gomega.Expect(image).Should(gomega.Equal(localImages[defaultImage])) 58 | }) 59 | 60 | ginkgo.It("should have an error if specify the wrong object type", func() { 61 | command.Run(o, "run", "--name", testContainerName, localImages[defaultImage]) 62 | command.RunWithoutSuccessfulExit(o, "inspect", "--type", "image", testContainerName) 63 | }) 64 | 65 | ginkgo.It("should have an error if inspect a non-existing image", func() { 66 | command.RunWithoutSuccessfulExit(o, "inspect", nonexistentImageName) 67 | }) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /tests/kill.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | 11 | "github.com/runfinch/common-tests/command" 12 | "github.com/runfinch/common-tests/option" 13 | ) 14 | 15 | // Kill tests killing a running container. 16 | func Kill(o *option.Option) { 17 | ginkgo.Describe("kill a container", func() { 18 | ginkgo.BeforeEach(func() { 19 | command.RemoveAll(o) 20 | }) 21 | ginkgo.AfterEach(func() { 22 | command.RemoveAll(o) 23 | }) 24 | 25 | ginkgo.When("the container is running", func() { 26 | ginkgo.BeforeEach(func() { 27 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "sleep", "infinity") 28 | }) 29 | 30 | ginkgo.It("should kill the running container", func() { 31 | containerShouldBeRunning(o, testContainerName) 32 | command.Run(o, "kill", testContainerName) 33 | command.RunWithoutSuccessfulExit(o, "exec", testContainerName, "echo", "foo") 34 | containerShouldNotBeRunning(o, testContainerName) 35 | }) 36 | 37 | for _, signal := range []string{"-s", "--signal"} { 38 | // With PID=1, `sleep infinity` will only exit when receiving SIGKILL. Default signal for kill is SIGKILL. 39 | // https://stackoverflow.com/questions/45148381/why-cant-i-ctrl-c-a-sleep-infinity-in-docker-when-it-runs-as-pid-1 40 | for _, term := range []string{"SIGTERM", "TERM"} { 41 | ginkgo.It(fmt.Sprintf("should not kill the running container with %s %s", signal, term), func() { 42 | containerShouldBeRunning(o, testContainerName) 43 | command.Run(o, "kill", signal, term, testContainerName) 44 | containerShouldBeRunning(o, testContainerName) 45 | }) 46 | } 47 | } 48 | }) 49 | 50 | ginkgo.It("should fail to send the signal if the container doesn't exist", func() { 51 | command.RunWithoutSuccessfulExit(o, "kill", nonexistentContainerName) 52 | }) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /tests/load.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/onsi/ginkgo/v2" 12 | 13 | "github.com/runfinch/common-tests/command" 14 | "github.com/runfinch/common-tests/ffs" 15 | "github.com/runfinch/common-tests/option" 16 | ) 17 | 18 | // Load tests loading images from tar file or stdin. 19 | func Load(o *option.Option) { 20 | ginkgo.Describe("load an image", func() { 21 | var tarFilePath string 22 | ginkgo.BeforeEach(func() { 23 | command.RemoveAll(o) 24 | pullImage(o, localImages[defaultImage]) 25 | tarFilePath = ffs.CreateTarFilePath() 26 | ginkgo.DeferCleanup(os.RemoveAll, filepath.Join(tarFilePath, "../")) 27 | }) 28 | ginkgo.AfterEach(func() { 29 | command.RemoveAll(o) 30 | }) 31 | // TODO: add test for input redirection sign 32 | // REF issue: https://github.com/lima-vm/lima/issues/1078 33 | for _, inputOption := range []string{"-i", "--input"} { 34 | ginkgo.It(fmt.Sprintf("should load an image with %s option", inputOption), func() { 35 | command.Run(o, "save", "-o", tarFilePath, localImages[defaultImage]) 36 | 37 | command.Run(o, "rmi", localImages[defaultImage]) 38 | imageShouldNotExist(o, localImages[defaultImage]) 39 | 40 | command.Run(o, "load", inputOption, tarFilePath) 41 | imageShouldExist(o, localImages[defaultImage]) 42 | }) 43 | 44 | ginkgo.It(fmt.Sprintf("should load multiple images with %s option", inputOption), func() { 45 | pullImage(o, localImages[olderAlpineImage]) 46 | command.Run(o, "save", "-o", tarFilePath, localImages[defaultImage], localImages[olderAlpineImage]) 47 | 48 | command.Run(o, "rmi", localImages[defaultImage], localImages[olderAlpineImage]) 49 | imageShouldNotExist(o, localImages[defaultImage]) 50 | imageShouldNotExist(o, localImages[olderAlpineImage]) 51 | 52 | command.Run(o, "load", inputOption, tarFilePath) 53 | imageShouldExist(o, localImages[defaultImage]) 54 | imageShouldExist(o, localImages[olderAlpineImage]) 55 | }) 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /tests/login.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/onsi/ginkgo/v2" 12 | "github.com/onsi/gomega/gbytes" 13 | 14 | "github.com/runfinch/common-tests/command" 15 | "github.com/runfinch/common-tests/ffs" 16 | "github.com/runfinch/common-tests/fnet" 17 | "github.com/runfinch/common-tests/option" 18 | ) 19 | 20 | // Login tests logging in a container registry. 21 | func Login(o *option.Option) { 22 | ginkgo.Describe("log in a container registry", func() { 23 | ginkgo.BeforeEach(func() { 24 | command.RemoveAll(o) 25 | }) 26 | ginkgo.AfterEach(func() { 27 | command.RemoveAll(o) 28 | }) 29 | ginkgo.When("the private registry is running and an image is built", func() { 30 | var registry string 31 | var tag string 32 | ginkgo.BeforeEach(func() { 33 | filename := "htpasswd" 34 | // The htpasswd is generated by 35 | // ` run --entrypoint htpasswd public.ecr.aws/docker/library/httpd:2 -Bbn testUser testPassword`. 36 | // We don't want to generate it on the fly because: 37 | // 1. Pulling the httpd image can take a long time, sometimes even more 10 seconds. 38 | // 2. It's unlikely that we will have to update this in the future. 39 | // 3. It's not the thing we want to validate by the functional tests. We only want the output produced by it. 40 | //nolint:gosec // This password is only used for testing purpose. 41 | htpasswd := "testUser:$2y$05$wE0sj3r9O9K9q7R0MXcfPuIerl/06L1IsxXkCuUr3QZ8lHWwicIdS" 42 | htpasswdDir := filepath.Dir(ffs.CreateTempFile(filename, htpasswd)) 43 | ginkgo.DeferCleanup(os.RemoveAll, htpasswdDir) 44 | port := fnet.GetFreePort() 45 | command.Run(o, "run", 46 | "-dp", fmt.Sprintf("%d:5000", port), 47 | "--name", "registry", 48 | "-v", fmt.Sprintf("%s:/auth", htpasswdDir), 49 | "-e", "REGISTRY_AUTH=htpasswd", 50 | "-e", "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", 51 | "-e", fmt.Sprintf("REGISTRY_AUTH_HTPASSWD_PATH=/auth/%s", filename), 52 | registryImage) 53 | registry = fmt.Sprintf(`localhost:%d`, port) 54 | tag = fmt.Sprintf(`%s/test-login:tag`, registry) 55 | buildContext := ffs.CreateBuildContext(fmt.Sprintf(`FROM %s 56 | CMD ["echo", "bar"] 57 | `, localImages[defaultImage])) 58 | ginkgo.DeferCleanup(os.RemoveAll, buildContext) 59 | command.Run(o, "build", "-t", tag, buildContext) 60 | }) 61 | for _, username := range []string{"-u", "--username"} { 62 | for _, password := range []string{"-p", "--password"} { 63 | ginkgo.It("should push an image after successfully logging in the registry with a correct credential", func() { 64 | command.Run(o, "login", registry, username, testUser, password, testPassword) 65 | ginkgo.DeferCleanup(func() { 66 | command.Run(o, "logout", registry) 67 | }) 68 | command.Run(o, "push", tag) 69 | }) 70 | ginkgo.It("should fail to push an image after failing to log in the registry with a wrong credential", func() { 71 | command.RunWithoutSuccessfulExit(o, "login", registry, username, testUser, password, "invalidPassword") 72 | command.RunWithoutSuccessfulExit(o, "login", registry, username, "invalidUser", password, testPassword) 73 | command.RunWithoutSuccessfulExit(o, "push", tag) 74 | }) 75 | } 76 | } 77 | ginkgo.It("should push an image after successfully logging in the registry with a correct credential by -password-stdin", func() { 78 | command.New(o, "login", registry, "-u", testUser, "--password-stdin"). 79 | WithStdin(gbytes.BufferWithBytes([]byte(testPassword))).Run() 80 | ginkgo.DeferCleanup(func() { 81 | command.Run(o, "logout", registry) 82 | }) 83 | command.Run(o, "push", tag) 84 | }) 85 | ginkgo.It("should fail to push an image after failing to log in the registry with a wrong credential by -password-stdin", func() { 86 | command.New(o, "login", registry, "-u", testUser, "--password-stdin"). 87 | WithStdin(gbytes.BufferWithBytes([]byte("invalidPassword"))).WithoutSuccessfulExit().Run() 88 | command.New(o, "login", registry, "-u", "invalidUser", "--password-stdin"). 89 | WithStdin(gbytes.BufferWithBytes([]byte(testPassword))).WithoutSuccessfulExit().Run() 90 | command.RunWithoutSuccessfulExit(o, "push", tag) 91 | }) 92 | }) 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /tests/logout.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/onsi/ginkgo/v2" 12 | 13 | "github.com/runfinch/common-tests/fnet" 14 | 15 | "github.com/runfinch/common-tests/command" 16 | "github.com/runfinch/common-tests/ffs" 17 | "github.com/runfinch/common-tests/option" 18 | ) 19 | 20 | // Logout tests logging out a container registry. 21 | func Logout(o *option.Option) { 22 | ginkgo.Describe("log out a container registry", func() { 23 | ginkgo.BeforeEach(func() { 24 | command.RemoveAll(o) 25 | }) 26 | ginkgo.AfterEach(func() { 27 | command.RemoveAll(o) 28 | }) 29 | ginkgo.When("the private registry is running and an image is built", func() { 30 | var registry string 31 | var tag string 32 | ginkgo.BeforeEach(func() { 33 | filename := "htpasswd" 34 | // The htpasswd is generated by 35 | // ` run --entrypoint htpasswd public.ecr.aws/docker/library/httpd:2 -Bbn testUser testPassword`. 36 | // We don't want to generate it on the fly because: 37 | // 1. Pulling the httpd image can take a long time, sometimes even more 10 seconds. 38 | // 2. It's unlikely that we will have to update this in the future. 39 | // 3. It's not the thing we want to validate by the functional tests. We only want the output produced by it. 40 | //nolint:gosec // This password is only used for testing purpose. 41 | htpasswd := "testUser:$2y$05$wE0sj3r9O9K9q7R0MXcfPuIerl/06L1IsxXkCuUr3QZ8lHWwicIdS" 42 | htpasswdDir := filepath.Dir(ffs.CreateTempFile(filename, htpasswd)) 43 | ginkgo.DeferCleanup(os.RemoveAll, htpasswdDir) 44 | port := fnet.GetFreePort() 45 | command.Run(o, "run", 46 | "-dp", fmt.Sprintf("%d:5000", port), 47 | "--name", "registry", 48 | "-v", fmt.Sprintf("%s:/auth", htpasswdDir), 49 | "-e", "REGISTRY_AUTH=htpasswd", 50 | "-e", "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", 51 | "-e", fmt.Sprintf("REGISTRY_AUTH_HTPASSWD_PATH=/auth/%s", filename), 52 | registryImage) 53 | registry = fmt.Sprintf(`localhost:%d`, port) 54 | tag = fmt.Sprintf(`%s/test-login:tag`, registry) 55 | buildContext := ffs.CreateBuildContext(fmt.Sprintf(`FROM %s 56 | CMD ["echo", "bar"] 57 | `, localImages[defaultImage])) 58 | ginkgo.DeferCleanup(os.RemoveAll, buildContext) 59 | command.Run(o, "build", "-t", tag, buildContext) 60 | }) 61 | ginkgo.It("should fail to push an image after logging out the registry", func() { 62 | command.Run(o, "login", registry, "-u", testUser, "-p", testPassword) 63 | ginkgo.DeferCleanup(func() { 64 | command.Run(o, "logout", registry) 65 | }) 66 | command.Run(o, "push", tag) 67 | command.Run(o, "logout", registry) 68 | command.RunWithoutSuccessfulExit(o, "push", tag) 69 | }) 70 | }) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /tests/logs.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "time" 10 | 11 | "github.com/onsi/ginkgo/v2" 12 | "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gexec" 14 | 15 | "github.com/runfinch/common-tests/command" 16 | "github.com/runfinch/common-tests/option" 17 | ) 18 | 19 | // Logs tests fetching logs of a container. 20 | func Logs(o *option.Option) { 21 | ginkgo.Describe("fetch logs of a container", func() { 22 | const foo = "foo" 23 | ginkgo.BeforeEach(func() { 24 | command.RemoveAll(o) 25 | }) 26 | ginkgo.AfterEach(func() { 27 | command.RemoveAll(o) 28 | }) 29 | 30 | ginkgo.When("the container is not running and has one line of logs", func() { 31 | ginkgo.BeforeEach(func() { 32 | // Currently, only containers created with `run -d` are supported. 33 | // https://github.com/containerd/nerdctl#whale-nerdctl-logs 34 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "echo", foo) 35 | }) 36 | 37 | ginkgo.It("should fetch the logs of a container", func() { 38 | output := command.StdoutStr(o, "logs", testContainerName) 39 | gomega.Expect(output).Should(gomega.Equal(foo)) 40 | }) 41 | 42 | for _, timestamps := range []string{"-t", "--timestamps"} { 43 | ginkgo.It(fmt.Sprintf("should include timestamp with %s flag", timestamps), func() { 44 | output := command.StdoutStr(o, "logs", timestamps, testContainerName) 45 | // `logs --timestamps` command will add an RFC3339Nano timestamp, 46 | // for example 2014-09-16T06:17:46.000000000Z, to each log entry. 47 | // "2006-01-02" is a golang common layout which specifies the format to be yyyy-MM-dd. 48 | gomega.Expect(output).Should(gomega.ContainSubstring(time.Now().UTC().Format("2006-01-02"))) 49 | gomega.Expect(output).Should(gomega.ContainSubstring(foo)) 50 | }) 51 | } 52 | 53 | ginkgo.It("should show log message depending on a relative time with --since flag", func() { 54 | time.Sleep(2 * time.Second) 55 | output := command.StdoutStr(o, "logs", "--since", "1s", testContainerName) 56 | gomega.Expect(output).Should(gomega.BeEmpty()) 57 | output = command.StdoutStr(o, "logs", "--since", "5s", testContainerName) 58 | gomega.Expect(output).Should(gomega.Equal(foo)) 59 | }) 60 | 61 | ginkgo.It("should show log message depending on a relative time with --until flag", func() { 62 | time.Sleep(2 * time.Second) 63 | output := command.StdoutStr(o, "logs", "--until", "1s", testContainerName) 64 | gomega.Expect(output).Should(gomega.Equal(foo)) 65 | output = command.StdoutStr(o, "logs", "--until", "5s", testContainerName) 66 | gomega.Expect(output).Should(gomega.BeEmpty()) 67 | }) 68 | }) 69 | 70 | ginkgo.When("the container is not running and has multiple lines of logs", func() { 71 | const bar = "bar" 72 | ginkgo.BeforeEach(func() { 73 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], 74 | "sh", "-c", fmt.Sprintf("echo %s; echo %s", foo, bar)) 75 | }) 76 | 77 | for _, tail := range []string{"-n", "--tail"} { 78 | ginkgo.It(fmt.Sprintf("should show number of lines from end of the logs with %s flag", tail), func() { 79 | expectedOutput := fmt.Sprintf("%s\n%s", foo, bar) 80 | output := command.StdoutStr(o, "logs", tail, "1", testContainerName) 81 | gomega.Expect(output).Should(gomega.Equal(bar)) 82 | output = command.StdoutStr(o, "logs", tail, "all", testContainerName) 83 | gomega.Expect(output).Should(gomega.Equal(expectedOutput)) 84 | }) 85 | } 86 | }) 87 | 88 | ginkgo.When("the container is running", func() { 89 | ginkgo.BeforeEach(func() { 90 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "sleep", "infinity") 91 | }) 92 | 93 | for _, follow := range []string{"-f", "--follow"} { 94 | ginkgo.It(fmt.Sprintf("should follow log output with %s flag", follow), func() { 95 | const newLog = "hello" 96 | session := command.RunWithoutWait(o, "logs", follow, testContainerName) 97 | defer session.Kill() 98 | gomega.Expect(session.Out.Contents()).Should(gomega.BeEmpty()) 99 | command.Run(o, "exec", testContainerName, "sh", "-c", fmt.Sprintf("echo %s >> /proc/1/fd/1", newLog)) 100 | // allow propagation time 101 | gomega.Eventually(func(session *gexec.Session) string { 102 | return strings.TrimSpace(string(session.Out.Contents())) 103 | }).WithArguments(session). 104 | WithTimeout(30 * time.Second). 105 | WithPolling(1 * time.Second). 106 | Should(gomega.Equal(newLog)) 107 | }) 108 | } 109 | }) 110 | 111 | ginkgo.It("should fail if container doesn't exist", func() { 112 | command.RunWithoutSuccessfulExit(o, "logs", nonexistentContainerName) 113 | }) 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /tests/network_create.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega" 11 | 12 | "github.com/runfinch/common-tests/command" 13 | "github.com/runfinch/common-tests/option" 14 | ) 15 | 16 | // NetworkCreate tests the "network create" command that creates a network. 17 | func NetworkCreate(o *option.Option) { 18 | ginkgo.Describe("create a network", func() { 19 | ginkgo.BeforeEach(func() { 20 | command.RemoveAll(o) 21 | }) 22 | ginkgo.AfterEach(func() { 23 | command.RemoveAll(o) 24 | }) 25 | // TODO: add tests for --ipam-opt, --opt=parent= 26 | ginkgo.It("should create a bridge network", func() { 27 | command.Run(o, "network", "create", testNetwork) 28 | gomega.Expect(command.StdoutStr(o, "network", "inspect", testNetwork, "--format", "{{.Name}}")).To(gomega.Equal(testNetwork)) 29 | }) 30 | 31 | ginkgo.It("containers under the same network can communicate with each other", func() { 32 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "sh", "-c", "echo hello | nc -l -p 80") 33 | ipAddr := command.StdoutStr(o, "inspect", "--format", "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}", testContainerName) 34 | output := command.StdoutStr(o, "run", localImages[defaultImage], "nc", fmt.Sprintf("%s:80", ipAddr)) 35 | gomega.Expect(output).Should(gomega.Equal("hello")) 36 | }) 37 | 38 | ginkgo.It("should create a network with custom subnet using --subnet flag", func() { 39 | // Choosing 10.5.0.0/16 because is mentioned in the doc - https://github.com/containerd/nerdctl#whale-nerdctl-network-create, 40 | // so it shouldn't overlap the subnets of the default networks. 41 | const subnet = "10.5.0.0/16" 42 | command.Run(o, "network", "create", "--subnet", subnet, testNetwork) 43 | output := command.StdoutStr(o, "network", "inspect", testNetwork, "--format", "{{(index .IPAM.Config 0).Subnet}}") 44 | gomega.Expect(output).Should(gomega.Equal(subnet)) 45 | }) 46 | 47 | ginkgo.It("should create a network with custom gateway using --gateway flag", func() { 48 | const ( 49 | subnet = "10.5.0.0/16" 50 | gateway = "10.5.0.3" 51 | ) 52 | command.Run(o, "network", "create", "--subnet", subnet, "--gateway", gateway, testNetwork) 53 | output := command.StdoutStr(o, "network", "inspect", testNetwork, "--format", "{{(index .IPAM.Config 0).Gateway}}") 54 | gomega.Expect(output).Should(gomega.Equal(gateway)) 55 | }) 56 | 57 | ginkgo.It("should create a network with custom ip range using --ip-range flag", func() { 58 | const ( 59 | subnet = "10.5.0.0/16" 60 | ipRange = "10.5.1.1/32" 61 | ) 62 | command.Run(o, "network", "create", "--subnet", subnet, "--ip-range", ipRange, testNetwork) 63 | output := command.StdoutStr(o, "network", "inspect", testNetwork, "--format", "{{(index .IPAM.Config 0).IPRange}}") 64 | gomega.Expect(output).Should(gomega.Equal(ipRange)) 65 | 66 | command.Run(o, "run", "-d", "--name", testContainerName, "--network", testNetwork, localImages[defaultImage], "sleep", "infinity") 67 | ipAddr := command.StdoutStr(o, "inspect", testContainerName, "--format", "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}") 68 | // Must be 10.5.1.1 because there is only one IP in the IP range. 69 | gomega.Expect(ipAddr).Should(gomega.Equal("10.5.1.1")) 70 | // Must fail because there is no available IP in the IP range now. 71 | command.RunWithoutSuccessfulExit(o, "run", "--name", "test-ctr2", "--network", testNetwork, localImages[defaultImage]) 72 | }) 73 | 74 | ginkgo.It("should create a network with label using --label flag", func() { 75 | command.Run(o, "network", "create", "--label", "key=val", testNetwork) 76 | output := command.StdoutStr(o, "network", "inspect", testNetwork, "--format", "{{.Labels.key}}") 77 | gomega.Expect(output).Should(gomega.Equal("val")) 78 | }) 79 | 80 | for _, driverFlag := range []string{"-d", "--driver"} { 81 | for _, driver := range []string{"macvlan", "ipvlan"} { 82 | ginkgo.It(fmt.Sprintf("should create %s network with %s flag", driver, driverFlag), func() { 83 | command.Run(o, "network", "create", driverFlag, driver, testNetwork) 84 | netType := command.StdoutStr(o, "network", "inspect", testNetwork, "--mode=native", 85 | "--format", "{{(index .CNI.plugins 0).type}}") 86 | gomega.Expect(netType).Should(gomega.Equal(driver)) 87 | }) 88 | } 89 | } 90 | 91 | for _, opt := range []string{"-o", "--opt"} { 92 | ginkgo.It(fmt.Sprintf("should set the containers network MTU with %s flag", opt), func() { 93 | command.Run(o, "network", "create", opt, "com.docker.network.driver.mtu=500", testNetwork) 94 | mtu := command.StdoutStr(o, "network", "inspect", testNetwork, "--mode=native", "--format", "{{(index .CNI.plugins 0).mtu}}") 95 | gomega.Expect(mtu).Should(gomega.Equal("500")) 96 | }) 97 | 98 | ginkgo.It(fmt.Sprintf("should set macvlan network mode to bridge with %s flag", opt), func() { 99 | command.Run(o, "network", "create", opt, "macvlan_mode=bridge", "-d", "macvlan", testNetwork) 100 | mode := command.StdoutStr(o, "network", "inspect", testNetwork, "--mode=native", "--format", "{{(index .CNI.plugins 0).mode}}") 101 | gomega.Expect(mode).Should(gomega.Equal("bridge")) 102 | }) 103 | 104 | ginkgo.It(fmt.Sprintf("should set ipvlan network mode to l3 with %s flag", opt), func() { 105 | command.Run(o, "network", "create", opt, "ipvlan_mode=l3", "-d", "ipvlan", testNetwork) 106 | mode := command.StdoutStr(o, "network", "inspect", testNetwork, "--mode=native", "--format", "{{(index .CNI.plugins 0).mode}}") 107 | gomega.Expect(mode).Should(gomega.Equal("l3")) 108 | }) 109 | } 110 | 111 | ginkgo.It("should set IPAM driver with --ipam-driver flag", func() { 112 | command.Run(o, "network", "create", "--ipam-driver=default", testNetwork) 113 | driverType := command.StdoutStr(o, "network", "inspect", testNetwork, "--mode=native", 114 | "--format", "{{(index .CNI.plugins 0).ipam.type}}") 115 | // In unix, default driver type is host-local. 116 | // https://github.com/containerd/nerdctl/blob/817d6ec27c01986f9cd16a65380294087ef8905f/pkg/netutil/netutil_unix.go#L162 117 | gomega.Expect(driverType).Should(gomega.Equal("host-local")) 118 | }) 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /tests/network_inspect.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/onsi/ginkgo/v2" 8 | "github.com/onsi/gomega" 9 | 10 | "github.com/runfinch/common-tests/command" 11 | "github.com/runfinch/common-tests/option" 12 | ) 13 | 14 | // NetworkInspect tests the "network inspect" command that displays detailed information on one or more networks. 15 | func NetworkInspect(o *option.Option) { 16 | ginkgo.Describe("display detailed information on network", func() { 17 | ginkgo.BeforeEach(func() { 18 | command.RemoveAll(o) 19 | }) 20 | ginkgo.AfterEach(func() { 21 | command.RemoveAll(o) 22 | }) 23 | 24 | ginkgo.It("should display detailed information about one network", func() { 25 | name := command.StdoutStr(o, "network", "inspect", bridgeNetwork, "--format", "{{.Name}}") 26 | gomega.Expect(name).Should(gomega.Equal(bridgeNetwork)) 27 | }) 28 | 29 | ginkgo.It("should display detailed information on multiple networks", func() { 30 | command.Run(o, "network", "create", testNetwork) 31 | lines := command.StdoutAsLines(o, "network", "inspect", bridgeNetwork, testNetwork, "--format", "{{.Name}}") 32 | gomega.Expect(lines).Should(gomega.ConsistOf(bridgeNetwork, testNetwork)) 33 | }) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /tests/network_ls.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega" 11 | 12 | "github.com/runfinch/common-tests/command" 13 | "github.com/runfinch/common-tests/option" 14 | ) 15 | 16 | // NetworkLs tests the "network ls" command that list networks. 17 | func NetworkLs(o *option.Option) { 18 | ginkgo.Describe("list networks", func() { 19 | ginkgo.BeforeEach(func() { 20 | command.RemoveAll(o) 21 | }) 22 | ginkgo.AfterEach(func() { 23 | command.RemoveAll(o) 24 | }) 25 | 26 | ginkgo.It("should list all the networks", func() { 27 | output := command.StdoutStr(o, "network", "ls") 28 | gomega.Expect(output).Should(gomega.ContainSubstring(bridgeNetwork)) 29 | }) 30 | 31 | ginkgo.It("should only display network name with --format flag", func() { 32 | lines := command.StdoutAsLines(o, "network", "ls", "--format", "{{.Name}}") 33 | gomega.Expect(lines).Should(gomega.ContainElement(bridgeNetwork)) 34 | }) 35 | 36 | for _, quiet := range []string{"-q", "--quiet"} { 37 | ginkgo.It(fmt.Sprintf("should only display network id with %s flag", quiet), func() { 38 | output := command.StdoutStr(o, "network", "ls", quiet) 39 | gomega.Expect(output).ShouldNot(gomega.BeEmpty()) 40 | gomega.Expect(output).ShouldNot(gomega.ContainSubstring(bridgeNetwork)) 41 | }) 42 | } 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /tests/network_rm.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/onsi/ginkgo/v2" 8 | "github.com/onsi/gomega" 9 | 10 | "github.com/runfinch/common-tests/command" 11 | "github.com/runfinch/common-tests/option" 12 | ) 13 | 14 | // NetworkRm tests the "network rm" command that removes one or more networks. 15 | func NetworkRm(o *option.Option) { 16 | ginkgo.Describe("remove one or more networks", func() { 17 | ginkgo.BeforeEach(func() { 18 | command.RemoveAll(o) 19 | command.Run(o, "network", "create", testNetwork) 20 | }) 21 | 22 | ginkgo.AfterEach(func() { 23 | command.RemoveAll(o) 24 | }) 25 | 26 | ginkgo.It("should remove a network", func() { 27 | gomega.Expect(command.StdoutAsLines(o, "network", "ls", "--format", "{{.Name}}")).Should(gomega.ContainElement(testNetwork)) 28 | command.Run(o, "network", "rm", testNetwork) 29 | gomega.Expect(command.StdoutAsLines(o, "network", "ls", "--format", "{{.Name}}")).ShouldNot(gomega.ContainElement(testNetwork)) 30 | }) 31 | 32 | ginkgo.It("should remove multiple networks", func() { 33 | const testNetwork2 = "test-network2" 34 | command.Run(o, "network", "create", testNetwork2) 35 | command.Run(o, "network", "rm", testNetwork, testNetwork2) 36 | lines := command.StdoutAsLines(o, "network", "ls", "--format", "{{.Name}}") 37 | gomega.Expect(lines).ShouldNot(gomega.ContainElement(testNetwork)) 38 | gomega.Expect(lines).ShouldNot(gomega.ContainElement(testNetwork2)) 39 | }) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /tests/port.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega" 11 | 12 | "github.com/runfinch/common-tests/command" 13 | "github.com/runfinch/common-tests/fnet" 14 | "github.com/runfinch/common-tests/option" 15 | ) 16 | 17 | // Port tests listing port mappings or a specific mapping for a container. 18 | func Port(o *option.Option) { 19 | ginkgo.Describe("list port mapping", func() { 20 | const containerPort = 4567 21 | ginkgo.BeforeEach(func() { 22 | command.RemoveAll(o) 23 | }) 24 | ginkgo.AfterEach(func() { 25 | command.RemoveAll(o) 26 | }) 27 | 28 | ginkgo.It("should output port mappings for a container", func() { 29 | hostPort := fnet.GetFreePort() 30 | command.Run(o, "run", "-p", fmt.Sprintf("%d:%d", hostPort, containerPort), "--name", testContainerName, localImages[defaultImage]) 31 | 32 | output := command.StdoutStr(o, "port", testContainerName) 33 | gomega.Expect(output).Should(gomega.Equal(fmt.Sprintf("%d/tcp -> 0.0.0.0:%d", containerPort, hostPort))) 34 | }) 35 | 36 | ginkgo.It("should output the host port according to container port", func() { 37 | hostPort := fnet.GetFreePort() 38 | command.Run(o, "run", "-p", fmt.Sprintf("%d:%d", hostPort, containerPort), "--name", testContainerName, localImages[defaultImage]) 39 | 40 | output := command.StdoutStr(o, "port", testContainerName, fmt.Sprintf("%d/tcp", containerPort)) 41 | gomega.Expect(output).Should(gomega.Equal(fmt.Sprintf("0.0.0.0:%d", hostPort))) 42 | }) 43 | 44 | ginkgo.It("should have error if specifying wrong protocol", func() { 45 | hostPort := fnet.GetFreePort() 46 | command.Run(o, 47 | "run", 48 | "-p", 49 | fmt.Sprintf("%d:%d/udp", hostPort, containerPort), 50 | "--name", 51 | testContainerName, 52 | localImages[defaultImage], 53 | ) 54 | 55 | command.RunWithoutSuccessfulExit(o, "port", testContainerName, fmt.Sprintf("%d/tcp", containerPort)) 56 | }) 57 | 58 | ginkgo.It("should still output the host port according to container port when no protocol is specified", func() { 59 | hostPort := fnet.GetFreePort() 60 | command.Run(o, "run", "-p", fmt.Sprintf("%d:%d", hostPort, containerPort), "--name", testContainerName, localImages[defaultImage]) 61 | 62 | output := command.StdoutStr(o, "port", testContainerName, fmt.Sprint(containerPort)) 63 | gomega.Expect(output).Should(gomega.Equal(fmt.Sprintf("0.0.0.0:%d", hostPort))) 64 | }) 65 | 66 | ginkgo.It("should have error if trying to print container port which is not published to any host port", func() { 67 | hostPort := fnet.GetFreePort() 68 | command.Run(o, "run", "-p", fmt.Sprintf("%d:%d", hostPort, containerPort), "--name", testContainerName, localImages[defaultImage]) 69 | 70 | command.RunWithoutSuccessfulExit(o, "port", testContainerName, "111/tcp") 71 | }) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /tests/ps.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | 13 | "github.com/runfinch/common-tests/command" 14 | "github.com/runfinch/common-tests/option" 15 | ) 16 | 17 | // Ps tests functionality of `ps` command. 18 | func Ps(o *option.Option) { 19 | sha256RegexTruncated := `^[a-f0-9]{12}$` 20 | sha256RegexFull := `^[a-f0-9]{64}$` 21 | 22 | containerNames := []string{"ctr_1", "ctr_2"} 23 | ginkgo.Describe("Ps command", func() { 24 | ginkgo.BeforeEach(func() { 25 | command.RemoveAll(o) 26 | command.Run(o, "network", "create", testNetwork) 27 | command.Run(o, "run", "-d", 28 | "--name", containerNames[0], 29 | localImages[defaultImage]) 30 | command.Run(o, "run", "-d", 31 | "--name", containerNames[1], 32 | localImages[defaultImage], "sleep", "infinity") 33 | }) 34 | 35 | ginkgo.AfterEach(func() { 36 | command.RemoveAll(o) 37 | }) 38 | ginkgo.It("should list only running containers", func() { 39 | psOutput := command.StdoutAsLines(o, "ps", "--format", "{{.Names}}") 40 | gomega.Expect(psOutput).ShouldNot(gomega.ContainElement(containerNames[0])) 41 | gomega.Expect(psOutput).Should(gomega.ContainElement(containerNames[1])) 42 | }) 43 | 44 | for _, flag := range []string{"-a", "--all"} { 45 | ginkgo.It(fmt.Sprintf("should list all containers [%s flag]", flag), func() { 46 | psOutput := command.StdoutAsLines(o, "ps", "--format", "{{.Names}}", flag) 47 | gomega.Expect(psOutput).Should(gomega.ContainElements(containerNames)) 48 | }) 49 | } 50 | 51 | ginkgo.It("should list ID of the containers", func() { 52 | psOutput := command.StdoutAsLines(o, "ps", "--format", "{{.ID}}") 53 | gomega.Expect(psOutput).Should(gomega.HaveEach(gomega.MatchRegexp(sha256RegexTruncated))) 54 | }) 55 | ginkgo.It("should list image of the containers", func() { 56 | psOutput := command.StdoutAsLines(o, "ps", "--format", "{{.Image}}") 57 | gomega.Expect(psOutput).Should(gomega.ContainElement(localImages[defaultImage])) 58 | }) 59 | ginkgo.It("should list command of the containers", func() { 60 | psOutput := command.StdoutStr(o, "ps", "--format", "{{.Command}}") 61 | gomega.Expect(psOutput).Should(gomega.ContainSubstring("sleep infinity")) 62 | }) 63 | ginkgo.It("should list creation date of the containers", func() { 64 | psOutput := command.StdoutAsLines(o, "ps", "--format", "{{.CreatedAt}}") 65 | gomega.Expect(psOutput).ShouldNot(gomega.ContainElement(gomega.BeEmpty())) 66 | }) 67 | ginkgo.It("should list only running containers", func() { 68 | psOutput := command.StdoutAsLines(o, "ps", "--format", "{{.Status}}") 69 | gomega.Expect(psOutput).Should(gomega.ContainElement("Up")) 70 | }) 71 | 72 | for _, flag := range []string{"-q", "--quiet"} { 73 | ginkgo.It(fmt.Sprintf("should list truncated container IDs [%s flag]", flag), func() { 74 | psOutput := command.StdoutAsLines(o, "ps", flag) 75 | gomega.Expect(psOutput).ShouldNot(gomega.BeEmpty()) 76 | gomega.Expect(psOutput).Should(gomega.HaveEach(gomega.MatchRegexp(sha256RegexTruncated))) 77 | }) 78 | } 79 | 80 | ginkgo.It("should list full container IDs", func() { 81 | psOutput := command.StdoutAsLines(o, "ps", "--format", "{{.ID}}", "--no-trunc") 82 | gomega.Expect(psOutput).ShouldNot(gomega.BeEmpty()) 83 | gomega.Expect(psOutput).Should(gomega.HaveEach(gomega.MatchRegexp(sha256RegexFull))) 84 | }) 85 | 86 | for _, flag := range []string{"-s", "--size"} { 87 | ginkgo.It(fmt.Sprintf("should list container size [%s flag]", flag), func() { 88 | psOutput := command.StdoutStr(o, "ps", "--format", "{{.Size}}", flag) 89 | gomega.Expect(psOutput).ShouldNot(gomega.BeEmpty()) 90 | }) 91 | } 92 | 93 | for _, flag := range []string{"-n", "--last"} { 94 | ginkgo.It(fmt.Sprintf("should list last 1 containers [%s flag]", flag), func() { 95 | psOutput := command.StdoutAsLines(o, "ps", "--format", "{{.Names}}", flag, "1") 96 | gomega.Expect(psOutput).ShouldNot(gomega.ContainElement(containerNames[0])) 97 | gomega.Expect(psOutput).Should(gomega.ContainElement(containerNames[1])) 98 | }) 99 | } 100 | 101 | for _, flag := range []string{"-l", "--latest"} { 102 | ginkgo.It(fmt.Sprintf("should list last 1 containers [%s flag]", flag), func() { 103 | psOutput := command.StdoutAsLines(o, "ps", "--format", "{{.Names}}", flag) 104 | gomega.Expect(psOutput).ShouldNot(gomega.ContainElement(containerNames[0])) 105 | gomega.Expect(psOutput).Should(gomega.ContainElement(containerNames[1])) 106 | }) 107 | } 108 | }) 109 | 110 | ginkgo.Describe("Ps command", func() { 111 | pwd, _ := os.Getwd() 112 | ginkgo.BeforeEach(func() { 113 | command.RemoveAll(o) 114 | command.Run(o, "network", "create", testNetwork) 115 | command.Run(o, "run", "-d", 116 | "--name", containerNames[0], 117 | "--label", "color=red", 118 | "-v", fmt.Sprintf("%s:%s", pwd, pwd), 119 | "-w", pwd, 120 | localImages[defaultImage]) 121 | command.Run(o, "run", "-d", 122 | "--label", "color=green", 123 | "--network", testNetwork, 124 | "-p", "8081:80", 125 | "--name", containerNames[1], 126 | localImages[defaultImage], "sleep", "infinity") 127 | }) 128 | 129 | ginkgo.AfterEach(func() { 130 | command.RemoveAll(o) 131 | }) 132 | ginkgo.It("should list port forwarding info of the containers", func() { 133 | psOutput := command.StdoutStr(o, "ps", "--format", "{{.Ports}}") 134 | gomega.Expect(psOutput).Should(gomega.ContainSubstring("8081")) 135 | }) 136 | }) 137 | 138 | ginkgo.Describe("Ps command", func() { 139 | pwd, _ := os.Getwd() 140 | ginkgo.BeforeEach(func() { 141 | command.RemoveAll(o) 142 | command.Run(o, "network", "create", testNetwork) 143 | command.Run(o, "run", "-d", 144 | "--name", containerNames[0], 145 | "--label", "color=red", 146 | "-v", fmt.Sprintf("%s:%s", pwd, pwd), 147 | "-w", pwd, 148 | localImages[defaultImage]) 149 | command.Run(o, "run", "-d", 150 | "--label", "color=green", 151 | "--network", testNetwork, 152 | "-p", "8081:80", 153 | "--name", containerNames[1], 154 | localImages[defaultImage], "sleep", "infinity") 155 | }) 156 | 157 | ginkgo.AfterEach(func() { 158 | command.RemoveAll(o) 159 | }) 160 | filterTests := []struct { 161 | filter string 162 | expectedOutput []string 163 | }{ 164 | { 165 | filter: fmt.Sprintf("name=%s", containerNames[0]), 166 | expectedOutput: []string{containerNames[0]}, 167 | }, 168 | { 169 | filter: "label=color=green", 170 | expectedOutput: []string{containerNames[1]}, 171 | }, 172 | { 173 | filter: "label=color", 174 | expectedOutput: containerNames, 175 | }, 176 | { 177 | filter: "exited=0", 178 | expectedOutput: []string{containerNames[0]}, 179 | }, 180 | { 181 | filter: "status=exited", 182 | expectedOutput: []string{containerNames[0]}, 183 | }, 184 | { 185 | filter: "status=running", 186 | expectedOutput: []string{containerNames[1]}, 187 | }, 188 | { 189 | filter: fmt.Sprintf("before=%s", containerNames[1]), 190 | expectedOutput: []string{containerNames[0]}, 191 | }, 192 | { 193 | filter: fmt.Sprintf("since=%s", containerNames[0]), 194 | expectedOutput: []string{containerNames[1]}, 195 | }, 196 | { 197 | filter: fmt.Sprintf("volume=%s", pwd), 198 | expectedOutput: []string{containerNames[0]}, 199 | }, 200 | { 201 | filter: fmt.Sprintf("network=%s", testNetwork), 202 | expectedOutput: []string{containerNames[1]}, 203 | }, 204 | } 205 | 206 | for _, test := range filterTests { 207 | ginkgo.It(fmt.Sprintf(" should list container with filter %s", test.filter), func() { 208 | output := command.StdoutAsLines(o, "ps", "-a", "--format", "{{.Names}}", "--filter", test.filter) 209 | gomega.Expect(output).Should(gomega.ContainElements(test.expectedOutput)) 210 | }) 211 | } 212 | }) 213 | } 214 | -------------------------------------------------------------------------------- /tests/pull.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/runfinch/common-tests/command" 8 | "github.com/runfinch/common-tests/option" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | ) 12 | 13 | // Pull tests pulling a container image. 14 | func Pull(o *option.Option) { 15 | ginkgo.Describe("pull a container image", func() { 16 | ginkgo.BeforeEach(func() { 17 | command.RemoveImages(o) 18 | }) 19 | 20 | ginkgo.AfterEach(func() { 21 | command.RemoveImages(o) 22 | }) 23 | 24 | ginkgo.It("should pull the default image successfully", func() { 25 | command.Run(o, "pull", localImages[defaultImage]) 26 | imageShouldExist(o, localImages[defaultImage]) 27 | }) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /tests/push.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | 13 | "github.com/runfinch/common-tests/command" 14 | "github.com/runfinch/common-tests/ffs" 15 | "github.com/runfinch/common-tests/fnet" 16 | "github.com/runfinch/common-tests/option" 17 | ) 18 | 19 | // Push tests pushing an image to a registry. 20 | func Push(o *option.Option) { 21 | ginkgo.Describe("Push a container image to registry", func() { 22 | var buildContext string 23 | var port int 24 | 25 | ginkgo.BeforeEach(func() { 26 | command.RemoveAll(o) 27 | buildContext = ffs.CreateBuildContext(fmt.Sprintf(`FROM %s 28 | CMD ["echo", "bar"] 29 | `, localImages[defaultImage])) 30 | ginkgo.DeferCleanup(os.RemoveAll, buildContext) 31 | port = fnet.GetFreePort() 32 | command.Run(o, "run", "-dp", fmt.Sprintf("%d:5000", port), "--name", "registry", registryImage) 33 | }) 34 | 35 | ginkgo.AfterEach(func() { 36 | command.RemoveAll(o) 37 | }) 38 | 39 | ginkgo.Context("Test push command without any flag", func() { 40 | ginkgo.It("should push an image with a valid tag to registry", func() { 41 | tag := fmt.Sprintf(`localhost:%d/test-push:tag`, port) 42 | command.Run(o, "build", "-t", tag, buildContext) 43 | command.Run(o, "push", tag) 44 | command.Run(o, "pull", tag) 45 | }) 46 | 47 | ginkgo.It("should return an error when pushing a nonexistent tag", func() { 48 | nonexistentTag := fmt.Sprintf(`localhost:%d/nonexistent:tag`, port) 49 | stderr := command.RunWithoutSuccessfulExit(o, "push", nonexistentTag).Err.Contents() 50 | gomega.Expect(stderr).To(gomega.ContainSubstring("not found")) 51 | }) 52 | }) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /tests/restart.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/onsi/gomega" 8 | 9 | "github.com/runfinch/common-tests/command" 10 | "github.com/runfinch/common-tests/option" 11 | 12 | "github.com/onsi/ginkgo/v2" 13 | ) 14 | 15 | // Restart tests "restart" command that will restart one or more running containers. 16 | func Restart(o *option.Option) { 17 | // TODO: add tests for -t/--time flag 18 | // REF issue - https://github.com/containerd/nerdctl/issues/1485 19 | ginkgo.Describe("restart command", ginkgo.Ordered, func() { 20 | ginkgo.BeforeEach(func() { 21 | command.RemoveAll(o) 22 | // Functionality wise, we only need `sleep infinity` to keep the container running, 23 | // but with PID=1, `sleep infinity` will only exit when receiving SIGKILL, 24 | // which means that we'll have to wait for the default timeout (10 seconds for now) to restart the container, 25 | // so we use `nc -l` instead to save time. 26 | // TODO: Remove the above comment after we add a test case for -t/--time flag with `sleep infinity` because it's more obvious. 27 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "nc", "-l") 28 | }) 29 | 30 | ginkgo.AfterEach(func() { 31 | command.RemoveAll(o) 32 | }) 33 | 34 | ginkgo.It("should restart a running container", func() { 35 | pid := getContainerPID(o, testContainerName) 36 | command.Run(o, "restart", testContainerName) 37 | newPid := getContainerPID(o, testContainerName) 38 | 39 | gomega.Expect(pid).NotTo(gomega.Equal(newPid)) 40 | }) 41 | 42 | ginkgo.It("should restart multiple running containers", func() { 43 | const ctrName = "ctr-name" 44 | command.Run(o, "run", "-d", "--name", ctrName, localImages[defaultImage], "nc", "-l") 45 | pid := getContainerPID(o, testContainerName) 46 | pid2 := getContainerPID(o, ctrName) 47 | command.Run(o, "restart", testContainerName, ctrName) 48 | newPid := getContainerPID(o, testContainerName) 49 | newPid2 := getContainerPID(o, ctrName) 50 | gomega.Expect(pid).NotTo(gomega.Equal(newPid)) 51 | gomega.Expect(pid2).NotTo(gomega.Equal(newPid2)) 52 | }) 53 | 54 | ginkgo.It("should have error when restarting a nonexistent container", func() { 55 | command.RunWithoutSuccessfulExit(o, "restart", nonexistentContainerName) 56 | }) 57 | }) 58 | } 59 | 60 | func getContainerPID(o *option.Option, containerName string) string { 61 | return command.StdoutStr(o, "inspect", containerName, "--format", "{{.State.Pid}}") 62 | } 63 | -------------------------------------------------------------------------------- /tests/rm.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega" 11 | 12 | "github.com/runfinch/common-tests/command" 13 | "github.com/runfinch/common-tests/option" 14 | ) 15 | 16 | // Rm tests removing a container. 17 | func Rm(o *option.Option) { 18 | ginkgo.Describe("remove a container", func() { 19 | ginkgo.BeforeEach(func() { 20 | command.RemoveAll(o) 21 | }) 22 | ginkgo.AfterEach(func() { 23 | command.RemoveAll(o) 24 | }) 25 | 26 | ginkgo.It("should remove the container when it is not running", func() { 27 | command.Run(o, "run", "--name", testContainerName, localImages[defaultImage]) 28 | containerShouldExist(o, testContainerName) 29 | 30 | command.Run(o, "rm", testContainerName) 31 | err := containerShouldNotExist(o, testContainerName) 32 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 33 | }) 34 | 35 | ginkgo.Context("when the container is running", func() { 36 | ginkgo.BeforeEach(func() { 37 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "sleep", "infinity") 38 | }) 39 | 40 | ginkgo.It("should not be able to remove the container without -f/--force flag", func() { 41 | command.RunWithoutSuccessfulExit(o, "rm", testContainerName) 42 | containerShouldExist(o, testContainerName) 43 | }) 44 | 45 | for _, force := range []string{"-f", "--force"} { 46 | ginkgo.It(fmt.Sprintf("should be able to remove the container with %s flag", force), func() { 47 | command.Run(o, "rm", force, testContainerName) 48 | err := containerShouldNotExist(o, testContainerName) 49 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 50 | }) 51 | } 52 | }) 53 | 54 | ginkgo.Context("when a volume is used by the container", func() { 55 | for _, volumes := range []string{"-v", "--volumes"} { 56 | ginkgo.It(fmt.Sprintf("with %s flag, should remove the container and the anonymous volume used by the container", volumes), 57 | func() { 58 | command.Run(o, "run", "-v", "/usr/share", "--name", testContainerName, localImages[defaultImage]) 59 | anonymousVolume := command.StdoutStr(o, "inspect", testContainerName, 60 | "--format", "{{range .Mounts}}{{.Name}}{{end}}") 61 | containerShouldExist(o, testContainerName) 62 | volumeShouldExist(o, anonymousVolume) 63 | command.Run(o, "rm", volumes, testContainerName) 64 | err := containerShouldNotExist(o, testContainerName) 65 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 66 | volumeShouldNotExist(o, anonymousVolume) 67 | }, 68 | ) 69 | 70 | ginkgo.It(fmt.Sprintf("with %s flag, should remove the container but can't remove the named volume used by container", volumes), 71 | func() { 72 | command.Run(o, "run", "-v", "foo:/usr/share", "--name", testContainerName, localImages[defaultImage]) 73 | volumeShouldExist(o, "foo") 74 | 75 | command.Run(o, "rm", volumes, testContainerName) 76 | err := containerShouldNotExist(o, testContainerName) 77 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 78 | volumeShouldExist(o, "foo") 79 | }, 80 | ) 81 | } 82 | }) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /tests/rmi.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/runfinch/common-tests/command" 10 | "github.com/runfinch/common-tests/option" 11 | 12 | "github.com/onsi/ginkgo/v2" 13 | ) 14 | 15 | // Rmi tests removing a container image. 16 | func Rmi(o *option.Option) { 17 | ginkgo.Describe("remove a container image", func() { 18 | ginkgo.BeforeEach(func() { 19 | command.RemoveAll(o) 20 | }) 21 | ginkgo.AfterEach(func() { 22 | command.RemoveAll(o) 23 | }) 24 | 25 | ginkgo.It("should remove an image when container is not running", func() { 26 | pullImage(o, localImages[defaultImage]) 27 | 28 | command.Run(o, "rmi", localImages[defaultImage]) 29 | imageShouldNotExist(o, localImages[defaultImage]) 30 | }) 31 | 32 | ginkgo.Context("when there is a container based on the image to be removed", func() { 33 | ginkgo.BeforeEach(func() { 34 | pullImage(o, localImages[defaultImage]) 35 | command.Run(o, "run", localImages[defaultImage]) 36 | }) 37 | 38 | ginkgo.It("should not be able to remove the image without -f/--force flag", func() { 39 | command.RunWithoutSuccessfulExit(o, "rmi", localImages[defaultImage]) 40 | imageShouldExist(o, localImages[defaultImage]) 41 | }) 42 | 43 | for _, force := range []string{"-f", "--force"} { 44 | ginkgo.It(fmt.Sprintf("should be able to remove the image with %s flag", force), func() { 45 | command.Run(o, "rmi", force, localImages[defaultImage]) 46 | imageShouldNotExist(o, localImages[defaultImage]) 47 | }) 48 | } 49 | }) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /tests/save.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "archive/tar" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "os" 13 | "path/filepath" 14 | 15 | "github.com/onsi/ginkgo/v2" 16 | "github.com/onsi/gomega" 17 | "github.com/onsi/gomega/gbytes" 18 | 19 | "github.com/runfinch/common-tests/command" 20 | "github.com/runfinch/common-tests/ffs" 21 | "github.com/runfinch/common-tests/option" 22 | ) 23 | 24 | type imageManifest struct { 25 | Layers []string 26 | RepoTags []string 27 | } 28 | 29 | // Save tests saving an image to a tar archive. 30 | func Save(o *option.Option) { 31 | ginkgo.Describe("save an image", func() { 32 | var tarFilePath string 33 | var tarFileContext string 34 | ginkgo.BeforeEach(func() { 35 | command.RemoveAll(o) 36 | tarFilePath = ffs.CreateTarFilePath() 37 | tarFileContext = filepath.Join(tarFilePath, "../") 38 | ginkgo.DeferCleanup(os.RemoveAll, tarFileContext) 39 | }) 40 | ginkgo.AfterEach(func() { 41 | command.RemoveAll(o) 42 | }) 43 | 44 | ginkgo.Context("when the images exist", func() { 45 | ginkgo.BeforeEach(func() { 46 | pullImage(o, localImages[defaultImage]) 47 | }) 48 | 49 | ginkgo.It("should save an image to stdout", func() { 50 | stdout := command.New(o, "save", localImages[defaultImage]).WithStdout(gbytes.NewBuffer()).Run().Out 51 | untar(stdout, tarFileContext) 52 | manifestContent := readManifestContent(tarFileContext) 53 | 54 | gomega.Expect(len(manifestContent)).Should(gomega.Equal(1)) 55 | gomega.Expect(manifestContent[0].RepoTags[0]).Should(gomega.Equal(localImages[defaultImage])) 56 | 57 | layersShouldExist(manifestContent[0].Layers, tarFileContext) 58 | }) 59 | 60 | for _, outputOption := range []string{"-o", "--output"} { 61 | ginkgo.It(fmt.Sprintf("should save an image with %s option", outputOption), func() { 62 | command.Run(o, "save", localImages[defaultImage], outputOption, tarFilePath) 63 | 64 | untarFile(tarFilePath, tarFileContext) 65 | 66 | manifestContent := readManifestContent(tarFileContext) 67 | gomega.Expect(len(manifestContent)).Should(gomega.Equal(1)) 68 | gomega.Expect(manifestContent[0].RepoTags[0]).Should(gomega.Equal(localImages[defaultImage])) 69 | layersShouldExist(manifestContent[0].Layers, tarFileContext) 70 | }) 71 | 72 | ginkgo.It(fmt.Sprintf("should save multiple images with %s option", outputOption), func() { 73 | pullImage(o, localImages[olderAlpineImage]) 74 | command.Run(o, "save", outputOption, tarFilePath, localImages[defaultImage], localImages[olderAlpineImage]) 75 | 76 | untarFile(tarFilePath, tarFileContext) 77 | 78 | manifestContent := readManifestContent(tarFileContext) 79 | gomega.Expect(len(manifestContent)).Should(gomega.Equal(2)) 80 | 81 | for i := range manifestContent { 82 | layersShouldExist(manifestContent[i].Layers, tarFileContext) 83 | } 84 | }) 85 | } 86 | }) 87 | 88 | ginkgo.It("should not be able to save an image if the image doesn't exist", func() { 89 | for _, outputOption := range []string{"-o", "--output"} { 90 | command.RunWithoutSuccessfulExit(o, "save", outputOption, tarFilePath, nonexistentImageName) 91 | command.RunWithoutSuccessfulExit(o, "save", outputOption, tarFilePath, nonexistentImageName) 92 | } 93 | }) 94 | }) 95 | } 96 | 97 | func layersShouldExist(layers []string, dir string) { 98 | for _, l := range layers { 99 | layerPath := filepath.Join(dir, l) 100 | _, err := os.Stat(layerPath) 101 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 102 | } 103 | } 104 | 105 | func untarFile(tarFilePath, tarFileContext string) { 106 | reader, err := os.Open(filepath.Clean(tarFilePath)) 107 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 108 | defer func() { 109 | gomega.Expect(reader.Close()).Should(gomega.Succeed()) 110 | }() 111 | untar(reader, tarFileContext) 112 | } 113 | 114 | func untar(reader io.Reader, targetDir string) { 115 | tarReader := tar.NewReader(reader) 116 | for { 117 | header, err := tarReader.Next() 118 | if errors.Is(err, io.EOF) { 119 | break 120 | } 121 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 122 | path := filepath.Join(targetDir, header.Name) //nolint:gosec // The following line resolves G305. 123 | gomega.Expect(path).To(gomega.HavePrefix(filepath.Clean(targetDir))) // https://security.snyk.io/research/zip-slip-vulnerability 124 | info := header.FileInfo() 125 | 126 | if info.IsDir() { 127 | gomega.Expect(os.MkdirAll(path, info.Mode())).Should(gomega.Succeed()) 128 | continue 129 | } 130 | 131 | file, err := os.OpenFile(filepath.Clean(path), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) 132 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 133 | //nolint:gosec // Using io.CopyN to fix G110 seems to be an overkill considering the attack possibility. 134 | _, err = io.Copy(file, tarReader) 135 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 136 | gomega.Expect(file.Close()).Should(gomega.Succeed()) 137 | } 138 | } 139 | 140 | func readManifestContent(tarFileContext string) []imageManifest { 141 | manifestFilePath := filepath.Join(tarFileContext, "manifest.json") 142 | manifestBytes, err := os.ReadFile(filepath.Clean(manifestFilePath)) 143 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 144 | var manifestContent []imageManifest 145 | err = json.Unmarshal(manifestBytes, &manifestContent) 146 | gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) 147 | return manifestContent 148 | } 149 | -------------------------------------------------------------------------------- /tests/start.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | 13 | "github.com/runfinch/common-tests/command" 14 | "github.com/runfinch/common-tests/option" 15 | ) 16 | 17 | // Start tests starting a container. 18 | func Start(o *option.Option) { 19 | ginkgo.Describe("start a container", func() { 20 | ginkgo.BeforeEach(func() { 21 | command.RemoveAll(o) 22 | }) 23 | ginkgo.AfterEach(func() { 24 | command.RemoveAll(o) 25 | }) 26 | 27 | ginkgo.It("should start the container if it is in Exited status", func() { 28 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "nc", "-l") 29 | containerShouldBeRunning(o, testContainerName) 30 | 31 | command.Run(o, "stop", testContainerName) 32 | command.RunWithoutSuccessfulExit(o, "exec", testContainerName, "echo", "foo") 33 | 34 | command.Run(o, "start", testContainerName) 35 | containerShouldBeRunning(o, testContainerName) 36 | }) 37 | 38 | for _, attach := range []string{"--attach", "-a", "-a=true", "--attach=true"} { 39 | ginkgo.It(fmt.Sprintf("with %s flag, should start the container with stdout", attach), func() { 40 | command.Run(o, "create", "--name", testContainerName, localImages[defaultImage], "echo", "foo") 41 | output := command.StdoutStr(o, "start", attach, testContainerName) 42 | gomega.Expect(output).To(gomega.Equal("foo")) 43 | }) 44 | } 45 | 46 | ginkgo.It("should run a container without an init process when --init=false flag is used", func() { 47 | command.Run(o, "run", "--name", testContainerName, "--init=false", localImages[defaultImage], "ps", "-ao", "pid,comm") 48 | psOutput := command.StdoutStr(o, "logs", testContainerName) 49 | 50 | // Split the output into lines 51 | lines := strings.Split(strings.TrimSpace(psOutput), "\n") 52 | 53 | processLine := lines[1] // Second line (after header) 54 | fields := strings.Fields(processLine) 55 | 56 | pid := fields[0] 57 | command := fields[1] 58 | gomega.Expect(pid).To(gomega.Equal("1"), "The only process should have PID 1") 59 | gomega.Expect(command).To(gomega.Equal("ps"), "The only process should be ps") 60 | 61 | // Verify there's no init process 62 | gomega.Expect(psOutput).NotTo(gomega.ContainSubstring("tini"), "There should be no tini process") 63 | }) 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /tests/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/onsi/ginkgo/v2" 8 | "github.com/onsi/gomega" 9 | 10 | "github.com/runfinch/common-tests/command" 11 | "github.com/runfinch/common-tests/option" 12 | ) 13 | 14 | // Stats tests displaying container resource usage statistics. 15 | func Stats(o *option.Option) { 16 | ginkgo.Describe("display a container", func() { 17 | ginkgo.BeforeEach(func() { 18 | command.RemoveAll(o) 19 | }) 20 | ginkgo.AfterEach(func() { 21 | command.RemoveAll(o) 22 | }) 23 | // TODO: add tests for -a flag 24 | // REF issue: https://github.com/containerd/nerdctl/issues/1415 25 | // TODO: add test for streaming data 26 | ginkgo.When("the container is running", func() { 27 | ginkgo.BeforeEach(func() { 28 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "sleep", "infinity") 29 | }) 30 | 31 | ginkgo.It("should disable streaming usage stats and print result with --no-stream flag", func() { 32 | output := command.StdoutStr(o, "stats", "--no-stream", testContainerName, "--format", "{{.Name}}") 33 | gomega.Expect(output).Should(gomega.Equal(testContainerName)) 34 | }) 35 | 36 | ginkgo.It("should not truncate output with --no-trunc flag", func() { 37 | noTruncated := command.StdoutStr(o, "stats", "--no-stream", "--no-trunc", testContainerName, "--format", "{{.ID}}") 38 | truncated := command.StdoutStr(o, "stats", "--no-stream", testContainerName, "--format", "{{.ID}}") 39 | gomega.Expect(len(noTruncated) > len(truncated)).Should(gomega.BeTrue()) 40 | gomega.Expect(noTruncated).Should(gomega.ContainSubstring(truncated)) 41 | }) 42 | }) 43 | 44 | ginkgo.It("should not print usage stats if container doesn't exist", func() { 45 | command.RunWithoutSuccessfulExit(o, "stats", nonexistentContainerName) 46 | }) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /tests/stop.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "time" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | "github.com/onsi/gomega" 12 | 13 | "github.com/runfinch/common-tests/command" 14 | "github.com/runfinch/common-tests/option" 15 | ) 16 | 17 | // Stop tests stopping a container. 18 | func Stop(o *option.Option) { 19 | ginkgo.Describe("stop a container", func() { 20 | ginkgo.BeforeEach(func() { 21 | command.RemoveAll(o) 22 | }) 23 | ginkgo.AfterEach(func() { 24 | command.RemoveAll(o) 25 | }) 26 | 27 | ginkgo.It("should stop the container if the container is running", func() { 28 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "nc", "-l") 29 | containerShouldBeRunning(o, testContainerName) 30 | 31 | command.Run(o, "stop", testContainerName) 32 | command.RunWithoutSuccessfulExit(o, "exec", testContainerName, "echo", "foo") 33 | }) 34 | 35 | for _, timeFlag := range []string{"-t", "--time"} { 36 | ginkgo.It(fmt.Sprintf("should stop running container within specified time by %s flag", timeFlag), func() { 37 | // With PID=1, `sleep infinity` does not exit due to receiving a SIGTERM, which is sent by the stop command. 38 | // Ref. https://superuser.com/a/1299463/730265 39 | command.Run(o, "run", "-d", "--name", testContainerName, localImages[defaultImage], "sleep", "infinity") 40 | gomega.Expect(command.StdoutStr(o, "exec", testContainerName, "echo", "foo")).To(gomega.Equal("foo")) 41 | startTime := time.Now() 42 | command.Run(o, "stop", "-t", "1", testContainerName) 43 | gomega.Expect(time.Since(startTime)).To(gomega.BeNumerically("~", 1*time.Second, 1500*time.Millisecond)) 44 | command.RunWithoutSuccessfulExit(o, "exec", testContainerName, "echo", "foo") 45 | }) 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /tests/tag.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/onsi/ginkgo/v2" 8 | "github.com/onsi/gomega" 9 | 10 | "github.com/runfinch/common-tests/command" 11 | "github.com/runfinch/common-tests/option" 12 | ) 13 | 14 | // Tag tests tagging a container image. 15 | func Tag(o *option.Option) { 16 | ginkgo.Describe("tag a container image", func() { 17 | ginkgo.BeforeEach(func() { 18 | command.RemoveAll(o) 19 | }) 20 | ginkgo.AfterEach(func() { 21 | command.RemoveAll(o) 22 | }) 23 | ginkgo.It("should tag an image when the image exists", func() { 24 | pullImage(o, localImages[defaultImage]) 25 | 26 | command.Run(o, "tag", localImages[defaultImage], testImageName) 27 | defaultImageID := command.Stdout(o, "images", "--quiet", "--no-trunc", localImages[defaultImage]) 28 | taggedImageID := command.Stdout(o, "images", "--quiet", "--no-trunc", testImageName) 29 | gomega.Expect(taggedImageID).ShouldNot(gomega.BeEmpty()) 30 | gomega.Expect(taggedImageID).To(gomega.Equal(defaultImageID)) 31 | }) 32 | ginkgo.It("should not tag an image when the image doesn't exist", func() { 33 | command.RunWithoutSuccessfulExit(o, "tag", nonexistentImageName, testImageName) 34 | imageShouldNotExist(o, testImageName) 35 | }) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /tests/tests.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package tests contains the exported functions that are meant to be imported as test cases. 5 | // 6 | // It should not export any other thing except for a SubcommandOption struct (e.g., LoginOption) that may be added in the future. 7 | // 8 | // Each file contains one subcommand to test and is named after that subcommand. 9 | // Note that the file names are not suffixed with _test so that they can appear in Go Doc. 10 | package tests 11 | 12 | import ( 13 | "fmt" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | "time" 18 | 19 | "github.com/onsi/ginkgo/v2" 20 | "github.com/onsi/gomega" 21 | "github.com/onsi/gomega/gexec" 22 | 23 | "github.com/runfinch/common-tests/fnet" 24 | 25 | "github.com/runfinch/common-tests/command" 26 | "github.com/runfinch/common-tests/ffs" 27 | "github.com/runfinch/common-tests/option" 28 | ) 29 | 30 | const ( 31 | alpineImage = "public.ecr.aws/docker/library/alpine:latest" 32 | testImageName = "test:tag" 33 | nonexistentImageName = "ne-repo:ne-tag" 34 | nonexistentContainerName = "ne-ctr" 35 | testContainerName = "ctr-test" 36 | testContainerName2 = "ctr-test-2" 37 | testVolumeName = "testVol" 38 | registryImage = "public.ecr.aws/docker/library/registry:latest" 39 | localRegistryName = "local-registry" 40 | testUser = "testUser" 41 | testPassword = "testPassword" 42 | sha256RegexFull = "^sha256:[a-f0-9]{64}$" 43 | bridgeNetwork = "bridge" 44 | testNetwork = "test-network" 45 | ) 46 | 47 | type localImage string 48 | 49 | const ( 50 | defaultImage localImage = "defaultImage" 51 | olderAlpineImage localImage = "olderAlpineImage" 52 | amazonLinux2Image localImage = "amazonLinux2Image" 53 | nginxImage localImage = "nginxImage" 54 | ) 55 | 56 | var remoteImages = map[localImage]string{ 57 | defaultImage: alpineImage, 58 | olderAlpineImage: "public.ecr.aws/docker/library/alpine:3.13", 59 | amazonLinux2Image: "public.ecr.aws/amazonlinux/amazonlinux:2", 60 | nginxImage: "public.ecr.aws/docker/library/nginx:latest", 61 | } 62 | 63 | var localImages = map[localImage]string{} 64 | 65 | // CGMode is the cgroups mode of the host system. 66 | // We copy the struct from containerd/cgroups [1] instead of using it as a library 67 | // because it only builds on linux, 68 | // while we don't really need the functions that make it only build on linux 69 | // (e.g., determine the cgroup version of the current host). 70 | // 71 | // [1] https://github.com/containerd/cgroups/blob/cc78c6c1e32dc5bde018d92999910fdace3cfa27/utils.go#L38-L50 72 | type CGMode int 73 | 74 | const ( 75 | // Unavailable cgroup mountpoint. 76 | Unavailable CGMode = iota 77 | // Legacy cgroups v1. 78 | Legacy 79 | // Hybrid with cgroups v1 and v2 controllers mounted. 80 | Hybrid 81 | // Unified with only cgroups v2 mounted. 82 | Unified 83 | 84 | retryPull = 3 85 | ) 86 | 87 | // SetupLocalRegistry can be invoked before running the tests to save time when pulling images during tests. 88 | // 89 | // It spins up a local registry, tags all remoteImages, pushes the new tagged images to the local registry, 90 | // and changes adds corresponding entries to localImages for all of the new tags pushed to local registry. 91 | // 92 | // After all the tests are done, invoke CleanupLocalRegistry to clean up the local registry. 93 | func SetupLocalRegistry(o *option.Option) { 94 | command.RemoveAll(o) 95 | hostPort := fnet.GetFreePort() 96 | containerID := command.StdoutStr(o, "run", "-d", "-p", 97 | fmt.Sprintf("%d:5000", hostPort), "--name", localRegistryName, registryImage) 98 | imageID := command.StdoutStr(o, "images", "-q") 99 | command.SetLocalRegistryContainerID(containerID) 100 | command.SetLocalRegistryImageID(imageID) 101 | command.SetLocalRegistryImageName(registryImage) 102 | 103 | for k, ref := range remoteImages { 104 | // split image tag according to spec 105 | // https://github.com/distribution/distribution/blob/d0deff9cd6c2b8c82c6f3d1c713af51df099d07b/reference/reference.go 106 | _, name, _ := strings.Cut(ref, "/") 107 | // allow up to a minute for remote pulls to account for external network 108 | // latency/throughput issues or throttling (default is 10 seconds) 109 | // retry pull for 3 times. 110 | var session *gexec.Session 111 | exitCode := -1 112 | for i := 0; i < retryPull; i++ { 113 | session = command.New(o, "pull", ref).WithoutWait().Run() 114 | select { 115 | case <-session.Exited: 116 | exitCode = session.ExitCode() 117 | case <-time.After(30 * time.Second): 118 | fmt.Printf("Timeout occurred, command hasn't exited yet (attempt %d)", i) 119 | session.Kill() 120 | } 121 | if exitCode == 0 { 122 | break 123 | } 124 | } 125 | if exitCode != 0 { 126 | ginkgo.Fail("Failed to pull image " + ref) 127 | } 128 | 129 | localRef := fmt.Sprintf("localhost:%d/%s", hostPort, name) 130 | command.Run(o, "tag", ref, localRef) 131 | command.Run(o, "push", localRef) 132 | command.Run(o, "rmi", ref) 133 | localImages[k] = localRef 134 | } 135 | } 136 | 137 | // CleanupLocalRegistry removes the local registry container and image. It's used together with SetupLocalRegistry, 138 | // and should be invoked after running all the tests. 139 | func CleanupLocalRegistry(o *option.Option) { 140 | containerID := command.StdoutStr(o, "inspect", localRegistryName, "--format", "{{.ID}}") 141 | command.Run(o, "rm", "-f", containerID) 142 | imageID := command.StdoutStr(o, "images", "-q") 143 | command.Run(o, "rmi", "-f", imageID) 144 | localImages = map[localImage]string{} 145 | } 146 | 147 | func pullImage(o *option.Option, imageName string) { 148 | command.Run(o, "pull", "-q", imageName) 149 | imageID := command.Stdout(o, "images", "--quiet", imageName) 150 | gomega.Expect(imageID).ShouldNot(gomega.BeEmpty()) 151 | } 152 | 153 | func removeImage(o *option.Option, imageName string) { 154 | command.Run(o, "rmi", "--force", imageName) 155 | imageID := command.Stdout(o, "images", "--quiet", imageName) 156 | gomega.Expect(string(imageID)).Should(gomega.BeEmpty()) 157 | } 158 | 159 | func containerShouldBeRunning(o *option.Option, containerNames ...string) { 160 | for _, containerName := range containerNames { 161 | gomega.Expect(command.Stdout(o, "ps", "-q", "--filter", 162 | fmt.Sprintf("name=%s", containerName))).NotTo(gomega.BeEmpty()) 163 | } 164 | } 165 | 166 | func containerShouldNotBeRunning(o *option.Option, containerNames ...string) { 167 | for _, containerName := range containerNames { 168 | gomega.Expect(command.Stdout(o, "ps", "-q", "--filter", 169 | fmt.Sprintf("name=%s", containerName))).To(gomega.BeEmpty()) 170 | } 171 | } 172 | 173 | func containerShouldExist(o *option.Option, containerNames ...string) { 174 | for _, containerName := range containerNames { 175 | gomega.Expect(command.Stdout(o, "ps", "-a", "-q", "--filter", 176 | fmt.Sprintf("name=%s", containerName))).NotTo(gomega.BeEmpty()) 177 | } 178 | } 179 | 180 | func containerShouldNotExist(o *option.Option, containerNames ...string) error { 181 | for _, containerName := range containerNames { 182 | containerExists := command.Stdout(o, "ps", "-a", "-q", "--filter", 183 | fmt.Sprintf("name=%s", containerName)) 184 | if len(containerExists) > 0 { 185 | return fmt.Errorf("containerd '%s' exists but should not", containerName) 186 | } 187 | } 188 | return nil 189 | } 190 | 191 | func imageShouldExist(o *option.Option, imageName string) { 192 | gomega.Expect(command.Stdout(o, "images", "-q", imageName)).NotTo(gomega.BeEmpty()) 193 | } 194 | 195 | func imageShouldNotExist(o *option.Option, imageName string) { 196 | gomega.Expect(command.Stdout(o, "images", "-q", imageName)).To(gomega.BeEmpty()) 197 | } 198 | 199 | func volumeShouldExist(o *option.Option, volumeName string) { 200 | gomega.Expect(command.Stdout(o, "volume", "ls", "-q", "--filter", 201 | fmt.Sprintf("name=%s", volumeName))).NotTo(gomega.BeEmpty()) 202 | } 203 | 204 | func volumeShouldNotExist(o *option.Option, volumeName string) { 205 | gomega.Expect(command.Stdout(o, "volume", "ls", "-q", "--filter", 206 | fmt.Sprintf("name=%s", volumeName))).To(gomega.BeEmpty()) 207 | } 208 | 209 | func fileShouldExist(path, content string) { 210 | gomega.Expect(path).To(gomega.BeARegularFile()) 211 | actualContent, err := os.ReadFile(filepath.Clean(path)) 212 | gomega.Expect(err).ToNot(gomega.HaveOccurred()) 213 | gomega.Expect(string(actualContent)).To(gomega.Equal(content)) 214 | } 215 | 216 | func fileShouldNotExist(path string) { 217 | gomega.Expect(path).ToNot(gomega.BeAnExistingFile()) 218 | } 219 | 220 | func fileShouldExistInContainer(o *option.Option, containerName, path, content string) { 221 | gomega.Expect(command.StdoutStr(o, "exec", containerName, "cat", path)).To(gomega.Equal(content)) 222 | } 223 | 224 | func fileShouldNotExistInContainer(o *option.Option, containerName, path string) { 225 | cmdOut := command.RunWithoutSuccessfulExit(o, "exec", containerName, "cat", path) 226 | gomega.Expect(cmdOut.Err.Contents()).To(gomega.ContainSubstring("No such file or directory")) 227 | } 228 | 229 | func buildImage(o *option.Option, imageName string) { 230 | dockerfile := fmt.Sprintf(`FROM %s 231 | CMD ["echo", "finch-test-dummy-output"] 232 | `, localImages[defaultImage]) 233 | buildContext := ffs.CreateBuildContext(dockerfile) 234 | ginkgo.DeferCleanup(os.RemoveAll, buildContext) 235 | command.Run(o, "build", "-q", "-t", imageName, buildContext) 236 | } 237 | -------------------------------------------------------------------------------- /tests/volume_create.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega" 11 | 12 | "github.com/runfinch/common-tests/command" 13 | "github.com/runfinch/common-tests/option" 14 | ) 15 | 16 | // VolumeCreate tests "volume create" command that creates a volume. 17 | func VolumeCreate(o *option.Option) { 18 | ginkgo.Describe("create a volume", func() { 19 | ginkgo.BeforeEach(func() { 20 | command.RemoveAll(o) 21 | }) 22 | ginkgo.AfterEach(func() { 23 | command.RemoveAll(o) 24 | }) 25 | 26 | ginkgo.It("should create a volume with name", func() { 27 | command.Run(o, "volume", "create", testVolumeName) 28 | volumeShouldExist(o, testVolumeName) 29 | }) 30 | 31 | ginkgo.It("data in volume should be shared between containers", func() { 32 | command.Run(o, "volume", "create", testVolumeName) 33 | command.Run( 34 | o, 35 | "run", 36 | "-v", 37 | fmt.Sprintf("%s:/tmp", testVolumeName), 38 | localImages[defaultImage], 39 | "sh", "-c", "echo foo > /tmp/test.txt", 40 | ) 41 | output := command.StdoutStr( 42 | o, 43 | "run", 44 | "-v", 45 | fmt.Sprintf("%s:/tmp", testVolumeName), 46 | localImages[defaultImage], 47 | "cat", 48 | "/tmp/test.txt", 49 | ) 50 | gomega.Expect(output).Should(gomega.Equal("foo")) 51 | }) 52 | 53 | ginkgo.It("should create a volume with label with --label flag", func() { 54 | command.Run(o, "volume", "create", "--label", "label=tag", testVolumeName) 55 | output := command.StdoutStr(o, "volume", "inspect", testVolumeName, "--format", "{{.Labels.label}}") 56 | gomega.Expect(output).Should(gomega.Equal("tag")) 57 | }) 58 | 59 | ginkgo.It("should create multiple labels with --label flag", func() { 60 | command.Run(o, "volume", "create", "--label", "label=tag", "--label", "label1=tag1", testVolumeName) 61 | tag := command.StdoutStr(o, "volume", "inspect", testVolumeName, "--format", "{{.Labels.label}}") 62 | tag1 := command.StdoutStr(o, "volume", "inspect", testVolumeName, "--format", "{{.Labels.label1}}") 63 | gomega.Expect(tag).Should(gomega.Equal("tag")) 64 | gomega.Expect(tag1).Should(gomega.Equal("tag1")) 65 | }) 66 | 67 | ginkgo.It("should not create a volume if the volume with the same name exists", func() { 68 | // TODO(macedonv): remove entire test after nerdctl v2 is supported on all platforms. 69 | if o.IsNerdctlV2() { 70 | ginkgo.Skip("Behavior is not supported on nerdctl v2") 71 | } 72 | command.Run(o, "volume", "create", testVolumeName) 73 | command.RunWithoutSuccessfulExit(o, "volume", "create", testVolumeName) 74 | }) 75 | 76 | ginkgo.It("should warn volume already exists if a volume with the same name exists", func() { 77 | // TODO(macedonv): remove check when nerdctl v2 is supported on all platforms. 78 | if o.IsNerdctlV1() { 79 | ginkgo.Skip("Behavior is not supported on nerdctl v1") 80 | } 81 | command.Run(o, "volume", "create", testVolumeName) 82 | session := command.Run(o, "volume", "create", testVolumeName) 83 | gomega.Expect(string(session.Err.Contents())).Should(gomega.ContainSubstring("already exists")) 84 | gomega.Expect(string(session.Out.Contents())).Should(gomega.ContainSubstring(testVolumeName)) 85 | }) 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /tests/volume_inspect.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/onsi/ginkgo/v2" 8 | "github.com/onsi/gomega" 9 | 10 | "github.com/runfinch/common-tests/command" 11 | "github.com/runfinch/common-tests/option" 12 | ) 13 | 14 | // VolumeInspect tests "volume inspect" command that displays detailed information on one or more volumes. 15 | func VolumeInspect(o *option.Option) { 16 | ginkgo.Describe("display detailed volume on a volume", func() { 17 | ginkgo.BeforeEach(func() { 18 | command.RemoveAll(o) 19 | }) 20 | ginkgo.AfterEach(func() { 21 | command.RemoveAll(o) 22 | }) 23 | 24 | ginkgo.It("should display the detailed information of volume", func() { 25 | command.Run(o, "volume", "create", testVolumeName) 26 | name := command.StdoutStr(o, "volume", "inspect", testVolumeName, "--format", "{{.Name}}") 27 | gomega.Expect(name).Should(gomega.Equal(testVolumeName)) 28 | mp := command.StdoutStr(o, "volume", "inspect", testVolumeName, "--format", "{{.Mountpoint}}") 29 | gomega.Expect(mp).ShouldNot(gomega.BeEmpty()) 30 | }) 31 | 32 | ginkgo.It("should display detailed information of multiple volumes", func() { 33 | const testVol2 = "testVol2" 34 | command.Run(o, "volume", "create", testVolumeName) 35 | command.Run(o, "volume", "create", testVol2) 36 | lines := command.StdoutAsLines(o, "volume", "inspect", testVolumeName, "testVol2", "--format", "{{.Name}}") 37 | gomega.Expect(lines).Should(gomega.ContainElements(testVolumeName, testVol2)) 38 | }) 39 | 40 | ginkgo.It("should have error if inspect a nonexistent volume", func() { 41 | command.RunWithoutSuccessfulExit(o, "volume", "inspect", "ne-volume") 42 | }) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /tests/volume_ls.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega" 11 | 12 | "github.com/runfinch/common-tests/command" 13 | "github.com/runfinch/common-tests/option" 14 | ) 15 | 16 | // VolumeLs tests "volume ls" command that lists volumes. 17 | func VolumeLs(o *option.Option) { 18 | ginkgo.Describe("list volumes", func() { 19 | ginkgo.BeforeEach(func() { 20 | command.RemoveAll(o) 21 | }) 22 | ginkgo.AfterEach(func() { 23 | command.RemoveAll(o) 24 | }) 25 | // TODO: add test for --filter after upgrading to nerdctl v0.23 26 | ginkgo.It("should display all the volumes", func() { 27 | const testVol2 = "testVol2" 28 | command.Run(o, "volume", "create", testVolumeName) 29 | command.Run(o, "volume", "create", testVol2) 30 | lines := command.StdoutAsLines(o, "volume", "ls", "--format", "{{.Name}}") 31 | gomega.Expect(lines).Should(gomega.ContainElements(testVolumeName, testVol2)) 32 | }) 33 | 34 | for _, quiet := range []string{"--quiet", "-q"} { 35 | ginkgo.It(fmt.Sprintf("should only display volume names with %s flag", quiet), func() { 36 | command.Run(o, "volume", "create", testVolumeName) 37 | gomega.Expect(command.StdoutAsLines(o, "volume", "ls", quiet)).Should(gomega.ContainElement(testVolumeName)) 38 | }) 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /tests/volume_prune.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega/gbytes" 11 | 12 | "github.com/runfinch/common-tests/command" 13 | "github.com/runfinch/common-tests/option" 14 | ) 15 | 16 | // VolumePrune tests "volume prune" command that removes all unused volumes. 17 | func VolumePrune(o *option.Option) { 18 | ginkgo.Describe("remove all unused volumes", func() { 19 | ginkgo.BeforeEach(func() { 20 | command.RemoveAll(o) 21 | }) 22 | ginkgo.AfterEach(func() { 23 | command.RemoveAll(o) 24 | }) 25 | 26 | ginkgo.It("should not remove a volume if it is used by a container", func() { 27 | command.Run(o, "run", "-v", fmt.Sprintf("%s:/tmp", testVolumeName), "--name", testContainerName, localImages[defaultImage]) 28 | command.Run(o, "volume", "prune", "--force", "--all") 29 | volumeShouldExist(o, testVolumeName) 30 | }) 31 | 32 | ginkgo.It("should remove all unused volumes with inputting y in prompt confirmation", func() { 33 | command.Run(o, "volume", "create", testVolumeName) 34 | command.New(o, "volume", "prune", "--all").WithStdin(gbytes.BufferWithBytes([]byte("y"))).Run() 35 | volumeShouldNotExist(o, testVolumeName) 36 | }) 37 | 38 | ginkgo.It("should not remove all unused volumes with inputting n in prompt confirmation", func() { 39 | command.Run(o, "volume", "create", testVolumeName) 40 | command.New(o, "volume", "prune", "--all").WithStdin(gbytes.BufferWithBytes([]byte("n"))).Run() 41 | volumeShouldExist(o, testVolumeName) 42 | }) 43 | 44 | for _, force := range []string{"--force", "-f"} { 45 | ginkgo.It(fmt.Sprintf("should remove all unused volumes without prompting for confirmation with %s flag", force), func() { 46 | command.Run(o, "volume", "create", testVolumeName) 47 | command.Run(o, "volume", "prune", force, "--all") 48 | volumeShouldNotExist(o, testVolumeName) 49 | }) 50 | } 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /tests/volume_rm.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega" 11 | 12 | "github.com/runfinch/common-tests/command" 13 | "github.com/runfinch/common-tests/option" 14 | ) 15 | 16 | // VolumeRm tests "volume rm" command that removes one or more volumes. 17 | func VolumeRm(o *option.Option) { 18 | ginkgo.Describe("remove a volume", func() { 19 | ginkgo.BeforeEach(func() { 20 | command.RemoveAll(o) 21 | }) 22 | ginkgo.AfterEach(func() { 23 | command.RemoveAll(o) 24 | }) 25 | 26 | ginkgo.When("volumes are not used by any container", func() { 27 | ginkgo.BeforeEach(func() { 28 | command.Run(o, "volume", "create", testVolumeName) 29 | }) 30 | ginkgo.It("should remove a volume", func() { 31 | volumeShouldExist(o, testVolumeName) 32 | command.Run(o, "volume", "rm", testVolumeName) 33 | volumeShouldNotExist(o, testVolumeName) 34 | }) 35 | 36 | ginkgo.It("should remove multiple volumes", func() { 37 | const testVol2 = "testVol2" 38 | command.Run(o, "volume", "create", "testVol2") 39 | gomega.Expect(command.StdoutAsLines(o, "volume", "ls", "--quiet")).Should(gomega.ContainElements(testVolumeName, testVol2)) 40 | command.Run(o, "volume", "rm", testVolumeName, testVol2) 41 | volumeShouldNotExist(o, testVolumeName) 42 | }) 43 | }) 44 | 45 | ginkgo.When("a volume is used by a container", func() { 46 | ginkgo.BeforeEach(func() { 47 | command.Run(o, "volume", "create", testVolumeName) 48 | command.Run(o, "run", "-v", fmt.Sprintf("%s:/tmp", testVolumeName), localImages[defaultImage]) 49 | }) 50 | 51 | // It's expected that `volume rm` can't remove the volume that is referenced to a container despite the container status. 52 | // REF - https://github.com/containerd/nerdctl/blob/657cf4be42f9e99ee0fd53103d4ded62d7137aa3/cmd/nerdctl/volume_rm.go#L36 53 | // TODO: add test for --force/-f after they are implemented. 54 | // REF - https://github.com/containerd/nerdctl/blob/657cf4be42f9e99ee0fd53103d4ded62d7137aa3/cmd/nerdctl/volume_rm.go#L43 55 | ginkgo.It("should not remove the volume that is referenced to a container", func() { 56 | command.RunWithoutSuccessfulExit(o, "volume", "rm", testVolumeName) 57 | volumeShouldExist(o, testVolumeName) 58 | }) 59 | }) 60 | }) 61 | } 62 | --------------------------------------------------------------------------------