├── .dockerignore ├── .editorconfig ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── SUPPORT.md ├── dependabot.yml ├── labels.yml └── workflows │ ├── build.yml │ ├── codeql.yml │ ├── labels.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── docker-bake.hcl ├── go.mod ├── go.sum ├── hack ├── lint.Dockerfile ├── test.sh └── vendor.Dockerfile ├── main.go ├── setup-user.go └── version.go /.dockerignore: -------------------------------------------------------------------------------- 1 | /bin 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 2 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.go] 15 | indent_style = tab 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) 4 | to the public under the [project's open source license](../LICENSE). 5 | 6 | ## Submitting a pull request 7 | 8 | 1. [Fork](https://github.com/crazy-max/yasu/fork) and clone the repository 9 | 2. Configure and install the dependencies: `go mod download` 10 | 3. Create a new branch: `git checkout -b my-branch-name` 11 | 4. Make your changes 12 | 5. Validate: `docker buildx bake validate` 13 | 6. Test your code: `docker buildx bake test` 14 | 7. Build the project: `docker buildx bake artifact-all image-all` 15 | 8. Push to your fork and [submit a pull request](https://github.com/crazy-max/yasu/compare) 16 | 9. Pat your self on the back and wait for your pull request to be reviewed and merged. 17 | 18 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 19 | 20 | * Make sure the `README.md` and any other relevant **documentation are kept up-to-date**. 21 | * I try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 22 | * Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as **separate pull requests**. 23 | * Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 24 | 25 | ## Resources 26 | 27 | * [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 28 | * [Using Pull Requests](https://docs.github.com/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-pull-requests) 29 | * [GitHub Help](https://docs.github.com) 30 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: crazy-max 2 | custom: https://www.paypal.me/crazyws 3 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support [![](https://isitmaintained.com/badge/resolution/crazy-max/yasu.svg)](https://isitmaintained.com/project/crazy-max/yasu) 2 | 3 | First, [be a good guy](https://github.com/kossnocorp/etiquette/blob/master/README.md). 4 | 5 | ## Reporting an issue 6 | 7 | Please do a search in [open issues](https://github.com/crazy-max/yasu/issues?utf8=%E2%9C%93&q=) to see if the issue 8 | or feature request has already been filed. 9 | 10 | If you find your issue already exists, make relevant comments and add your 11 | [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in 12 | place of a "+1" comment. 13 | 14 | :+1: - upvote 15 | 16 | :-1: - downvote 17 | 18 | If you cannot find an existing issue that describes your bug or feature, submit an issue using the guidelines below. 19 | 20 | ## Writing good bug reports and feature requests 21 | 22 | File a single issue per problem and feature request. 23 | 24 | * Do not enumerate multiple bugs or feature requests in the same issue. 25 | * Do not add your issue as a comment to an existing issue unless it's for the identical input. Many issues look similar, but have different causes. 26 | 27 | The more information you can provide, the more likely someone will be successful reproducing the issue and finding 28 | a fix. 29 | 30 | You are now ready to [create a new issue](https://github.com/crazy-max/yasu/issues/new/choose)! 31 | 32 | ## Closure policy 33 | 34 | * Issues that don't have the information requested above (when applicable) will be closed immediately and the poster directed to the support guidelines. 35 | * Issues that go a week without a response from original poster are subject to closure at my discretion. 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | open-pull-requests-limit: 10 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | time: "08:00" 9 | timezone: "Europe/Paris" 10 | labels: 11 | - ":game_die: dependencies" 12 | - ":robot: bot" 13 | - package-ecosystem: "github-actions" 14 | open-pull-requests-limit: 10 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | time: "08:00" 19 | timezone: "Europe/Paris" 20 | labels: 21 | - ":game_die: dependencies" 22 | - ":robot: bot" 23 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | ## more info https://github.com/crazy-max/ghaction-github-labeler 2 | - # automerge 3 | name: ":bell: automerge" 4 | color: "8f4fbc" 5 | description: "" 6 | - # bot 7 | name: ":robot: bot" 8 | color: "69cde9" 9 | description: "" 10 | - # bug 11 | name: ":bug: bug" 12 | color: "b60205" 13 | description: "" 14 | - # dependencies 15 | name: ":game_die: dependencies" 16 | color: "0366d6" 17 | description: "" 18 | - # documentation 19 | name: ":memo: documentation" 20 | color: "c5def5" 21 | description: "" 22 | - # duplicate 23 | name: ":busts_in_silhouette: duplicate" 24 | color: "cccccc" 25 | description: "" 26 | - # enhancement 27 | name: ":sparkles: enhancement" 28 | color: "0054ca" 29 | description: "" 30 | - # feature request 31 | name: ":bulb: feature request" 32 | color: "0e8a16" 33 | description: "" 34 | - # feedback 35 | name: ":mega: feedback" 36 | color: "03a9f4" 37 | description: "" 38 | - # future maybe 39 | name: ":rocket: future maybe" 40 | color: "fef2c0" 41 | description: "" 42 | - # good first issue 43 | name: ":hatching_chick: good first issue" 44 | color: "7057ff" 45 | description: "" 46 | - # help wanted 47 | name: ":pray: help wanted" 48 | color: "4caf50" 49 | description: "" 50 | - # invalid 51 | name: ":no_entry_sign: invalid" 52 | color: "e6e6e6" 53 | description: "" 54 | - # investigate 55 | name: ":mag: investigate" 56 | color: "e6625b" 57 | description: "" 58 | - # needs more info 59 | name: ":thinking: needs more info" 60 | color: "795548" 61 | description: "" 62 | - # pinned 63 | name: ":pushpin: pinned" 64 | color: "28008e" 65 | description: "" 66 | - # question 67 | name: ":question: question" 68 | color: "3f51b5" 69 | description: "" 70 | - # sponsor 71 | name: ":sparkling_heart: sponsor" 72 | color: "fedbf0" 73 | description: "" 74 | - # stale 75 | name: ":skull: stale" 76 | color: "237da0" 77 | description: "" 78 | - # upstream 79 | name: ":eyes: upstream" 80 | color: "fbca04" 81 | description: "" 82 | - # wontfix 83 | name: ":coffin: wontfix" 84 | color: "ffffff" 85 | description: "" 86 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 8 | permissions: 9 | contents: read 10 | 11 | on: 12 | push: 13 | branches: 14 | - 'master' 15 | tags: 16 | - 'v*' 17 | pull_request: 18 | 19 | env: 20 | DOCKERHUB_SLUG: crazymax/yasu 21 | GHCR_SLUG: ghcr.io/crazy-max/yasu 22 | DESTDIR: ./bin 23 | 24 | jobs: 25 | prepare: 26 | runs-on: ubuntu-latest 27 | outputs: 28 | validate-targets: ${{ steps.validate-targets.outputs.matrix }} 29 | artifact-platforms: ${{ steps.artifact-platforms.outputs.matrix }} 30 | steps: 31 | - 32 | name: Checkout 33 | uses: actions/checkout@v4 34 | - 35 | name: Validate targets matrix 36 | id: validate-targets 37 | run: | 38 | echo "matrix=$(docker buildx bake validate --print | jq -cr '.target | keys')" >> $GITHUB_OUTPUT 39 | - 40 | name: Artifact platforms matrix 41 | id: artifact-platforms 42 | run: | 43 | echo "matrix=$(docker buildx bake artifact-all --print | jq -cr '.target."artifact-all".platforms')" >> $GITHUB_OUTPUT 44 | 45 | validate: 46 | runs-on: ubuntu-latest 47 | needs: 48 | - prepare 49 | strategy: 50 | fail-fast: false 51 | matrix: 52 | target: ${{ fromJson(needs.prepare.outputs.validate-targets) }} 53 | steps: 54 | - 55 | name: Checkout 56 | uses: actions/checkout@v4 57 | - 58 | name: Set up Docker Buildx 59 | uses: docker/setup-buildx-action@v3 60 | - 61 | name: Validate 62 | uses: docker/bake-action@v5 63 | with: 64 | targets: ${{ matrix.target }} 65 | 66 | test: 67 | runs-on: ubuntu-latest 68 | steps: 69 | - 70 | name: Checkout 71 | uses: actions/checkout@v4 72 | - 73 | name: Set up Docker Buildx 74 | uses: docker/setup-buildx-action@v3 75 | - 76 | name: Test 77 | uses: docker/bake-action@v5 78 | with: 79 | targets: test 80 | 81 | artifact: 82 | runs-on: ubuntu-latest 83 | needs: 84 | - prepare 85 | - validate 86 | strategy: 87 | fail-fast: false 88 | matrix: 89 | platform: ${{ fromJson(needs.prepare.outputs.artifact-platforms) }} 90 | steps: 91 | - 92 | name: Prepare 93 | run: | 94 | platform=${{ matrix.platform }} 95 | echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV 96 | - 97 | name: Checkout 98 | uses: actions/checkout@v4 99 | with: 100 | fetch-depth: 0 101 | - 102 | name: Set up QEMU 103 | uses: docker/setup-qemu-action@v3 104 | - 105 | name: Set up Docker Buildx 106 | uses: docker/setup-buildx-action@v3 107 | - 108 | name: Build 109 | uses: docker/bake-action@v5 110 | with: 111 | targets: artifact 112 | provenance: mode=max 113 | sbom: true 114 | pull: true 115 | set: | 116 | *.platform=${{ matrix.platform }} 117 | *.cache-from=type=gha,scope=artifact-${{ env.PLATFORM_PAIR }} 118 | *.cache-to=type=gha,scope=artifact-${{ env.PLATFORM_PAIR }},mode=max 119 | - 120 | name: Rename provenance and sbom 121 | working-directory: ${{ env.DESTDIR }}/artifact 122 | run: | 123 | binname=$(find . -name 'yasu_*') 124 | filename=$(basename "$binname" | sed -E 's/\.(tar\.gz|zip)$//') 125 | mv "provenance.json" "${filename}.provenance.json" 126 | mv "sbom-binary.spdx.json" "${filename}.sbom.json" 127 | find . -name 'sbom*.json' -exec rm {} \; 128 | - 129 | name: List artifacts 130 | run: | 131 | tree -nh ${{ env.DESTDIR }} 132 | - 133 | name: Upload artifact 134 | uses: actions/upload-artifact@v4 135 | with: 136 | name: yasu-${{ env.PLATFORM_PAIR }} 137 | path: ${{ env.DESTDIR }} 138 | if-no-files-found: error 139 | 140 | release: 141 | runs-on: ubuntu-latest 142 | permissions: 143 | # required to create GitHub release 144 | contents: write 145 | needs: 146 | - artifact 147 | - test 148 | steps: 149 | - 150 | name: Checkout 151 | uses: actions/checkout@v4 152 | - 153 | name: Download artifacts 154 | uses: actions/download-artifact@v4 155 | with: 156 | path: ${{ env.DESTDIR }} 157 | pattern: yasu-* 158 | merge-multiple: true 159 | - 160 | name: List artifacts 161 | run: | 162 | tree -nh ${{ env.DESTDIR }} 163 | - 164 | name: Set up Docker Buildx 165 | uses: docker/setup-buildx-action@v3 166 | - 167 | name: Build 168 | uses: docker/bake-action@v5 169 | with: 170 | targets: release 171 | provenance: false 172 | - 173 | name: GitHub Release 174 | uses: softprops/action-gh-release@v2 175 | if: startsWith(github.ref, 'refs/tags/') 176 | with: 177 | draft: true 178 | files: | 179 | ${{ env.DESTDIR }}/release/* 180 | env: 181 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 182 | 183 | image: 184 | runs-on: ubuntu-latest 185 | permissions: 186 | # same as global permissions 187 | contents: read 188 | # required to push to GHCR 189 | packages: write 190 | needs: 191 | - artifact 192 | - test 193 | steps: 194 | - 195 | name: Checkout 196 | uses: actions/checkout@v4 197 | with: 198 | fetch-depth: 0 199 | - 200 | name: Prepare 201 | run: | 202 | cfroms= 203 | while read -r platform; do 204 | if [ -n "$cfroms" ]; then cfroms="${cfroms}\n"; fi 205 | cfroms="${cfroms}*.cache-from=type=gha,scope=artifact-${platform//\//-}" 206 | done < <(docker buildx bake artifact-all --print | jq -r '.target."artifact-all".platforms[]') 207 | echo "CACHE_FROMS<> $GITHUB_ENV 208 | echo -e "$cfroms" >> $GITHUB_ENV 209 | echo "EOF" >> $GITHUB_ENV 210 | - 211 | name: Docker meta 212 | id: meta 213 | uses: docker/metadata-action@v5 214 | with: 215 | images: | 216 | ${{ env.DOCKERHUB_SLUG }} 217 | ${{ env.GHCR_SLUG }} 218 | tags: | 219 | type=semver,pattern={{version}} 220 | type=ref,event=pr 221 | type=edge 222 | labels: | 223 | org.opencontainers.image.title=yasu 224 | org.opencontainers.image.description=Yet Another Switch User 225 | - 226 | name: Set up QEMU 227 | uses: docker/setup-qemu-action@v3 228 | - 229 | name: Set up Docker Buildx 230 | uses: docker/setup-buildx-action@v3 231 | - 232 | name: Login to DockerHub 233 | if: github.event_name != 'pull_request' 234 | uses: docker/login-action@v3 235 | with: 236 | username: ${{ secrets.DOCKER_USERNAME }} 237 | password: ${{ secrets.DOCKER_PASSWORD }} 238 | - 239 | name: Login to GHCR 240 | if: github.event_name != 'pull_request' 241 | uses: docker/login-action@v3 242 | with: 243 | registry: ghcr.io 244 | username: ${{ github.repository_owner }} 245 | password: ${{ secrets.GITHUB_TOKEN }} 246 | - 247 | name: Build 248 | uses: docker/bake-action@v5 249 | with: 250 | files: | 251 | ./docker-bake.hcl 252 | ${{ steps.meta.outputs.bake-file }} 253 | targets: image-all 254 | provenance: mode=max 255 | sbom: true 256 | pull: true 257 | push: ${{ github.event_name != 'pull_request' }} 258 | set: | 259 | ${{ env.CACHE_FROMS }} 260 | - 261 | name: Check manifest 262 | if: github.event_name != 'pull_request' 263 | run: | 264 | docker buildx imagetools inspect ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }} 265 | docker buildx imagetools inspect ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }} 266 | - 267 | name: Inspect image 268 | if: github.event_name != 'pull_request' 269 | run: | 270 | docker pull ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }} 271 | docker image inspect ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }} 272 | docker pull ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }} 273 | docker image inspect ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }} 274 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: codeql 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 8 | permissions: 9 | contents: read 10 | 11 | on: 12 | schedule: 13 | - cron: '0 12 * * 6' 14 | push: 15 | branches: 16 | - 'master' 17 | tags: 18 | - '*' 19 | pull_request: 20 | 21 | jobs: 22 | codeql: 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # same as global permissions 26 | contents: read 27 | # required for code scanning 28 | security-events: write 29 | steps: 30 | - 31 | name: Checkout 32 | uses: actions/checkout@v4 33 | with: 34 | fetch-depth: 2 35 | - 36 | name: Checkout HEAD on PR 37 | if: ${{ github.event_name == 'pull_request' }} 38 | run: | 39 | git checkout HEAD^2 40 | - 41 | name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: go 45 | - 46 | name: Autobuild 47 | uses: github/codeql-action/autobuild@v3 48 | - 49 | name: Perform CodeQL Analysis 50 | uses: github/codeql-action/analyze@v3 51 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | name: labels 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 8 | permissions: 9 | contents: read 10 | 11 | on: 12 | push: 13 | branches: 14 | - 'master' 15 | paths: 16 | - '.github/labels.yml' 17 | - '.github/workflows/labels.yml' 18 | pull_request: 19 | paths: 20 | - '.github/labels.yml' 21 | - '.github/workflows/labels.yml' 22 | 23 | jobs: 24 | labeler: 25 | runs-on: ubuntu-latest 26 | permissions: 27 | # same as global permissions 28 | contents: read 29 | # required to update labels 30 | issues: write 31 | steps: 32 | - 33 | name: Checkout 34 | uses: actions/checkout@v4 35 | - 36 | name: Run Labeler 37 | uses: crazy-max/ghaction-github-labeler@v5 38 | with: 39 | dry-run: ${{ github.event_name == 'pull_request' }} 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 8 | permissions: 9 | contents: read 10 | 11 | on: 12 | push: 13 | branches: 14 | - 'master' 15 | pull_request: 16 | 17 | jobs: 18 | alpine: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | variant: 24 | - 3.14 25 | - 3.15 26 | - 3.16 27 | - edge 28 | steps: 29 | - 30 | name: Checkout 31 | uses: actions/checkout@v4 32 | - 33 | name: Test 34 | uses: docker/bake-action@v5 35 | with: 36 | targets: test-alpine 37 | env: 38 | TEST_ALPINE_VARIANT: ${{ matrix.variant }} 39 | 40 | debian: 41 | runs-on: ubuntu-latest 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | variant: 46 | - buster 47 | - bullseye 48 | - sid 49 | steps: 50 | - 51 | name: Checkout 52 | uses: actions/checkout@v4 53 | - 54 | name: Test 55 | uses: docker/bake-action@v5 56 | with: 57 | targets: test-debian 58 | env: 59 | TEST_DEBIAN_VARIANT: ${{ matrix.variant }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 10m 3 | 4 | linters: 5 | enable: 6 | - depguard 7 | - gofmt 8 | - goimports 9 | - revive 10 | - govet 11 | - importas 12 | - ineffassign 13 | - misspell 14 | - typecheck 15 | - errname 16 | - makezero 17 | - whitespace 18 | - unused 19 | disable-all: true 20 | 21 | linters-settings: 22 | depguard: 23 | rules: 24 | main: 25 | deny: 26 | # The io/ioutil package has been deprecated. 27 | # https://go.dev/doc/go1.16#ioutil 28 | - pkg: "io/ioutil" 29 | desc: The io/ioutil package has been deprecated. 30 | importas: 31 | no-unaliased: true 32 | 33 | issues: 34 | exclude-rules: 35 | - linters: 36 | - revive 37 | text: "stutters" 38 | 39 | # show all 40 | max-issues-per-linter: 0 41 | max-same-issues: 0 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.20.1 (2023/12/21) 4 | 5 | * Fix release archive extension (#61) 6 | 7 | ## 1.20.0 (2023/12/17) 8 | 9 | * Cherry-picks from upstream (#55) 10 | * Use `github.com/moby/sys/user` (https://github.com/tianon/gosu/pull/134) 11 | * Check for `setuid` to also disallow `setgid` (https://github.com/tianon/gosu/pull/129) 12 | * Disallow installing gosu with `setuid` (https://github.com/tianon/gosu/pull/89) 13 | * Switch from GPL to Apache-2.0 (https://github.com/tianon/gosu/commit/975771e79e281c541fab943a53243604271b4f59) 14 | * Go 1.21 (#53 #54) 15 | * Bump golang.org/x/sys from 0.13.0 to 0.15.0 (#56) 16 | 17 | ## 1.19.0 (2022/07/11) 18 | 19 | * Go 1.18 (#33) 20 | * Bump github.com/opencontainers/runc from 1.0.3 to 1.1.3 (#21 #32) 21 | * ci: matrix to test against more variants (#34) 22 | * Enhance dockerfiles (#20) 23 | 24 | ## 1.18.0 (2021/12/18) 25 | 26 | * Bump github.com/opencontainers/runc from 1.0.1 to 1.0.3 (#18 #19) 27 | 28 | ## 1.17.0 (2021/08/01) 29 | 30 | * Bump github.com/opencontainers/runc from 1.0.0 to 1.0.1 (#17) 31 | 32 | ## 1.16.0 (2021/06/26) 33 | 34 | * Add support for `riscv64` and create `linux/mips64le` image (#16) 35 | * Bump github.com/opencontainers/runc from 1.0.0-rc95 to 1.0.0 (#15) 36 | 37 | ## 1.15.0 (2021/06/18) 38 | 39 | * Go 1.16 (#14) 40 | * Bump github.com/opencontainers/runc from 1.0.0-rc93 to 1.0.0-rc95 (#13) 41 | * Set `cacheonly` output for validators 42 | * Switch to `docker/metadata-action` 43 | 44 | ## 1.14.1 (2021/03/06) 45 | 46 | * Switch to `goreleaser-xx` (#2) 47 | * Add `arm/v5` platform 48 | 49 | ## 1.14.0 (2021/03/04) 50 | 51 | * Rename project `yasu`. Asked by `gosu` maintainer to avoid confusion. See [tianon/gosu#82 (comment)](https://github.com/tianon/gosu/pull/82#issuecomment-790874961) 52 | 53 | ## 1.13.2 (2021/03/03) 54 | 55 | * Bump github.com/opencontainers/runc from 1.0.0-rc92 to 1.0.0-rc93 (#1) 56 | * Fix module name 57 | 58 | ## 1.13.1 (2021/03/02) 59 | 60 | * Missing platform for Docker image 61 | 62 | ## 1.13.0 (2021/03/02) 63 | 64 | * **Fork [tianon/gosu](https://github.com/tianon/gosu/issues/69)** 65 | * Use [buildx bake](https://github.com/docker/buildx) and [goreleaser](https://goreleaser.com/) 66 | * More platforms support 67 | * `arm/v7` 68 | * `mips/hardfloat` 69 | * `mips/softfloat` 70 | * `mipsle/hardfloat` 71 | * `mipsle/softfloat` 72 | * `mips64/hardfloat` 73 | * `mips64/softfloat` 74 | * `mips64le/hardfloat` 75 | * `mips64le/softfloat` 76 | * Add vendor and lint validation bake targets 77 | * Switch to GitHub Actions 78 | * Add dependabot 79 | * Mutualize tests and handle them through bake and GHA 80 | * Publish Docker image (from scratch with only gosu binary) 81 | 82 | ## 1.12 (2018/10/16) 83 | 84 | * built on Go 1.13.10, `runc` 1.0.0-rc10, Alpine 3.11 85 | * added `mips64le` support ([tianon/gosu#69](https://github.com/tianon/gosu/issues/69)) 86 | * dropped `ppc64` support (not to be confused with ppc64le) 87 | 88 | ## 1.11 (2018/10/16) 89 | 90 | * built on Go 1.11.1, `runc` 1.0.0-rc5, Alpine 3.8 91 | * added explicit `--version` and `--help` flags ([tianon/gosu#44](https://github.com/tianon/gosu/issues/44)) 92 | 93 | ## 1.10 (2016/05/11) 94 | 95 | * built on Go 1.7 ([tianon/gosu#25](https://github.com/tianon/gosu/issues/25)) 96 | * official `s390x` release binary ([tianon/gosu#28](https://github.com/tianon/gosu/issues/28)) 97 | * slightly simpler usage output 98 | 99 | ## 1.9 (2016/05/11) 100 | 101 | * fix cross-compilation of official binaries ([tianon/gosu#19](https://github.com/tianon/gosu/issues/19)) 102 | 103 | ## 1.8 (2016/04/19) 104 | 105 | * build against Go 1.6 106 | * add `-s` and `-w` to `-ldflags` so that release binaries are even smaller (~2.6M down to ~1.8M) 107 | * add simple integration test suite 108 | 109 | ## 1.7 (2015/11/08) 110 | 111 | * update to use `github.com/opencontainers/runc/libcontainer` instead of `github.com/docker/libcontainer` 112 | * add `arm64`, `ppc64`, and `ppc64le` to cross-compiled official binaries 113 | 114 | ## 1.6 (2015/10/06) 115 | 116 | * revert `fchown(2)` all open file descriptors; turns out that's NOT OK (see discussion [tianon/gosu#8](https://github.com/tianon/gosu/issues/8) for details) 117 | 118 | ## 1.5 (2015/04/20) 119 | 120 | * build against Go 1.5 121 | * `fchown(2)` all open file descriptors before switching users so that they can be used appropriately by the user we're switching to 122 | 123 | ## 1.4 (2015/04/20) 124 | 125 | * update `libcontainer` dependency to [docker-archive/libcontainer@`b322073`](https://github.com/docker-archive/libcontainer/commit/b322073f27b0e9e60b2ab07eff7f4e96a24cb3f9) 126 | 127 | ## 1.3 (2015/03/24) 128 | 129 | * `golang:1.4` 130 | * always set `HOME` ([tianon/gosu#3](https://github.com/tianon/gosu/issues/3)) 131 | 132 | ## 1.2 (2014/11/19) 133 | 134 | * now built from golang 135 | * cross compiled for multiple arches 136 | * first GPG signed release 137 | 138 | ## 1.1 (2014/07/14) 139 | 140 | * add `LockOSThread` and explicit `GOMAXPROCS` to ensure even more explicitly that we're running in the same thread for the duration 141 | * add better version output (including compilation info) 142 | * build against Go 1.3 (via [tianon/golang](https://registry.hub.docker.com/u/tianon/golang/) and the new `Dockerfile`+`build.sh`) 143 | 144 | ## 1.0 (2014/06/02) 145 | 146 | * add `VERSION` constant (and put it in the usage output) 147 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG GO_VERSION="1.21" 4 | ARG ALPINE_VERSION="3.18" 5 | ARG XX_VERSION="1.3.0" 6 | 7 | ARG TEST_ALPINE_VARIANT="3.16" 8 | ARG TEST_DEBIAN_VARIANT="bullseye" 9 | 10 | FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx 11 | 12 | FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base 13 | COPY --from=xx / / 14 | ENV CGO_ENABLED=0 15 | RUN apk add --no-cache file git 16 | WORKDIR /src 17 | 18 | FROM base AS version 19 | ARG GIT_REF 20 | RUN --mount=target=. </dev/null || cp /artifacts/* /out/ 57 | sha256sum -b yasu_* > ./checksums.txt 58 | sha256sum -c --strict checksums.txt 59 | EOT 60 | 61 | FROM scratch AS release 62 | COPY --link --from=releaser /out / 63 | 64 | FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS build-artifact 65 | RUN apk add --no-cache bash tar 66 | WORKDIR /work 67 | ARG TARGETOS 68 | ARG TARGETARCH 69 | ARG TARGETVARIANT 70 | RUN --mount=type=bind,target=/src \ 71 | --mount=type=bind,from=binary,target=/build \ 72 | --mount=type=bind,from=version,source=/tmp/.version,target=/tmp/.version <= 2.32.1-0.2`, in Debian; https://manpages.debian.org/buster/util-linux/setpriv.1.en.html): 187 | 188 | ```shell 189 | $ docker run -it --rm buildpack-deps:buster-scm setpriv --reuid=nobody --regid=nogroup --init-groups ps faux 190 | USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND 191 | nobody 1 5.0 0.0 9592 1252 pts/0 RNs+ 23:21 0:00 ps faux 192 | ``` 193 | 194 | ### Others 195 | 196 | I'm not terribly familiar with them, but a few other alternatives I'm aware of include: 197 | 198 | * `chpst` (part of `runit`) 199 | 200 | ## Contributing 201 | 202 | Want to contribute? Awesome! The most basic way to show your support is to star 203 | the project, or to raise issues. You can also support this project by [**becoming a sponsor on GitHub**](https://github.com/sponsors/crazy-max) 204 | or by making a [PayPal donation](https://www.paypal.me/crazyws) to ensure this 205 | journey continues indefinitely! 206 | 207 | Thanks again for your support, it is much appreciated! :pray: 208 | 209 | ## License 210 | 211 | Apache-2.0. See `LICENSE` for more details. 212 | -------------------------------------------------------------------------------- /docker-bake.hcl: -------------------------------------------------------------------------------- 1 | variable "GO_VERSION" { 2 | default = null 3 | } 4 | 5 | variable "DESTDIR" { 6 | default = "./bin" 7 | } 8 | 9 | # GITHUB_REF is the actual ref that triggers the workflow and used as version 10 | # when tag is pushed! https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables 11 | variable "GITHUB_REF" { 12 | default = "" 13 | } 14 | 15 | target "_common" { 16 | args = { 17 | GO_VERSION = GO_VERSION 18 | } 19 | } 20 | 21 | # Special target: https://github.com/docker/metadata-action#bake-definition 22 | target "docker-metadata-action" { 23 | tags = ["yasu:local"] 24 | } 25 | 26 | group "default" { 27 | targets = ["image-local"] 28 | } 29 | 30 | target "binary" { 31 | inherits = ["_common"] 32 | target = "binary" 33 | output = ["${DESTDIR}/build"] 34 | } 35 | 36 | target "artifact" { 37 | inherits = ["_common"] 38 | target = "artifact" 39 | output = ["${DESTDIR}/artifact"] 40 | } 41 | 42 | target "artifact-all" { 43 | inherits = ["artifact"] 44 | platforms = [ 45 | "linux/386", 46 | "linux/amd64", 47 | "linux/arm/v5", 48 | "linux/arm/v6", 49 | "linux/arm/v7", 50 | "linux/arm64", 51 | "linux/mips/hardfloat", 52 | "linux/mips/softfloat", 53 | "linux/mipsle/hardfloat", 54 | "linux/mipsle/softfloat", 55 | "linux/mips64/hardfloat", 56 | "linux/mips64/softfloat", 57 | "linux/mips64le/hardfloat", 58 | "linux/mips64le/softfloat", 59 | "linux/ppc64le", 60 | "linux/riscv64", 61 | "linux/s390x" 62 | ] 63 | } 64 | 65 | target "release" { 66 | target = "release" 67 | output = ["${DESTDIR}/release"] 68 | contexts = { 69 | artifacts = "${DESTDIR}/artifact" 70 | } 71 | } 72 | 73 | target "image" { 74 | inherits = ["_common", "docker-metadata-action"] 75 | } 76 | 77 | target "image-local" { 78 | inherits = ["image"] 79 | output = ["type=docker"] 80 | } 81 | 82 | target "image-all" { 83 | inherits = ["image"] 84 | platforms = [ 85 | "linux/386", 86 | "linux/amd64", 87 | "linux/arm/v5", 88 | "linux/arm/v6", 89 | "linux/arm/v7", 90 | "linux/arm64", 91 | "linux/mips64le", 92 | "linux/ppc64le", 93 | "linux/riscv64", 94 | "linux/s390x" 95 | ] 96 | } 97 | 98 | target "vendor" { 99 | inherits = ["_common"] 100 | dockerfile = "./hack/vendor.Dockerfile" 101 | target = "update" 102 | output = ["."] 103 | } 104 | 105 | group "validate" { 106 | targets = ["lint", "vendor-validate"] 107 | } 108 | 109 | target "lint" { 110 | inherits = ["_common"] 111 | dockerfile = "./hack/lint.Dockerfile" 112 | target = "lint" 113 | output = ["type=cacheonly"] 114 | } 115 | 116 | target "vendor-validate" { 117 | inherits = ["_common"] 118 | dockerfile = "./hack/vendor.Dockerfile" 119 | target = "validate" 120 | output = ["type=cacheonly"] 121 | } 122 | 123 | group "test" { 124 | targets = ["test-alpine", "test-debian"] 125 | } 126 | 127 | variable "TEST_ALPINE_VARIANT" { 128 | default = null 129 | } 130 | target "test-alpine" { 131 | inherits = ["_common"] 132 | target = "test-alpine" 133 | output = ["type=cacheonly"] 134 | args = { 135 | TEST_ALPINE_VARIANT = TEST_ALPINE_VARIANT 136 | } 137 | } 138 | 139 | variable "TEST_DEBIAN_VARIANT" { 140 | default = null 141 | } 142 | target "test-debian" { 143 | inherits = ["_common"] 144 | target = "test-debian" 145 | output = ["type=cacheonly"] 146 | args = { 147 | TEST_DEBIAN_VARIANT = TEST_DEBIAN_VARIANT 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/crazy-max/yasu 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/moby/sys/user v0.1.0 7 | golang.org/x/sys v0.21.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= 2 | github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= 3 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 4 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 5 | -------------------------------------------------------------------------------- /hack/lint.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG GO_VERSION="1.21" 4 | ARG ALPINE_VERSION="3.18" 5 | ARG GOLANGCI_LINT_VERSION="v1.55" 6 | 7 | FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base 8 | ENV GOFLAGS="-buildvcs=false" 9 | RUN apk add --no-cache gcc linux-headers musl-dev 10 | WORKDIR /src 11 | 12 | FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint 13 | FROM base AS lint 14 | RUN --mount=type=bind,target=. \ 15 | --mount=type=cache,target=/root/.cache \ 16 | --mount=from=golangci-lint,source=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \ 17 | golangci-lint run ./... 18 | -------------------------------------------------------------------------------- /hack/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | 4 | yasut() { 5 | spec="$1"; shift 6 | expec="$1"; shift 7 | 8 | real="$(yasu "$spec" id -u):$(yasu "$spec" id -g):$(yasu "$spec" id -G)" 9 | [ "$expec" = "$real" ] 10 | 11 | expec="$1"; shift 12 | 13 | # have to "|| true" this one because of "id: unknown ID 1000" (rightfully) having a nonzero exit code 14 | real="$(yasu "$spec" id -un):$(yasu "$spec" id -gn):$(yasu "$spec" id -Gn)" || true 15 | [ "$expec" = "$real" ] 16 | } 17 | 18 | id 19 | 20 | yasut 0 "0:0:$(id -G root)" "root:root:$(id -Gn root)" 21 | yasut 0:0 '0:0:0' 'root:root:root' 22 | yasut root "0:0:$(id -G root)" "root:root:$(id -Gn root)" 23 | yasut 0:root '0:0:0' 'root:root:root' 24 | yasut root:0 '0:0:0' 'root:root:root' 25 | yasut root:root '0:0:0' 'root:root:root' 26 | yasut 1000 "1000:$(id -g):$(id -g)" "1000:$(id -gn):$(id -gn)" 27 | yasut 0:1000 '0:1000:1000' 'root:1000:1000' 28 | yasut 1000:1000 '1000:1000:1000' '1000:1000:1000' 29 | yasut root:1000 '0:1000:1000' 'root:1000:1000' 30 | yasut 1000:root '1000:0:0' '1000:root:root' 31 | yasut 1000:daemon "1000:$(id -g daemon):$(id -g daemon)" '1000:daemon:daemon' 32 | yasut games "$(id -u games):$(id -g games):$(id -G games)" 'games:games:games users' 33 | yasut games:daemon "$(id -u games):$(id -g daemon):$(id -g daemon)" 'games:daemon:daemon' 34 | 35 | yasut 0: "0:0:$(id -G root)" "root:root:$(id -Gn root)" 36 | yasut '' "$(id -u):$(id -g):$(id -G)" "$(id -un):$(id -gn):$(id -Gn)" 37 | yasut ':0' "$(id -u):0:0" "$(id -un):root:root" 38 | 39 | [ "$(yasu 0 env | grep '^HOME=')" = 'HOME=/root' ] 40 | [ "$(yasu 0:0 env | grep '^HOME=')" = 'HOME=/root' ] 41 | [ "$(yasu root env | grep '^HOME=')" = 'HOME=/root' ] 42 | [ "$(yasu 0:root env | grep '^HOME=')" = 'HOME=/root' ] 43 | [ "$(yasu root:0 env | grep '^HOME=')" = 'HOME=/root' ] 44 | [ "$(yasu root:root env | grep '^HOME=')" = 'HOME=/root' ] 45 | [ "$(yasu 0:1000 env | grep '^HOME=')" = 'HOME=/root' ] 46 | [ "$(yasu root:1000 env | grep '^HOME=')" = 'HOME=/root' ] 47 | [ "$(yasu 1000 env | grep '^HOME=')" = 'HOME=/' ] 48 | [ "$(yasu 1000:0 env | grep '^HOME=')" = 'HOME=/' ] 49 | [ "$(yasu 1000:root env | grep '^HOME=')" = 'HOME=/' ] 50 | [ "$(yasu games env | grep '^HOME=')" = 'HOME=/usr/games' ] 51 | [ "$(yasu games:daemon env | grep '^HOME=')" = 'HOME=/usr/games' ] 52 | 53 | # make sure we error out properly in unexpected cases like an invalid username 54 | ! yasu bogus true 55 | ! yasu 0day true 56 | ! yasu 0:bogus true 57 | ! yasu 0:0day true 58 | -------------------------------------------------------------------------------- /hack/vendor.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG GO_VERSION="1.21" 4 | ARG ALPINE_VERSION="3.18" 5 | 6 | FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base 7 | RUN apk add --no-cache git linux-headers musl-dev 8 | WORKDIR /src 9 | 10 | FROM base AS vendored 11 | RUN --mount=type=bind,target=.,rw \ 12 | --mount=type=cache,target=/go/pkg/mod <&2 'ERROR: Vendor result differs. Please vendor your package with "docker buildx bake vendor"' 31 | echo "$diff" 32 | exit 1 33 | fi 34 | EOT 35 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | "syscall" 13 | "text/template" 14 | ) 15 | 16 | func init() { 17 | // make sure we only have one process and that it runs on the main thread (so that ideally, when we Exec, we keep our user switches and stuff) 18 | runtime.GOMAXPROCS(1) 19 | runtime.LockOSThread() 20 | } 21 | 22 | func version() string { 23 | return fmt.Sprintf(`%s (%s on %s/%s; %s)`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.Compiler) 24 | } 25 | 26 | func usage() string { 27 | t := template.Must(template.New("usage").Parse(` 28 | Usage: {{ .Self }} user-spec command [args] 29 | eg: {{ .Self }} tianon bash 30 | {{ .Self }} nobody:root bash -c 'whoami && id' 31 | {{ .Self }} 1000:1 id 32 | 33 | {{ .Self }} version: {{ .Version }} 34 | {{ .Self }} license: Apache-2.0 (full text at https://github.com/crazy-max/yasu) 35 | `)) 36 | var b bytes.Buffer 37 | template.Must(t, t.Execute(&b, struct { 38 | Self string 39 | Version string 40 | }{ 41 | Self: filepath.Base(os.Args[0]), 42 | Version: version(), 43 | })) 44 | return strings.TrimSpace(b.String()) + "\n" 45 | } 46 | 47 | func main() { 48 | log.SetFlags(0) // no timestamps on our logs 49 | 50 | if ok := os.Getenv("GOSU_PLEASE_LET_ME_BE_COMPLETELY_INSECURE_I_GET_TO_KEEP_ALL_THE_PIECES"); ok != "I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain. Time to die." { 51 | if fi, err := os.Stat("/proc/self/exe"); err != nil { 52 | log.Fatalf("error: %v", err) 53 | } else if fi.Mode()&os.ModeSetuid != 0 { 54 | // ... oh no 55 | log.Fatalf("error: %q appears to be installed with the 'setuid' bit set, which is an *extremely* insecure and completely unsupported configuration! (what you want instead is likely 'sudo' or 'su')", os.Args[0]) 56 | } else if fi.Mode()&os.ModeSetgid != 0 { 57 | // ... oh no 58 | log.Fatalf("error: %q appears to be installed with the 'setgid' bit set, which is not quite *as* insecure as 'setuid', but still not great, and definitely a completely unsupported configuration! (what you want instead is likely 'sudo' or 'su')", os.Args[0]) 59 | } 60 | } 61 | 62 | if len(os.Args) >= 2 { 63 | switch os.Args[1] { 64 | case "--help", "-h", "-?": 65 | fmt.Println(usage()) 66 | os.Exit(0) 67 | case "--version", "-v": 68 | fmt.Println(version()) 69 | os.Exit(0) 70 | } 71 | } 72 | if len(os.Args) <= 2 { 73 | log.Println(usage()) 74 | os.Exit(1) 75 | } 76 | 77 | // clear HOME so that SetupUser will set it 78 | os.Unsetenv("HOME") 79 | 80 | if err := SetupUser(os.Args[1]); err != nil { 81 | log.Fatalf("error: failed switching to %q: %v", os.Args[1], err) 82 | } 83 | 84 | name, err := exec.LookPath(os.Args[2]) 85 | if err != nil { 86 | log.Fatalf("error: %v", err) 87 | } 88 | 89 | if err = syscall.Exec(name, os.Args[2:], os.Environ()); err != nil { 90 | log.Fatalf("error: exec failed: %v", err) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /setup-user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/moby/sys/user" 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | // this function comes from libcontainer/init_linux.go 11 | // we don't use that directly because we don't want the whole namespaces package imported here 12 | // (also, because we need minor modifications and it's not even exported) 13 | 14 | // SetupUser changes the groups, gid, and uid for the user inside the container 15 | func SetupUser(u string) error { 16 | // Set up defaults. 17 | defaultExecUser := user.ExecUser{ 18 | Uid: unix.Getuid(), 19 | Gid: unix.Getgid(), 20 | Home: "/", 21 | } 22 | passwdPath, err := user.GetPasswdPath() 23 | if err != nil { 24 | return err 25 | } 26 | groupPath, err := user.GetGroupPath() 27 | if err != nil { 28 | return err 29 | } 30 | execUser, err := user.GetExecUserPath(u, &defaultExecUser, passwdPath, groupPath) 31 | if err != nil { 32 | return err 33 | } 34 | if err := unix.Setgroups(execUser.Sgids); err != nil { 35 | return err 36 | } 37 | if err := unix.Setgid(execUser.Gid); err != nil { 38 | return err 39 | } 40 | if err := unix.Setuid(execUser.Uid); err != nil { 41 | return err 42 | } 43 | // if we didn't get HOME already, set it based on the user's HOME 44 | if envHome := os.Getenv("HOME"); envHome == "" { 45 | if err := os.Setenv("HOME", execUser.Home); err != nil { 46 | return err 47 | } 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Version is the app version 4 | var Version = "dev" 5 | --------------------------------------------------------------------------------