├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci-docker.yml │ ├── conventional-commits.yml │ ├── go-test.yml │ ├── golangci-lint.yml │ └── publish.yml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── cardano-up │ ├── context.go │ ├── down.go │ ├── info.go │ ├── install.go │ ├── list.go │ ├── list_test.go │ ├── logs.go │ ├── main.go │ ├── uninstall.go │ ├── up.go │ ├── update.go │ ├── upgrade.go │ ├── validate.go │ └── version.go ├── go.mod ├── go.sum ├── internal ├── consolelog │ └── consolelog.go └── version │ └── version.go ├── packages └── README.md └── pkgmgr ├── config.go ├── config_test.go ├── context.go ├── docker.go ├── error.go ├── installed_package.go ├── package.go ├── package_test.go ├── pkgmgr.go ├── registry.go ├── registry_test.go ├── resolver.go ├── resolver_test.go ├── state.go └── template.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Blink Labs 2 | # 3 | * @blinklabs-io/core 4 | *.md @blinklabs-io/core @blinklabs-io/docs @blinklabs-io/pms 5 | LICENSE @blinklabs-io/core @blinklabs-io/pms 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "docker" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | - package-ecosystem: "gomod" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | -------------------------------------------------------------------------------- /.github/workflows/ci-docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker CI 2 | 3 | on: 4 | pull_request: 5 | branches: ['main'] 6 | paths: ['Dockerfile','cmd/**','docs/**','internal/**','go.*','.github/workflows/ci-docker.yml'] 7 | 8 | env: 9 | GHCR_IMAGE_NAME: ghcr.io/blinklabs-io/cardano-up 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | docker: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: '0' 21 | - name: qemu 22 | uses: docker/setup-qemu-action@v3 23 | - uses: docker/setup-buildx-action@v3 24 | - id: meta 25 | uses: docker/metadata-action@v5 26 | with: 27 | images: ${{ env.GHCR_IMAGE_NAME }} 28 | - name: build 29 | uses: docker/build-push-action@v6 30 | with: 31 | context: . 32 | push: false 33 | ### TODO: test multiple platforms 34 | # platforms: linux/amd64,linux/arm64 35 | tags: ${{ steps.meta.outputs.tags }} 36 | labels: ${{ steps.meta.outputs.labels }} 37 | -------------------------------------------------------------------------------- /.github/workflows/conventional-commits.yml: -------------------------------------------------------------------------------- 1 | # The below is pulled from upstream and slightly modified 2 | # https://github.com/webiny/action-conventional-commits/blob/master/README.md#usage 3 | 4 | name: Conventional Commits 5 | 6 | on: 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | name: Conventional Commits 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: webiny/action-conventional-commits@v1.3.0 18 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: go-test 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | go-test: 16 | name: go-test 17 | strategy: 18 | matrix: 19 | go-version: [1.23.x, 1.24.x] 20 | platform: [ubuntu-latest] 21 | runs-on: ${{ matrix.platform }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version: ${{ matrix.go-version }} 27 | - name: go-test 28 | run: go test ./... 29 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | golangci: 15 | name: lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version: 1.23.x 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@v8 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | tags: 7 | - 'v*.*.*' 8 | 9 | concurrency: ${{ github.ref }} 10 | 11 | jobs: 12 | create-draft-release: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | outputs: 17 | RELEASE_ID: ${{ steps.create-release.outputs.result }} 18 | steps: 19 | - run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV" 20 | - uses: actions/github-script@v7 21 | id: create-release 22 | if: startsWith(github.ref, 'refs/tags/') 23 | with: 24 | github-token: ${{ secrets.GITHUB_TOKEN }} 25 | result-encoding: string 26 | script: | 27 | try { 28 | const response = await github.rest.repos.createRelease({ 29 | draft: true, 30 | generate_release_notes: true, 31 | name: process.env.RELEASE_TAG, 32 | owner: context.repo.owner, 33 | prerelease: false, 34 | repo: context.repo.repo, 35 | tag_name: process.env.RELEASE_TAG, 36 | }); 37 | 38 | return response.data.id; 39 | } catch (error) { 40 | core.setFailed(error.message); 41 | } 42 | 43 | build-binaries: 44 | strategy: 45 | matrix: 46 | os: [linux, darwin] #, freebsd, windows] 47 | arch: [amd64, arm64] 48 | runs-on: ubuntu-latest 49 | needs: [create-draft-release] 50 | permissions: 51 | actions: write 52 | attestations: write 53 | checks: write 54 | contents: write 55 | id-token: write 56 | packages: write 57 | statuses: write 58 | steps: 59 | - run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV" 60 | - uses: actions/checkout@v4 61 | with: 62 | fetch-depth: '0' 63 | - uses: actions/setup-go@v5 64 | with: 65 | go-version: 1.23.x 66 | - name: Build binary 67 | run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} make build 68 | - name: Upload release asset 69 | if: startsWith(github.ref, 'refs/tags/') 70 | run: | 71 | _filename=cardano-up-${{ env.RELEASE_TAG }}-${{ matrix.os }}-${{ matrix.arch }} 72 | if [[ ${{ matrix.os }} == windows ]]; then 73 | _filename=${_filename}.exe 74 | fi 75 | cp cardano-up ${_filename} 76 | curl \ 77 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 78 | -H "Content-Type: application/octet-stream" \ 79 | --data-binary @${_filename} \ 80 | https://uploads.github.com/repos/${{ github.repository_owner }}/cardano-up/releases/${{ needs.create-draft-release.outputs.RELEASE_ID }}/assets?name=${_filename} 81 | - name: Attest binary 82 | uses: actions/attest-build-provenance@v2 83 | with: 84 | subject-path: 'cardano-up' 85 | 86 | build-images: 87 | runs-on: ubuntu-latest 88 | needs: [create-draft-release] 89 | permissions: 90 | actions: write 91 | attestations: write 92 | checks: write 93 | contents: write 94 | id-token: write 95 | packages: write 96 | statuses: write 97 | steps: 98 | - run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV" 99 | - uses: actions/checkout@v4 100 | with: 101 | fetch-depth: '0' 102 | - name: Set up QEMU 103 | uses: docker/setup-qemu-action@v3 104 | - name: Set up Docker Buildx 105 | uses: docker/setup-buildx-action@v3 106 | - name: Login to Docker Hub 107 | uses: docker/login-action@v3 108 | with: 109 | username: blinklabs 110 | password: ${{ secrets.DOCKER_PASSWORD }} # uses token 111 | - name: Login to GHCR 112 | uses: docker/login-action@v3 113 | with: 114 | username: ${{ github.repository_owner }} 115 | password: ${{ secrets.GITHUB_TOKEN }} 116 | registry: ghcr.io 117 | - id: meta 118 | uses: docker/metadata-action@v5 119 | with: 120 | images: | 121 | blinklabs/cardano-up 122 | ghcr.io/${{ github.repository }} 123 | tags: | 124 | # Only version, no revision 125 | type=match,pattern=v(.*)-(.*),group=1 126 | # branch 127 | type=ref,event=branch 128 | # semver 129 | type=semver,pattern={{version}} 130 | - name: Build images 131 | uses: docker/build-push-action@v6 132 | id: push 133 | with: 134 | outputs: "type=registry,push=true" 135 | platforms: linux/amd64,linux/arm64 136 | tags: ${{ steps.meta.outputs.tags }} 137 | labels: ${{ steps.meta.outputs.labels }} 138 | - name: Attest Docker Hub image 139 | uses: actions/attest-build-provenance@v2 140 | with: 141 | subject-name: index.docker.io/blinklabs/cardano-up 142 | subject-digest: ${{ steps.push.outputs.digest }} 143 | push-to-registry: true 144 | - name: Attest GHCR image 145 | uses: actions/attest-build-provenance@v2 146 | with: 147 | subject-name: ghcr.io/${{ github.repository }} 148 | subject-digest: ${{ steps.push.outputs.digest }} 149 | push-to-registry: true 150 | # Update Docker Hub from README 151 | - name: Docker Hub Description 152 | uses: peter-evans/dockerhub-description@v4 153 | with: 154 | username: blinklabs 155 | password: ${{ secrets.DOCKER_PASSWORD }} 156 | repository: blinklabs/cardano-up 157 | readme-filepath: ./README.md 158 | short-description: "Command line utility for managing Cardano services for local development" 159 | 160 | finalize-release: 161 | runs-on: ubuntu-latest 162 | permissions: 163 | contents: write 164 | needs: [create-draft-release, build-binaries, build-images] 165 | steps: 166 | - uses: actions/github-script@v7 167 | if: startsWith(github.ref, 'refs/tags/') 168 | with: 169 | github-token: ${{ secrets.GITHUB_TOKEN }} 170 | script: | 171 | try { 172 | await github.rest.repos.updateRelease({ 173 | owner: context.repo.owner, 174 | repo: context.repo.repo, 175 | release_id: ${{ needs.create-draft-release.outputs.RELEASE_ID }}, 176 | draft: false, 177 | }); 178 | } catch (error) { 179 | core.setFailed(error.message); 180 | } 181 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | # Program binary 24 | /cardano-up 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | issues-exit-code: 1 4 | tests: false 5 | linters: 6 | enable: 7 | - asasalint 8 | - asciicheck 9 | - bidichk 10 | - bodyclose 11 | - contextcheck 12 | - copyloopvar 13 | - durationcheck 14 | - errchkjson 15 | - errorlint 16 | - exhaustive 17 | - fatcontext 18 | - gocheckcompilerdirectives 19 | - gochecksumtype 20 | - gomodguard 21 | - gosec 22 | - gosmopolitan 23 | - loggercheck 24 | - makezero 25 | - musttag 26 | - nilerr 27 | - nilnesserr 28 | - noctx 29 | - perfsprint 30 | - prealloc 31 | - protogetter 32 | - reassign 33 | - recvcheck 34 | - rowserrcheck 35 | - spancheck 36 | - sqlclosecheck 37 | - testifylint 38 | - unparam 39 | - usestdlibvars 40 | - whitespace 41 | - zerologlint 42 | disable: 43 | - depguard 44 | exclusions: 45 | generated: lax 46 | presets: 47 | - comments 48 | - common-false-positives 49 | - legacy 50 | - std-error-handling 51 | paths: 52 | - docs 53 | - third_party$ 54 | - builtin$ 55 | - examples$ 56 | issues: 57 | max-issues-per-linter: 0 58 | max-same-issues: 0 59 | formatters: 60 | enable: 61 | - gci 62 | - gofmt 63 | - gofumpt 64 | - goimports 65 | exclusions: 66 | generated: lax 67 | paths: 68 | - docs 69 | - third_party$ 70 | - builtin$ 71 | - examples$ 72 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/blinklabs-io/go:1.24.2-1 AS build 2 | 3 | WORKDIR /code 4 | COPY . . 5 | RUN make build 6 | 7 | FROM cgr.dev/chainguard/glibc-dynamic AS cardano-up 8 | COPY --from=build /code/cardano-up /bin/ 9 | ENTRYPOINT ["cardano-up"] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Blink Labs Software 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Determine root directory 2 | ROOT_DIR=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 3 | 4 | # Gather all .go files for use in dependencies below 5 | GO_FILES=$(shell find $(ROOT_DIR) -name '*.go') 6 | 7 | # Gather list of expected binaries 8 | BINARIES=$(shell cd $(ROOT_DIR)/cmd && ls -1 | grep -v ^common) 9 | 10 | # Extract Go module name from go.mod 11 | GOMODULE=$(shell grep ^module $(ROOT_DIR)/go.mod | awk '{ print $$2 }') 12 | 13 | # Set version strings based on git tag and current ref 14 | GO_LDFLAGS=-ldflags "-s -w -X '$(GOMODULE)/internal/version.Version=$(shell git describe --tags --exact-match 2>/dev/null)' -X '$(GOMODULE)/internal/version.CommitHash=$(shell git rev-parse --short HEAD)'" 15 | 16 | .PHONY: build mod-tidy clean format golines test 17 | 18 | # Alias for building program binary 19 | build: $(BINARIES) 20 | 21 | # Builds and installs binary in ~/.local/bin 22 | install: build 23 | mv $(BINARIES) $(HOME)/.local/bin 24 | 25 | uninstall: 26 | rm -f $(HOME)/.local/bin/$(BINARIES) 27 | 28 | mod-tidy: 29 | # Needed to fetch new dependencies and add them to go.mod 30 | go mod tidy 31 | 32 | clean: 33 | rm -f $(BINARIES) 34 | 35 | format: mod-tidy 36 | go fmt ./... 37 | gofmt -s -w $(GO_FILES) 38 | 39 | golines: 40 | golines -w --ignore-generated --chain-split-dots --max-len=80 --reformat-tags . 41 | 42 | test: mod-tidy 43 | go test -v -race ./... 44 | 45 | # Build our program binaries 46 | # Depends on GO_FILES to determine when rebuild is needed 47 | $(BINARIES): mod-tidy $(GO_FILES) 48 | CGO_ENABLED=0 \ 49 | go build \ 50 | $(GO_LDFLAGS) \ 51 | -o $(@) \ 52 | ./cmd/$(@) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cardano-up 2 | 3 | ## Installation 4 | 5 | ### Install latest release (recommended) 6 | 7 | You can download the latest releases from the [Releases page](https://github.com/blinklabs-io/cardano-up/releases). 8 | Place the downloaded binary in `/usr/local/bin`, `~/.local/bin`, or some other convenient location and make sure 9 | that location has been added to your `$PATH`. Our recommendation is to use `~/.local/bin` as that is where this 10 | tool will install wrapper scripts. 11 | 12 | NOTE: On MacOS, you will need to allow `/` to be used by Docker Desktop 13 | 14 | ## Basic usage 15 | 16 | ### List available packages 17 | 18 | ``` 19 | cardano-up list-available 20 | ``` 21 | 22 | ### Install a package and interact with it 23 | 24 | Add `~/.local/bin` to your `$PATH` by adding the following to your shell RC/profile to make any 25 | commands/scripts installed readily available 26 | 27 | ``` 28 | export PATH=~/.local/bin:$PATH 29 | ``` 30 | 31 | Install cardano-node 32 | 33 | ``` 34 | cardano-up install cardano-node 35 | ``` 36 | 37 | You can also add any env vars exported by the installed packages to your env by adding the following to your shell RC/profile: 38 | 39 | ``` 40 | eval $(cardano-up context env) 41 | ``` 42 | 43 | You should now be able to run `cardano-cli` normally. 44 | 45 | ``` 46 | cardano-cli query tip --testnet-magic 2 47 | ``` 48 | 49 | ### Uninstall a package 50 | 51 | ``` 52 | cardano-up uninstall cardano-node 53 | ``` 54 | 55 | ### Enabling shell auto-complete 56 | 57 | Enable for current session: 58 | 59 | ``` 60 | source <(cardano-up completion bash) 61 | ``` 62 | 63 | Enable for all future sessions: 64 | 65 | ``` 66 | cardano-up completion bash > /etc/bash_completion.d/cardano-up 67 | ``` 68 | 69 | ## Contexts 70 | 71 | Contexts are used to allow you to install multiple copies of the same package with different network configurations side by side. They allow you to do things 72 | such as running a `preprod` and `mainnet` Cardano node on the same machine, or even have multiple `preview` Cardano node instances running different versions 73 | of the node. 74 | 75 | Commands such as `install`, `uninstall`, and `list` work in the active context. You can use the `context` command to change the active context or manage available contexts. 76 | 77 | ## Command reference 78 | 79 | The `cardano-up` command consists of multiple subcommands. You can list all subcommands by running `cardano-up` with no arguments or with the `--help` option. 80 | 81 | ``` 82 | $ cardano-up 83 | Usage: 84 | cardano-up [command] 85 | 86 | Available Commands: 87 | completion Generate the autocompletion script for the specified shell 88 | context Manage the current context 89 | down Stops all Docker containers 90 | help Help about any command 91 | info Show info for an installed package 92 | install Install package 93 | list List installed packages 94 | list-available List available packages 95 | logs Show logs for an installed package 96 | uninstall Uninstall package 97 | up Starts all Docker containers 98 | update Update the package registry cache 99 | upgrade Upgrade package 100 | validate Validate package file(s) in the given directory 101 | version Displays the version 102 | 103 | Flags: 104 | -D, --debug enable debug logging 105 | -h, --help help for cardano-up 106 | -v, --verbose Show all available versions of packages 107 | 108 | Use "cardano-up [command] --help" for more information about a command. 109 | ``` 110 | 111 | ### `completion` 112 | 113 | The `completion` subcommand generates shell auto-completion configuration for various supported shells. Run `completion help ` for more information on installing completion support for your shell. 114 | 115 | ### `context` 116 | 117 | The `context` subcommand manages contexts. It has subcommands of its own for the various context-related functions. 118 | 119 | #### `context create` 120 | 121 | Create a new context with a given name, optionally specifying a description and a Cardano network 122 | 123 | #### `context delete` 124 | 125 | Delete the context with the given name, if it exists 126 | 127 | #### `context env` 128 | 129 | Output environment variables for the active context 130 | 131 | #### `context list` 132 | 133 | Lists the available contexts 134 | 135 | #### `context select` 136 | 137 | Sets the active context to the given context name 138 | 139 | ### `down` 140 | 141 | Stops all running services for packages in the active context 142 | 143 | ### `help` 144 | 145 | Displays usage information for commands and subcommands 146 | 147 | ### `info` 148 | 149 | Shows information for an installed package, including the name, version, context name, any post-install notes, etc. 150 | 151 | ### `install` 152 | 153 | Installs the specified package, optionally setting the network for the active context 154 | 155 | ### `list` 156 | 157 | Lists installed packages in the active context, or all contexts with `-A` 158 | 159 | ### `list-available` 160 | 161 | By default `list-available` lists only the latest version of each package available for install. 162 | Use the `--verbose` or `(-v)` flag to display all available versions of each package. 163 | 164 | ### `logs` 165 | 166 | Displays logs from a running service for the specified package in the active context 167 | 168 | ### `uninstall` 169 | 170 | Uninstalls the specified package in the active context 171 | 172 | ### `up` 173 | 174 | Starts all services for packages in the active context 175 | 176 | ### `update` 177 | 178 | Force a refresh of the package registry cache 179 | 180 | ### `upgrade` 181 | 182 | Upgrade the specified package 183 | 184 | ### `validate` 185 | 186 | Validates packages defined in specified path 187 | 188 | ### `version` 189 | 190 | Displays the version 191 | 192 | ## Development 193 | 194 | ### Install from source 195 | 196 | Before starting, make sure that you have at least Go 1.21 installed locally. Run the following 197 | to download the latest source code and build. 198 | 199 | ``` 200 | go install github.com/blinklabs-io/cardano-up/cmd/cardano-up@main 201 | ``` 202 | 203 | Once that completes, you should have a `cardano-up` binary in `~/go/bin`. 204 | 205 | ``` 206 | $ ls -lh ~/go/bin/cardano-up 207 | -rwxrwxr-x 1 agaffney agaffney 16M Mar 16 08:13 /home/agaffney/go/bin/cardano-up 208 | ``` 209 | 210 | You may need to add a line like the following to your shell RC/profile to update your PATH 211 | to be able to find the binary. 212 | 213 | ``` 214 | export PATH=~/go/bin:$PATH 215 | ``` 216 | 217 | ### Compile from source 218 | 219 | There is a Makefile (you will need `make` installed) which you can invoke. 220 | 221 | ```bash 222 | make 223 | ``` 224 | 225 | This will create a `cardano-up` binary in the repository root. 226 | 227 | ### Creating and maintaining packages 228 | 229 | Packages and their versions are defined under `packages/` in this repo. Each separate package name has its own subdirectory, 230 | and each version of a particular package is defined in a separate file under that subdirectory. For example, package `foo` with 231 | version `1.2.3` would live in `packages/foo/foo-1.2.3.yaml`. 232 | 233 | #### Testing local changes to packages 234 | 235 | The remote package repo will be used by default when running `cardano-up`. To instead use the package files in a local directory, you 236 | can run it like: 237 | 238 | ```bash 239 | REGISTRY_DIR=packages/ cardano-up ... 240 | ``` 241 | 242 | #### Validating package files 243 | 244 | There is a built-in subcommand for validating package files. It will be run automatically for a PR, but you can also run it manually. 245 | 246 | ```bash 247 | cardano-up validate packages/ 248 | ``` 249 | 250 | #### Templating 251 | 252 | Package manifest files are evaluated as a Go template before being parsed as YAML. The following values are available for use in templates. 253 | 254 | | Name | Description | 255 | | --- | --- | 256 | | `.Package` | | 257 | | `.Package.Name` | Full package name including the version | 258 | | `.Package.ShortName` | Package name | 259 | | `.Package.Version` | Package version | 260 | | `.Package.Options` | Provided package options | 261 | | `.Paths` | | 262 | | `.Paths.BinDir` | Binary dir for package | 263 | | `.Paths.CacheDir` | Cache dir for package | 264 | | `.Paths.ContextDir` | Context dir for package | 265 | | `.Paths.DataDir` | Data dir for package | 266 | | `.Ports` | Container port mappings | 267 | 268 | #### Package manifest format 269 | 270 | The package manifest format is a YAML file with the following fields: 271 | 272 | | Field | Required | Description | 273 | | --- | :---: | --- | 274 | | `name` | x | Package name. This must match the prefix of the package manifest filename and the parent directory name | 275 | | `version` | x | Package version | 276 | | `description` | | Package description | 277 | | `preInstallScript` | | Arbitrary command that will be run before the package is installed | 278 | | `postInstallScript` | | Arbitrary command that will be run after the package is installed | 279 | | `preUninstallScript` | | Arbitrary command that will be run before the package is uninstalled | 280 | | `postUninstallScript` | | Arbitrary command that will be run after the package is uninstalled | 281 | | `installSteps` | | Steps to install package | 282 | | `dependencies` | | Dependencies for the package | 283 | | `tags` | | Tags for the package | 284 | | `options` | | Install-time options | 285 | | `outputs` | | Package outputs | 286 | 287 | ##### `installSteps` 288 | 289 | The install steps for a package consist of a list of resources to manage. They are applied in order on install and reverse order on uninstall. 290 | 291 | Each install step may contain a condition that will make it's evaluation optional. A condition will be implicitly wrapped in `{{ if ` and ` }}True{{ else }}False{{ end }}` and evaluated by the templating engine. 292 | 293 | ###### `docker` 294 | 295 | The `docker` install step type manages a Docker container. 296 | 297 | Example: 298 | 299 | ```yaml 300 | installSteps: 301 | - docker: 302 | containerName: nginx 303 | image: nginx 304 | ``` 305 | 306 | | Field | Required | Description | 307 | | --- | :---: | --- | 308 | | `containerName` | x | Name of the container to create. This will be automatically prefixed by the package name | 309 | | `image` | x | Docker image to use for container | 310 | | `env` | | Environment variables for container (expects a map) | 311 | | `command` | | Override container command (expects a list) | 312 | | `args` | | Override container args (expects a list) | 313 | | `binds` | | Volume binds in the Docker `-v` flag format (expects a list) | 314 | | `ports` | | Ports to map in the Docker `-p` flag format (expects a list). NOTE: assigning a static port mapping may cause conflicts | 315 | | `pullOnly` | | Only pull the image to pre-fetch it (expects a bool, defaults to creating container) | 316 | 317 | ###### `file` 318 | 319 | The `file` install step type manages a file. 320 | 321 | Example: 322 | 323 | ```yaml 324 | installSteps: 325 | - file: 326 | filename: my-file 327 | source: my-source-file 328 | ``` 329 | 330 | | Field | Required | Description | 331 | | --- | :---: | --- | 332 | | `filename` | x | Name of destination file. This will be created within the package's data directory | 333 | | `source` | | Path to source file. This should be a relative path within the package manifest directory. This takes precedence over `content` if both are provided | 334 | | `content` | | Inline content for destination file | 335 | | `mode` | | Octal file mode for destination file | 336 | | `binary` | | Whether this file is an executable file for the package (expects bool, defaults to `false`) | 337 | 338 | ##### `dependencies` 339 | 340 | Dependencies for a package are specified in the following format. At minimum they contain a package name. They may optionally contain a list of required package 341 | options and version range(s). 342 | 343 | Examples: 344 | 345 | Package `foo` with at least version `1.0.2` 346 | 347 | ``` 348 | foo >= 1.0.2 349 | ``` 350 | 351 | Package `foo` with at least version `1.0.2` but less than `2.0.0` 352 | 353 | ``` 354 | foo < 2.0.0, >= 1.0.2 355 | ``` 356 | 357 | Package `bar` with at least version `3.0.0`, option `optA` turned on, and option `optB` turned off 358 | 359 | ``` 360 | bar[optA,-optB] >= 3.0.0 361 | ``` 362 | 363 | ##### `tags` 364 | 365 | The tags for a package should be a list of arbitrary string values corresponding to the supported platforms and architectures. They should be one or more of: 366 | 367 | * `docker` 368 | * `linux` 369 | * `darwin` 370 | * `amd64` 371 | * `arm` 372 | 373 | ##### `options` 374 | 375 | The options for a package allow defining optional feature flags. The value of these flags is available to templates in the package manifest. 376 | 377 | Example: 378 | 379 | ```yaml 380 | options: 381 | - name: foo 382 | description: Option foo 383 | default: false 384 | ``` 385 | 386 | This option could then be referenced as `.Package.Options.foo` in package templates. 387 | 388 | | Field | Required | Description | 389 | | --- | :---: | --- | 390 | | `name` | x | Name of the option | 391 | | `description` | | Description of the option | 392 | | `default` | | Default value for option (defaults to `false`) | 393 | 394 | ##### `outputs` 395 | 396 | The outputs defined in a package will be translated into environment variables for the user to consume. 397 | 398 | Example: 399 | 400 | ```yaml 401 | - name: socket_path 402 | description: Path to the Cardano Node UNIX socket 403 | value: '{{ .Paths.ContextDir }}/node-ipc/node.socket' 404 | ``` 405 | 406 | When used in package `cardano-node`, this will generate an env var named `CARDANO_NODE_SOCKET_PATH` with a path inside the package's data directory. 407 | 408 | | Field | Required | Description | 409 | | --- | :---: | --- | 410 | | `name` | x | Name of the output. This will have the package name automatically prepended and be made upper case | 411 | | `description` | | Description of the output | 412 | | `value` | x | Template that will be evaluated to generate the static output value | 413 | -------------------------------------------------------------------------------- /cmd/cardano-up/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "log/slog" 21 | "os" 22 | "sort" 23 | 24 | "github.com/blinklabs-io/cardano-up/pkgmgr" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | var contextFlags = struct { 29 | description string 30 | network string 31 | force bool 32 | }{} 33 | 34 | func contextCommand() *cobra.Command { 35 | contextCommand := &cobra.Command{ 36 | Use: "context", 37 | Short: "Manage the current context", 38 | } 39 | contextCommand.AddCommand( 40 | contextListCommand(), 41 | contextSelectCommand(), 42 | contextCreateCommand(), 43 | contextDeleteCommand(), 44 | contextEnvCommand(), 45 | ) 46 | 47 | return contextCommand 48 | } 49 | 50 | func contextListCommand() *cobra.Command { 51 | return &cobra.Command{ 52 | Use: "list", 53 | Short: "List available contexts", 54 | Run: func(cmd *cobra.Command, args []string) { 55 | pm := createPackageManager() 56 | activeContext, _ := pm.ActiveContext() 57 | contexts := pm.Contexts() 58 | slog.Info("Contexts (* is active):\n") 59 | slog.Info( 60 | fmt.Sprintf( 61 | " %-15s %-15s %s", 62 | "Name", 63 | "Network", 64 | "Description", 65 | ), 66 | ) 67 | var tmpContextNames []string 68 | for contextName := range contexts { 69 | tmpContextNames = append(tmpContextNames, contextName) 70 | } 71 | sort.Strings(tmpContextNames) 72 | // for contextName, context := range contexts { 73 | for _, contextName := range tmpContextNames { 74 | context := contexts[contextName] 75 | activeMarker := " " 76 | if contextName == activeContext { 77 | activeMarker = "*" 78 | } 79 | slog.Info( 80 | fmt.Sprintf( 81 | "%s %-15s %-15s %s", 82 | activeMarker, 83 | contextName, 84 | context.Network, 85 | context.Description, 86 | ), 87 | ) 88 | } 89 | }, 90 | } 91 | } 92 | 93 | func contextSelectCommand() *cobra.Command { 94 | return &cobra.Command{ 95 | Use: "select ", 96 | Short: "Select the active context", 97 | Args: func(cmd *cobra.Command, args []string) error { 98 | if len(args) == 0 { 99 | return errors.New("no context name provided") 100 | } 101 | if len(args) > 1 { 102 | return errors.New("only one context name may be specified") 103 | } 104 | return nil 105 | }, 106 | Run: func(cmd *cobra.Command, args []string) { 107 | pm := createPackageManager() 108 | if err := pm.SetActiveContext(args[0]); err != nil { 109 | slog.Error(fmt.Sprintf("failed to set active context: %s", err)) 110 | os.Exit(1) 111 | } 112 | slog.Info( 113 | fmt.Sprintf( 114 | "Selected context %q", 115 | args[0], 116 | ), 117 | ) 118 | }, 119 | } 120 | } 121 | 122 | func contextCreateCommand() *cobra.Command { 123 | cmd := &cobra.Command{ 124 | Use: "create ", 125 | Short: "Create a new context", 126 | Args: func(cmd *cobra.Command, args []string) error { 127 | if len(args) == 0 { 128 | return errors.New("no context name provided") 129 | } 130 | if len(args) > 1 { 131 | return errors.New("only one context name may be specified") 132 | } 133 | return nil 134 | }, 135 | Run: func(cmd *cobra.Command, args []string) { 136 | pm := createPackageManager() 137 | tmpContextName := args[0] 138 | tmpContext := pkgmgr.Context{ 139 | Description: contextFlags.description, 140 | Network: contextFlags.network, 141 | } 142 | if err := pm.AddContext(tmpContextName, tmpContext); err != nil { 143 | slog.Error(fmt.Sprintf("failed to add context: %s", err)) 144 | os.Exit(1) 145 | } 146 | }, 147 | } 148 | cmd.Flags(). 149 | StringVarP(&contextFlags.description, "description", "d", "", "specifies description for context") 150 | cmd.Flags(). 151 | StringVarP(&contextFlags.network, "network", "n", "", "specifies network for context. if not specified, it's set automatically on the first package install") 152 | return cmd 153 | } 154 | 155 | func contextDeleteCommand() *cobra.Command { 156 | cmd := &cobra.Command{ 157 | Use: "delete ", 158 | Short: "Delete a context", 159 | Args: func(cmd *cobra.Command, args []string) error { 160 | if len(args) == 0 { 161 | return errors.New("no context name provided") 162 | } 163 | if len(args) > 1 { 164 | return errors.New("only one context name may be specified") 165 | } 166 | return nil 167 | }, 168 | Run: func(cmd *cobra.Command, args []string) { 169 | pm := createPackageManager() 170 | // Store original context name 171 | origContextName, _ := pm.ActiveContext() 172 | // Make sure we're not deleting the active context 173 | if args[0] == origContextName { 174 | slog.Error(pkgmgr.ErrContextNoDeleteActive.Error()) 175 | os.Exit(1) 176 | } 177 | // Temporarily switch to selected context 178 | if err := pm.SetActiveContext(args[0]); err != nil { 179 | slog.Error(err.Error()) 180 | os.Exit(1) 181 | } 182 | installedPackages := pm.InstalledPackages() 183 | if len(installedPackages) > 0 { 184 | if !contextFlags.force { 185 | // Switch back to original context 186 | if err := pm.SetActiveContext(origContextName); err != nil { 187 | slog.Warn(err.Error()) 188 | } 189 | slog.Error( 190 | "cannot delete context with packages installed. Uninstall packages or run with -f/--force", 191 | ) 192 | os.Exit(1) 193 | } 194 | for _, installedPkg := range installedPackages { 195 | // Uninstall package 196 | if err := pm.Uninstall(installedPkg.Package.Name, false, true); err != nil { 197 | slog.Warn(err.Error()) 198 | } 199 | } 200 | } 201 | // Switch back to original context 202 | if err := pm.SetActiveContext(origContextName); err != nil { 203 | slog.Error(err.Error()) 204 | os.Exit(1) 205 | } 206 | if err := pm.DeleteContext(args[0]); err != nil { 207 | slog.Error(fmt.Sprintf("failed to delete context: %s", err)) 208 | os.Exit(1) 209 | } 210 | slog.Info( 211 | fmt.Sprintf( 212 | "Deleted context %q", 213 | args[0], 214 | ), 215 | ) 216 | }, 217 | } 218 | cmd.Flags(). 219 | BoolVarP(&contextFlags.force, "force", "f", false, "force removal of context with packages installed") 220 | return cmd 221 | } 222 | 223 | func contextEnvCommand() *cobra.Command { 224 | cmd := &cobra.Command{ 225 | Use: "env", 226 | Short: "Generate environment vars for current context", 227 | Run: func(cmd *cobra.Command, args []string) { 228 | pm := createPackageManager() 229 | contextEnv := pm.ContextEnv() 230 | var tmpKeys []string 231 | for k := range contextEnv { 232 | tmpKeys = append(tmpKeys, k) 233 | } 234 | sort.Strings(tmpKeys) 235 | for _, key := range tmpKeys { 236 | slog.Info( 237 | fmt.Sprintf( 238 | "export %s=%s", 239 | key, 240 | contextEnv[key], 241 | ), 242 | ) 243 | } 244 | }, 245 | } 246 | return cmd 247 | } 248 | -------------------------------------------------------------------------------- /cmd/cardano-up/down.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func downCommand() *cobra.Command { 11 | cmd := &cobra.Command{ 12 | Use: "down", 13 | Short: "Stops all Docker containers", 14 | Long: `Stops all running Docker containers for installed packages in the current context.`, 15 | RunE: func(cmd *cobra.Command, args []string) error { 16 | pm := createPackageManager() 17 | if err := pm.Down(); err != nil { 18 | slog.Error(err.Error()) 19 | os.Exit(1) 20 | } 21 | return nil 22 | }, 23 | } 24 | return cmd 25 | } 26 | -------------------------------------------------------------------------------- /cmd/cardano-up/info.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "log/slog" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | func infoCommand() *cobra.Command { 26 | return &cobra.Command{ 27 | Use: "info", 28 | Aliases: []string{"status"}, 29 | Short: "Show info for an installed package", 30 | Args: func(cmd *cobra.Command, args []string) error { 31 | if len(args) == 0 { 32 | return errors.New("no package provided") 33 | } 34 | if len(args) > 1 { 35 | return errors.New("only one package may be specified a a time") 36 | } 37 | return nil 38 | }, 39 | Run: func(cmd *cobra.Command, args []string) { 40 | pm := createPackageManager() 41 | if err := pm.Info(args[0]); err != nil { 42 | slog.Error(err.Error()) 43 | os.Exit(1) 44 | } 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cmd/cardano-up/install.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "log/slog" 21 | "os" 22 | 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | const ( 27 | // The default network used when installing into an empty context 28 | defaultNetwork = "preprod" 29 | ) 30 | 31 | var installFlags = struct { 32 | network string 33 | }{} 34 | 35 | func installCommand() *cobra.Command { 36 | installCmd := &cobra.Command{ 37 | Use: "install", 38 | Short: "Install package", 39 | Args: func(cmd *cobra.Command, args []string) error { 40 | if len(args) == 0 { 41 | return errors.New("no package provided") 42 | } 43 | if len(args) > 1 { 44 | return errors.New("only one package may be specified at a time") 45 | } 46 | return nil 47 | }, 48 | Run: installCommandRun, 49 | } 50 | installCmd.Flags(). 51 | StringVarP(&installFlags.network, "network", "n", "", fmt.Sprintf("specifies network for package (defaults to %q for empty context)", defaultNetwork)) 52 | return installCmd 53 | } 54 | 55 | func installCommandRun(cmd *cobra.Command, args []string) { 56 | pm := createPackageManager() 57 | activeContextName, activeContext := pm.ActiveContext() 58 | // Update context network if specified 59 | if installFlags.network != "" { 60 | activeContext.Network = installFlags.network 61 | if err := pm.UpdateContext(activeContextName, activeContext); err != nil { 62 | slog.Error(err.Error()) 63 | os.Exit(1) 64 | } 65 | slog.Debug( 66 | fmt.Sprintf( 67 | "set active context network to %q", 68 | installFlags.network, 69 | ), 70 | ) 71 | } 72 | // Check that context network is set 73 | if activeContext.Network == "" { 74 | activeContext.Network = defaultNetwork 75 | if err := pm.UpdateContext(activeContextName, activeContext); err != nil { 76 | slog.Error(err.Error()) 77 | os.Exit(1) 78 | } 79 | slog.Warn( 80 | fmt.Sprintf( 81 | "defaulting to network %q for context %q", 82 | defaultNetwork, 83 | activeContextName, 84 | ), 85 | ) 86 | } 87 | // Install requested package 88 | if err := pm.Install(args[0]); err != nil { 89 | slog.Error(err.Error()) 90 | os.Exit(1) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /cmd/cardano-up/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "log/slog" 20 | 21 | "github.com/blinklabs-io/cardano-up/pkgmgr" 22 | "github.com/hashicorp/go-version" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | var listFlags = struct { 27 | all bool 28 | }{} 29 | 30 | func listAvailableCommand() *cobra.Command { 31 | cmd := &cobra.Command{ 32 | Use: "list-available", 33 | Short: "List available packages", 34 | Run: func(cmd *cobra.Command, args []string) { 35 | pm := createPackageManager() 36 | packages := pm.AvailablePackages() 37 | verbose, _ := cmd.Flags().GetBool("verbose") 38 | 39 | slog.Info("Available packages:\n") 40 | slog.Info( 41 | fmt.Sprintf( 42 | "%-20s %-12s %s", 43 | "Name", 44 | "Version", 45 | "Description", 46 | ), 47 | ) 48 | if verbose { 49 | // show all versions of packages 50 | for _, tmpPackage := range packages { 51 | printPackageInfo(tmpPackage) 52 | } 53 | } else { 54 | // Shows only latest version of each package 55 | latestPackages := make(map[string]int) 56 | order := make([]string, 0) 57 | for index, pkg := range packages { 58 | packageName := pkg.Name 59 | packageVersion := pkg.Version 60 | existingIndex, exists := latestPackages[packageName] 61 | if !exists || compareVersions(packageVersion, packages[existingIndex].Version) { 62 | if !exists { 63 | order = append(order, packageName) 64 | } 65 | latestPackages[packageName] = index 66 | } 67 | } 68 | for _, name := range order { 69 | printPackageInfo(packages[latestPackages[name]]) 70 | } 71 | } 72 | }, 73 | } 74 | // Added a verbose flag 75 | cmd.Flags().BoolP("verbose", "v", false, "Show all versions of packages") 76 | return cmd 77 | } 78 | 79 | func listCommand() *cobra.Command { 80 | listCmd := &cobra.Command{ 81 | Use: "list", 82 | Short: "List installed packages", 83 | Run: func(cmd *cobra.Command, args []string) { 84 | pm := createPackageManager() 85 | activeContextName, _ := pm.ActiveContext() 86 | var packages []pkgmgr.InstalledPackage 87 | if listFlags.all { 88 | packages = pm.InstalledPackagesAllContexts() 89 | slog.Info("Installed packages (all contexts):\n") 90 | } else { 91 | packages = pm.InstalledPackages() 92 | slog.Info(fmt.Sprintf("Installed packages (from context %q):\n", activeContextName)) 93 | } 94 | if len(packages) > 0 { 95 | slog.Info( 96 | fmt.Sprintf( 97 | "%-20s %-12s %-15s %s", 98 | "Name", 99 | "Version", 100 | "Context", 101 | "Description", 102 | ), 103 | ) 104 | for _, tmpPackage := range packages { 105 | slog.Info( 106 | fmt.Sprintf( 107 | "%-20s %-12s %-15s %s", 108 | tmpPackage.Package.Name, 109 | tmpPackage.Package.Version, 110 | tmpPackage.Context, 111 | tmpPackage.Package.Description, 112 | ), 113 | ) 114 | } 115 | } else { 116 | slog.Info(`No packages installed`) 117 | } 118 | }, 119 | } 120 | listCmd.Flags(). 121 | BoolVarP(&listFlags.all, "all", "A", false, "show packages from all contexts (defaults to only active context)") 122 | return listCmd 123 | } 124 | 125 | // Prints packge details 126 | func printPackageInfo(pkg pkgmgr.Package) { 127 | slog.Info( 128 | fmt.Sprintf( 129 | "%-20s %-12s %s", 130 | pkg.Name, 131 | pkg.Version, 132 | pkg.Description, 133 | ), 134 | ) 135 | if len(pkg.Dependencies) > 0 { 136 | tmpOutput := " Requires: " 137 | for idx, dep := range pkg.Dependencies { 138 | tmpOutput += dep 139 | if idx < len(pkg.Dependencies)-1 { 140 | tmpOutput += ` | ` 141 | } 142 | } 143 | slog.Info(tmpOutput) 144 | } 145 | } 146 | 147 | // Compare semantic version of packages 148 | func compareVersions(v1 string, v2 string) bool { 149 | ver1, err1 := version.NewVersion(v1) 150 | ver2, err2 := version.NewVersion(v2) 151 | 152 | if err1 != nil || err2 != nil { 153 | return v1 > v2 154 | } 155 | return ver1.GreaterThan(ver2) 156 | } 157 | -------------------------------------------------------------------------------- /cmd/cardano-up/list_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestCompareVersions(t *testing.T) { 6 | cases := []struct { 7 | v1, v2 string 8 | expected bool 9 | }{ 10 | {"10.1.1", "2.0.0", true}, 11 | {"1.10.0", "1.9.0", true}, 12 | {"1.2.3", "1.2.3", false}, 13 | {"0.9.5", "1.0.0", false}, 14 | } 15 | 16 | for _, tc := range cases { 17 | result := compareVersions(tc.v1, tc.v2) 18 | if result == tc.expected { 19 | t.Logf( 20 | "Test Passed: compareVersions(%s, %s) = %v (Expected: %v)\n", 21 | tc.v1, 22 | tc.v2, 23 | result, 24 | tc.expected, 25 | ) 26 | 27 | } else { 28 | t.Errorf("Test Failed: compareVersions(%s, %s) = %v; Expected %v", tc.v1, tc.v2, result, tc.expected) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cmd/cardano-up/logs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "log/slog" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | var logsFlags = struct { 26 | follow bool 27 | tail string 28 | }{} 29 | 30 | func logsCommand() *cobra.Command { 31 | logsCmd := &cobra.Command{ 32 | Use: "logs", 33 | Short: "Show logs for an installed package", 34 | Args: func(cmd *cobra.Command, args []string) error { 35 | if len(args) == 0 { 36 | return errors.New("no package provided") 37 | } 38 | if len(args) > 1 { 39 | return errors.New("only one package may be specified at a time") 40 | } 41 | return nil 42 | }, 43 | Run: func(cmd *cobra.Command, args []string) { 44 | pm := createPackageManager() 45 | if err := pm.Logs(args[0], logsFlags.follow, logsFlags.tail, os.Stdout, os.Stderr); err != nil { 46 | slog.Error(err.Error()) 47 | os.Exit(1) 48 | } 49 | }, 50 | } 51 | logsCmd.Flags(). 52 | StringVarP(&logsFlags.tail, "tail", "n", "", "display at most X lines from the end of the log") 53 | logsCmd.Flags(). 54 | BoolVarP(&logsFlags.follow, "follow", "f", false, "follow log output") 55 | return logsCmd 56 | } 57 | -------------------------------------------------------------------------------- /cmd/cardano-up/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "log/slog" 20 | "os" 21 | 22 | "github.com/blinklabs-io/cardano-up/internal/consolelog" 23 | "github.com/blinklabs-io/cardano-up/pkgmgr" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | const ( 28 | programName = "cardano-up" 29 | ) 30 | 31 | func main() { 32 | globalFlags := struct { 33 | debug bool 34 | }{} 35 | 36 | rootCmd := &cobra.Command{ 37 | Use: programName, 38 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 39 | // Configure default logger 40 | logLevel := slog.LevelInfo 41 | if globalFlags.debug { 42 | logLevel = slog.LevelDebug 43 | } 44 | logger := slog.New( 45 | consolelog.NewHandler(os.Stdout, &slog.HandlerOptions{ 46 | Level: logLevel, 47 | }), 48 | ) 49 | slog.SetDefault(logger) 50 | }, 51 | } 52 | 53 | // Global flags 54 | rootCmd.PersistentFlags(). 55 | BoolVarP(&globalFlags.debug, "debug", "D", false, "enable debug logging") 56 | 57 | // Add subcommands 58 | rootCmd.AddCommand( 59 | contextCommand(), 60 | versionCommand(), 61 | listCommand(), 62 | listAvailableCommand(), 63 | logsCommand(), 64 | infoCommand(), 65 | installCommand(), 66 | uninstallCommand(), 67 | upCommand(), 68 | downCommand(), 69 | updateCommand(), 70 | upgradeCommand(), 71 | validateCommand(), 72 | ) 73 | 74 | if err := rootCmd.Execute(); err != nil { 75 | // NOTE: we purposely don't display the error, since cobra will have already displayed it 76 | os.Exit(1) 77 | } 78 | } 79 | 80 | func createPackageManager() *pkgmgr.PackageManager { 81 | cfg, err := pkgmgr.NewDefaultConfig() 82 | if err != nil { 83 | slog.Error(fmt.Sprintf("failed to create package manager: %s", err)) 84 | os.Exit(1) 85 | } 86 | // Allow setting registry URL/dir via env var 87 | if url, ok := os.LookupEnv("REGISTRY_URL"); ok { 88 | cfg.RegistryUrl = url 89 | } 90 | if dir, ok := os.LookupEnv("REGISTRY_DIR"); ok { 91 | cfg.RegistryDir = dir 92 | } 93 | pm, err := pkgmgr.NewPackageManager(cfg) 94 | if err != nil { 95 | slog.Error(fmt.Sprintf("failed to create package manager: %s", err)) 96 | os.Exit(1) 97 | } 98 | return pm 99 | } 100 | -------------------------------------------------------------------------------- /cmd/cardano-up/uninstall.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "log/slog" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | var uninstallFlags = struct { 26 | keepData bool 27 | }{} 28 | 29 | func uninstallCommand() *cobra.Command { 30 | uninstallCmd := &cobra.Command{ 31 | Use: "uninstall", 32 | Short: "Uninstall package", 33 | Args: func(cmd *cobra.Command, args []string) error { 34 | if len(args) == 0 { 35 | return errors.New("no package provided") 36 | } 37 | if len(args) > 1 { 38 | return errors.New("only one package may be specified at a time") 39 | } 40 | return nil 41 | }, 42 | Run: func(cmd *cobra.Command, args []string) { 43 | pm := createPackageManager() 44 | // Uninstall package 45 | if err := pm.Uninstall(args[0], uninstallFlags.keepData, false); err != nil { 46 | slog.Error(err.Error()) 47 | os.Exit(1) 48 | } 49 | }, 50 | } 51 | uninstallCmd.Flags(). 52 | BoolVarP(&uninstallFlags.keepData, "keep-data", "k", false, "don't cleanup package data") 53 | return uninstallCmd 54 | } 55 | -------------------------------------------------------------------------------- /cmd/cardano-up/up.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func upCommand() *cobra.Command { 11 | cmd := &cobra.Command{ 12 | Use: "up", 13 | Short: "Starts all Docker containers", 14 | Long: `Starts all stopped Docker containers for installed packages in the current context.`, 15 | RunE: func(cmd *cobra.Command, args []string) error { 16 | pm := createPackageManager() 17 | installedPackages := pm.InstalledPackages() 18 | if len(installedPackages) == 0 { 19 | slog.Warn( 20 | "no packages installed...automatically installing cardano-node", 21 | ) 22 | installCommandRun(cmd, []string{"cardano-node"}) 23 | } else { 24 | if err := pm.Up(); err != nil { 25 | slog.Error(err.Error()) 26 | os.Exit(1) 27 | } 28 | } 29 | return nil 30 | }, 31 | } 32 | return cmd 33 | } 34 | -------------------------------------------------------------------------------- /cmd/cardano-up/update.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "log/slog" 19 | "os" 20 | 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | func updateCommand() *cobra.Command { 25 | updateCmd := &cobra.Command{ 26 | Use: "update", 27 | Short: "Update the package registry cache", 28 | Run: func(cmd *cobra.Command, args []string) { 29 | pm := createPackageManager() 30 | if err := pm.UpdatePackages(); err != nil { 31 | slog.Error(err.Error()) 32 | os.Exit(1) 33 | } 34 | }, 35 | } 36 | return updateCmd 37 | } 38 | -------------------------------------------------------------------------------- /cmd/cardano-up/upgrade.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "log/slog" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | func upgradeCommand() *cobra.Command { 26 | upgradeCmd := &cobra.Command{ 27 | Use: "upgrade", 28 | Short: "Upgrade package", 29 | Args: func(cmd *cobra.Command, args []string) error { 30 | if len(args) == 0 { 31 | return errors.New("no package provided") 32 | } 33 | if len(args) > 1 { 34 | return errors.New("only one package may be specified at a time") 35 | } 36 | return nil 37 | }, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | pm := createPackageManager() 40 | // Upgrade requested package 41 | if err := pm.Upgrade(args[0]); err != nil { 42 | slog.Error(err.Error()) 43 | os.Exit(1) 44 | } 45 | }, 46 | } 47 | return upgradeCmd 48 | } 49 | -------------------------------------------------------------------------------- /cmd/cardano-up/validate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "log/slog" 21 | "os" 22 | "path/filepath" 23 | 24 | "github.com/blinklabs-io/cardano-up/pkgmgr" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | func validateCommand() *cobra.Command { 29 | validateCmd := &cobra.Command{ 30 | Use: "validate [path]", 31 | Short: "Validate package file(s) in the given directory", 32 | Args: func(cmd *cobra.Command, args []string) error { 33 | if len(args) > 1 { 34 | return errors.New( 35 | "only one package directory may be specified at a time", 36 | ) 37 | } 38 | return nil 39 | }, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | packagesDir := "." 42 | if len(args) > 0 { 43 | packagesDir = args[0] 44 | } 45 | absPackagesDir, err := filepath.Abs(packagesDir) 46 | if err != nil { 47 | slog.Error(err.Error()) 48 | os.Exit(1) 49 | } 50 | cfg, err := pkgmgr.NewDefaultConfig() 51 | if err != nil { 52 | slog.Error( 53 | fmt.Sprintf("failed to create package manager: %s", err), 54 | ) 55 | os.Exit(1) 56 | } 57 | // Point at provided registry dir 58 | cfg.RegistryDir = absPackagesDir 59 | pm, err := pkgmgr.NewPackageManager(cfg) 60 | if err != nil { 61 | slog.Error( 62 | fmt.Sprintf("failed to create package manager: %s", err), 63 | ) 64 | os.Exit(1) 65 | } 66 | slog.Info( 67 | "Validating packages in path " + absPackagesDir, 68 | ) 69 | if err := pm.ValidatePackages(); err != nil { 70 | slog.Error("problems were found") 71 | os.Exit(1) 72 | } 73 | slog.Info("No problems found!") 74 | }, 75 | } 76 | return validateCmd 77 | } 78 | -------------------------------------------------------------------------------- /cmd/cardano-up/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "log/slog" 20 | 21 | "github.com/blinklabs-io/cardano-up/internal/version" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | func versionCommand() *cobra.Command { 26 | return &cobra.Command{ 27 | Use: "version", 28 | Short: "Displays the version", 29 | Run: func(cmd *cobra.Command, args []string) { 30 | slog.Info( 31 | fmt.Sprintf("%s %s", programName, version.GetVersionString()), 32 | ) 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blinklabs-io/cardano-up 2 | 3 | go 1.23.6 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/Masterminds/sprig/v3 v3.3.0 9 | github.com/blinklabs-io/gouroboros v0.121.0 10 | github.com/docker/docker v28.2.2+incompatible 11 | github.com/docker/go-connections v0.5.0 12 | github.com/hashicorp/go-version v1.7.0 13 | github.com/spf13/cobra v1.9.1 14 | gopkg.in/yaml.v3 v3.0.1 15 | ) 16 | 17 | require ( 18 | dario.cat/mergo v1.0.1 // indirect 19 | filippo.io/edwards25519 v1.1.0 // indirect 20 | github.com/Masterminds/goutils v1.1.1 // indirect 21 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 22 | github.com/Microsoft/go-winio v0.4.14 // indirect 23 | github.com/btcsuite/btcd/btcutil v1.1.6 // indirect 24 | github.com/containerd/errdefs v1.0.0 // indirect 25 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 26 | github.com/containerd/log v0.1.0 // indirect 27 | github.com/distribution/reference v0.5.0 // indirect 28 | github.com/docker/go-units v0.5.0 // indirect 29 | github.com/felixge/httpsnoop v1.0.4 // indirect 30 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 31 | github.com/go-logr/logr v1.4.2 // indirect 32 | github.com/go-logr/stdr v1.2.2 // indirect 33 | github.com/gogo/protobuf v1.3.2 // indirect 34 | github.com/google/uuid v1.6.0 // indirect 35 | github.com/huandu/xstrings v1.5.0 // indirect 36 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 37 | github.com/jinzhu/copier v0.4.0 // indirect 38 | github.com/mitchellh/copystructure v1.2.0 // indirect 39 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 40 | github.com/moby/docker-image-spec v1.3.1 // indirect 41 | github.com/moby/sys/atomicwriter v0.1.0 // indirect 42 | github.com/moby/term v0.5.0 // indirect 43 | github.com/morikuni/aec v1.0.0 // indirect 44 | github.com/opencontainers/go-digest v1.0.0 // indirect 45 | github.com/opencontainers/image-spec v1.0.2 // indirect 46 | github.com/pkg/errors v0.9.1 // indirect 47 | github.com/shopspring/decimal v1.4.0 // indirect 48 | github.com/spf13/cast v1.7.0 // indirect 49 | github.com/spf13/pflag v1.0.6 // indirect 50 | github.com/utxorpc/go-codegen v0.16.0 // indirect 51 | github.com/x448/float16 v0.8.4 // indirect 52 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 53 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect 54 | go.opentelemetry.io/otel v1.34.0 // indirect 55 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 // indirect 56 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 57 | go.opentelemetry.io/otel/sdk v1.23.1 // indirect 58 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 59 | golang.org/x/crypto v0.38.0 // indirect 60 | golang.org/x/net v0.38.0 // indirect 61 | golang.org/x/sys v0.33.0 // indirect 62 | golang.org/x/time v0.5.0 // indirect 63 | google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect 64 | google.golang.org/protobuf v1.36.3 // indirect 65 | gotest.tools/v3 v3.5.1 // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 4 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 5 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 6 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 7 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 8 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 9 | github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= 10 | github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 11 | github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= 12 | github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 13 | github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= 14 | github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= 15 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 16 | github.com/blinklabs-io/gouroboros v0.121.0 h1:Hb8amqG2dOztE1r5cuZAUUKSUzusDuffdGgSSh+qIfs= 17 | github.com/blinklabs-io/gouroboros v0.121.0/go.mod h1:hAJS7mv7dYMbjXujmr6X8pJIzbYvDQIoQo10orJiOuo= 18 | github.com/blinklabs-io/ouroboros-mock v0.3.8 h1:+DAt2rx0ouZUxee5DBMgZq3I1+ZdxFSHG9g3tYl/FKU= 19 | github.com/blinklabs-io/ouroboros-mock v0.3.8/go.mod h1:UwQIf4KqZwO13P9d90fbi3UL/X7JaJfeEbqk+bEeFQA= 20 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 21 | github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= 22 | github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= 23 | github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= 24 | github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= 25 | github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= 26 | github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= 27 | github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= 28 | github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= 29 | github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= 30 | github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= 31 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 32 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 33 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 34 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 35 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 36 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 37 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 38 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= 39 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 40 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 41 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 42 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 43 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 44 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 45 | github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 46 | github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 47 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 48 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 49 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 50 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 51 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 52 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 53 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 54 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 56 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 58 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 59 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 60 | github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= 61 | github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 62 | github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= 63 | github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 64 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 65 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 66 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 67 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 68 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 69 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 70 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 71 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 72 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 73 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 74 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 75 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 76 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 77 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 78 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 79 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 80 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 81 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 82 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 83 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 84 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 85 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 86 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 87 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 88 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 89 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 90 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 91 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 92 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 93 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 94 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 95 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 96 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 97 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 98 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 99 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= 100 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= 101 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 102 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 103 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 104 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 105 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 106 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 107 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 108 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 109 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 110 | github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= 111 | github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= 112 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 113 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 114 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 115 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 116 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 117 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 118 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 119 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 120 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 121 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 122 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 123 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 124 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 125 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 126 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 127 | github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= 128 | github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= 129 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 130 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 131 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 132 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 133 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 134 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 135 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 136 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 137 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 138 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 139 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 140 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 141 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 142 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 143 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 144 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 145 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 146 | github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= 147 | github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 148 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 149 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 150 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 151 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 152 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 153 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 154 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 155 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 156 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 157 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 158 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 159 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 160 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 161 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 162 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 163 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 164 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 165 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 166 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 167 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 168 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 169 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 170 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 171 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 172 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 173 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 174 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 175 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 176 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 177 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 178 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 179 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 180 | github.com/utxorpc/go-codegen v0.16.0 h1:jPTyKtv2OI6Ms7U/goAYbaP6axAZ39vRmoWdjO/rkeM= 181 | github.com/utxorpc/go-codegen v0.16.0/go.mod h1:2Nwq1md4HEcO2guvTpH45slGHO2aGRbiXKx73FM65ow= 182 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 183 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 184 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 185 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 186 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 187 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 188 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU= 189 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA= 190 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 191 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 192 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 h1:o8iWeVFa1BcLtVEV0LzrCxV2/55tB3xLxADr6Kyoey4= 193 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1/go.mod h1:SEVfdK4IoBnbT2FXNM/k8yC08MrfbhWk3U4ljM8B3HE= 194 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 h1:cfuy3bXmLJS7M1RZmAL6SuhGtKUp2KEsrm00OlAXkq4= 195 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1/go.mod h1:22jr92C6KwlwItJmQzfixzQM3oyyuYLCfHiMY+rpsPU= 196 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 197 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 198 | go.opentelemetry.io/otel/sdk v1.23.1 h1:O7JmZw0h76if63LQdsBMKQDWNb5oEcOThG9IrxscV+E= 199 | go.opentelemetry.io/otel/sdk v1.23.1/go.mod h1:LzdEVR5am1uKOOwfBWFef2DCi1nu3SA8XQxx2IerWFk= 200 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 201 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 202 | go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= 203 | go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= 204 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 205 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 206 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 207 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 208 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 209 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 210 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 211 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 212 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 213 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 214 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 215 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 216 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 217 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 218 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 219 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 220 | golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 221 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 222 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 223 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 224 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 226 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 227 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 228 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 229 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 230 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 231 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 232 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 233 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 235 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 236 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 240 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 241 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 242 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 243 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 244 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 245 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 246 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 247 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 248 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 249 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 250 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 251 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 252 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 253 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 254 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 255 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 256 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 257 | google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= 258 | google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= 259 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= 260 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 261 | google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= 262 | google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 263 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 264 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 265 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 266 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 267 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 268 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 269 | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= 270 | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 271 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 272 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 273 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 274 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 275 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 276 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 277 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 278 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 279 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 280 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 281 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 282 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 283 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 284 | -------------------------------------------------------------------------------- /internal/consolelog/consolelog.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package consolelog 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "io" 21 | "log/slog" 22 | ) 23 | 24 | const ( 25 | colorBrightRed = "91" 26 | colorBrightYellow = "93" 27 | colorBrightMagenta = "95" 28 | ) 29 | 30 | type Handler struct { 31 | h slog.Handler 32 | out io.Writer 33 | } 34 | 35 | func NewHandler(out io.Writer, opts *slog.HandlerOptions) *Handler { 36 | if opts == nil { 37 | opts = &slog.HandlerOptions{} 38 | } 39 | return &Handler{ 40 | out: out, 41 | h: slog.NewTextHandler(out, &slog.HandlerOptions{ 42 | Level: opts.Level, 43 | }), 44 | } 45 | } 46 | 47 | func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool { 48 | return h.h.Enabled(ctx, level) 49 | } 50 | 51 | func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { 52 | return &Handler{h: h.h.WithAttrs(attrs)} 53 | } 54 | 55 | func (h *Handler) WithGroup(name string) slog.Handler { 56 | return &Handler{h: h.h.WithGroup(name)} 57 | } 58 | 59 | func (h *Handler) Handle(ctx context.Context, r slog.Record) error { 60 | var levelTag string 61 | switch r.Level { 62 | case slog.LevelDebug: 63 | levelTag = fmt.Sprintf("\033[%smDEBUG:\033[0m ", colorBrightMagenta) 64 | case slog.LevelInfo: 65 | // No tag for INFO 66 | levelTag = "" 67 | case slog.LevelWarn: 68 | levelTag = fmt.Sprintf("\033[%smWARNING:\033[0m ", colorBrightYellow) 69 | case slog.LevelError: 70 | levelTag = fmt.Sprintf("\033[%smERROR:\033[0m ", colorBrightRed) 71 | } 72 | msg := levelTag + r.Message + "\n" 73 | if _, err := h.out.Write([]byte(msg)); err != nil { 74 | return err 75 | } 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package version 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | // These are populated at build time 22 | var ( 23 | Version string 24 | CommitHash string 25 | ) 26 | 27 | func GetVersionString() string { 28 | if Version != "" { 29 | return fmt.Sprintf("%s (commit %s)", Version, CommitHash) 30 | } else { 31 | return fmt.Sprintf("devel (commit %s)", CommitHash) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | # Packages 2 | 3 | The packages repository has moved to 4 | 5 | github.com/blinklabs-io/cardano-up-packages 6 | 7 | and will automatically be used with newer instances of cardano-up. Upgrade 8 | your package to get the latest version. 9 | -------------------------------------------------------------------------------- /pkgmgr/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "fmt" 19 | "log/slog" 20 | "os" 21 | "path/filepath" 22 | "runtime" 23 | ) 24 | 25 | type Config struct { 26 | BinDir string 27 | CacheDir string 28 | ConfigDir string 29 | ContextDir string 30 | DataDir string 31 | Logger *slog.Logger 32 | Template *Template 33 | RequiredPackageTags []string 34 | RegistryUrl string 35 | RegistryDir string 36 | } 37 | 38 | func NewDefaultConfig() (Config, error) { 39 | userHomeDir, err := os.UserHomeDir() 40 | if err != nil { 41 | return Config{}, fmt.Errorf( 42 | "could not determine user home directory: %w", 43 | err, 44 | ) 45 | } 46 | userBinDir := userHomeDir + "/.local/bin" 47 | userDataDir := userHomeDir + "/.local/share" 48 | userCacheDir, err := os.UserCacheDir() 49 | if err != nil { 50 | return Config{}, fmt.Errorf( 51 | "could not determine user cache directory: %w", 52 | err, 53 | ) 54 | } 55 | userConfigDir, err := os.UserConfigDir() 56 | if err != nil { 57 | return Config{}, fmt.Errorf( 58 | "could not determine user config directory: %w", 59 | err, 60 | ) 61 | } 62 | ret := Config{ 63 | BinDir: userBinDir, 64 | CacheDir: filepath.Join( 65 | userCacheDir, 66 | "cardano-up", 67 | ), 68 | ConfigDir: filepath.Join( 69 | userConfigDir, 70 | "cardano-up", 71 | ), 72 | DataDir: filepath.Join( 73 | userDataDir, 74 | "cardano-up", 75 | ), 76 | Logger: slog.Default(), 77 | RequiredPackageTags: []string{ 78 | "docker", 79 | runtime.GOOS, 80 | runtime.GOARCH, 81 | }, 82 | RegistryUrl: "https://github.com/blinklabs-io/cardano-up-packages/archive/refs/heads/main.zip", 83 | } 84 | return ret, nil 85 | } 86 | -------------------------------------------------------------------------------- /pkgmgr/config_test.go: -------------------------------------------------------------------------------- 1 | package pkgmgr_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "testing" 8 | 9 | "github.com/blinklabs-io/cardano-up/pkgmgr" 10 | ) 11 | 12 | func TestNewDefaultConfig(t *testing.T) { 13 | testHome := "/path/to/user/home" 14 | var expectedCacheDir, expectedConfigDir string 15 | switch runtime.GOOS { 16 | case "linux": 17 | expectedCacheDir = filepath.Join(testHome, ".cache/cardano-up") 18 | expectedConfigDir = filepath.Join(testHome, ".config/cardano-up") 19 | case "darwin": 20 | expectedCacheDir = filepath.Join(testHome, "Library/Caches/cardano-up") 21 | expectedConfigDir = filepath.Join( 22 | testHome, 23 | "Library/Application Support/cardano-up", 24 | ) 25 | default: 26 | t.Fatalf("unsupported OS: %s", runtime.GOOS) 27 | } 28 | origEnvVars := setEnvVars( 29 | map[string]string{ 30 | "HOME": testHome, 31 | "XDG_CONFIG_HOME": "", 32 | "XDG_CACHE_HOME": "", 33 | }, 34 | ) 35 | defer func() { 36 | setEnvVars(origEnvVars) 37 | }() 38 | cfg, err := pkgmgr.NewDefaultConfig() 39 | if err != nil { 40 | t.Fatalf("unexpected error: %s", err) 41 | } 42 | if cfg.CacheDir != expectedCacheDir { 43 | t.Fatalf( 44 | "did not get expected cache dir, got %q, expected %q", 45 | cfg.CacheDir, 46 | expectedCacheDir, 47 | ) 48 | } 49 | if cfg.ConfigDir != expectedConfigDir { 50 | t.Fatalf( 51 | "did not get expected config dir, got %q, expected %q", 52 | cfg.ConfigDir, 53 | expectedConfigDir, 54 | ) 55 | } 56 | } 57 | 58 | func TestNewDefaultConfigXdgConfigCacheEnvVars(t *testing.T) { 59 | testHome := "/path/to/user/home" 60 | testXdgCacheHome := filepath.Join(testHome, ".cache-test") 61 | testXdgConfigHome := filepath.Join(testHome, ".config-test") 62 | var expectedCacheDir, expectedConfigDir string 63 | switch runtime.GOOS { 64 | case "linux": 65 | expectedCacheDir = filepath.Join(testXdgCacheHome, "cardano-up") 66 | expectedConfigDir = filepath.Join(testXdgConfigHome, "cardano-up") 67 | case "darwin": 68 | expectedCacheDir = filepath.Join(testHome, "Library/Caches/cardano-up") 69 | expectedConfigDir = filepath.Join( 70 | testHome, 71 | "Library/Application Support/cardano-up", 72 | ) 73 | default: 74 | t.Fatalf("unsupported OS: %s", runtime.GOOS) 75 | } 76 | origEnvVars := setEnvVars( 77 | map[string]string{ 78 | "HOME": testHome, 79 | "XDG_CONFIG_HOME": testXdgConfigHome, 80 | "XDG_CACHE_HOME": testXdgCacheHome, 81 | }, 82 | ) 83 | defer func() { 84 | setEnvVars(origEnvVars) 85 | }() 86 | cfg, err := pkgmgr.NewDefaultConfig() 87 | if err != nil { 88 | t.Fatalf("unexpected error: %s", err) 89 | } 90 | if cfg.CacheDir != expectedCacheDir { 91 | t.Fatalf( 92 | "did not get expected cache dir, got %q, expected %q", 93 | cfg.CacheDir, 94 | expectedCacheDir, 95 | ) 96 | } 97 | if cfg.ConfigDir != expectedConfigDir { 98 | t.Fatalf( 99 | "did not get expected config dir, got %q, expected %q", 100 | cfg.ConfigDir, 101 | expectedConfigDir, 102 | ) 103 | } 104 | } 105 | 106 | func TestNewDefaultConfigEmptyHome(t *testing.T) { 107 | expectedErrs := map[string]string{ 108 | "linux": "could not determine user home directory: $HOME is not defined", 109 | "darwin": "could not determine user home directory: $HOME is not defined", 110 | } 111 | origEnvVars := setEnvVars( 112 | map[string]string{ 113 | "HOME": "", 114 | "XDG_CONFIG_HOME": "", 115 | "XDG_CACHE_HOME": "", 116 | }, 117 | ) 118 | defer func() { 119 | setEnvVars(origEnvVars) 120 | }() 121 | _, err := pkgmgr.NewDefaultConfig() 122 | expectedErr, ok := expectedErrs[runtime.GOOS] 123 | if !ok { 124 | t.Fatalf("unsupported OS: %s", runtime.GOOS) 125 | } 126 | if err == nil || err.Error() != expectedErr { 127 | t.Fatalf( 128 | "did not get expected error, got %q, expected %q", 129 | err.Error(), 130 | expectedErr, 131 | ) 132 | } 133 | } 134 | 135 | func setEnvVars(envVars map[string]string) map[string]string { 136 | origVars := map[string]string{} 137 | for k, v := range envVars { 138 | origVars[k] = os.Getenv(k) 139 | os.Setenv(k, v) 140 | } 141 | return origVars 142 | } 143 | -------------------------------------------------------------------------------- /pkgmgr/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | const ( 18 | defaultContextName = "default" 19 | ) 20 | 21 | var defaultContext = Context{ 22 | Description: "Default context", 23 | } 24 | 25 | type Context struct { 26 | Description string `yaml:"description"` 27 | Network string `yaml:"network"` 28 | NetworkMagic uint32 `yaml:"networkMagic"` 29 | } 30 | -------------------------------------------------------------------------------- /pkgmgr/docker.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "bufio" 19 | "context" 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "log/slog" 25 | "os" 26 | "path/filepath" 27 | "sort" 28 | "strings" 29 | 30 | "github.com/docker/docker/api/types/container" 31 | "github.com/docker/docker/api/types/image" 32 | "github.com/docker/docker/client" 33 | "github.com/docker/docker/pkg/stdcopy" 34 | "github.com/docker/go-connections/nat" 35 | ) 36 | 37 | const ( 38 | dockerInstallError = `could not contact Docker daemon 39 | 40 | Docker is required to be already installed and running. Please refer to the following pages for more information 41 | about how to install Docker. 42 | 43 | * https://docs.docker.com/get-docker/ 44 | * https://docs.docker.com/engine/install/ 45 | 46 | If Docker is already installed but the socket is not in a standard location, you can use the DOCKER_HOST environment 47 | variable to point to it. 48 | ` 49 | ) 50 | 51 | type DockerService struct { 52 | client *client.Client 53 | logger *slog.Logger 54 | ContainerId string 55 | ContainerName string 56 | Image string 57 | Env map[string]string 58 | Command []string 59 | Args []string 60 | Binds []string 61 | Ports []string 62 | } 63 | 64 | func NewDockerServiceFromContainerName( 65 | containerName string, 66 | logger *slog.Logger, 67 | ) (*DockerService, error) { 68 | ret := &DockerService{ 69 | logger: logger, 70 | } 71 | client, err := ret.getClient() 72 | if err != nil { 73 | return nil, err 74 | } 75 | tmpContainers, err := client.ContainerList( 76 | context.Background(), 77 | container.ListOptions{ 78 | All: true, 79 | }, 80 | ) 81 | if err != nil { 82 | return nil, err 83 | } 84 | for _, tmpContainer := range tmpContainers { 85 | for _, tmpContainerName := range tmpContainer.Names { 86 | tmpContainerName = strings.TrimPrefix(tmpContainerName, `/`) 87 | if tmpContainerName == containerName { 88 | ret.ContainerId = tmpContainer.ID 89 | if err := ret.refresh(); err != nil { 90 | return nil, err 91 | } 92 | return ret, nil 93 | } 94 | } 95 | } 96 | return nil, ErrContainerNotExists 97 | } 98 | 99 | func (d *DockerService) Running() (bool, error) { 100 | container, err := d.inspect() 101 | if err != nil { 102 | return false, err 103 | } 104 | return container.State.Running, nil 105 | } 106 | 107 | func (d *DockerService) Start() error { 108 | running, err := d.Running() 109 | if err != nil { 110 | return err 111 | } 112 | if !running { 113 | client, err := d.getClient() 114 | if err != nil { 115 | return err 116 | } 117 | d.logger.Debug("starting container " + d.ContainerName) 118 | if err := client.ContainerStart( 119 | context.Background(), 120 | d.ContainerId, 121 | container.StartOptions{}, 122 | ); err != nil { 123 | return err 124 | } 125 | } 126 | return nil 127 | } 128 | 129 | func (d *DockerService) Stop() error { 130 | running, err := d.Running() 131 | if err != nil { 132 | return err 133 | } 134 | if running { 135 | client, err := d.getClient() 136 | if err != nil { 137 | return err 138 | } 139 | d.logger.Debug("stopping container " + d.ContainerName) 140 | stopTimeout := 60 141 | if err := client.ContainerStop( 142 | context.Background(), 143 | d.ContainerId, 144 | container.StopOptions{ 145 | Timeout: &stopTimeout, 146 | }, 147 | ); err != nil { 148 | return err 149 | } 150 | } 151 | return nil 152 | } 153 | 154 | func (d *DockerService) Create() error { 155 | client, err := d.getClient() 156 | if err != nil { 157 | return err 158 | } 159 | if err := d.pullImage(); err != nil { 160 | return err 161 | } 162 | // Convert env 163 | tmpEnv := []string{} 164 | for k, v := range d.Env { 165 | tmpEnv = append( 166 | tmpEnv, 167 | fmt.Sprintf("%s=%s", k, v), 168 | ) 169 | } 170 | sort.Strings(tmpEnv) 171 | // Convert ports 172 | _, tmpPorts, err := nat.ParsePortSpecs(d.Ports) 173 | if err != nil { 174 | return err 175 | } 176 | 177 | // Create a new PortSet to expose all ports 178 | exposePorts := make(nat.PortSet) 179 | for port := range tmpPorts { 180 | exposePorts[port] = struct{}{} 181 | } 182 | 183 | // Set the desired user ID and group ID 184 | userID := os.Getuid() 185 | groupID := os.Getgid() 186 | userAndGroup := fmt.Sprintf("%d:%d", userID, groupID) 187 | // Create container 188 | d.logger.Debug("creating container " + d.ContainerName) 189 | resp, err := client.ContainerCreate( 190 | context.Background(), 191 | &container.Config{ 192 | Hostname: d.ContainerName, 193 | Image: d.Image, 194 | Entrypoint: d.Command, 195 | Cmd: d.Args, 196 | Env: tmpEnv[:], 197 | User: userAndGroup, 198 | ExposedPorts: exposePorts, 199 | }, 200 | &container.HostConfig{ 201 | RestartPolicy: container.RestartPolicy{ 202 | Name: container.RestartPolicyUnlessStopped, 203 | }, 204 | Binds: d.Binds[:], 205 | PortBindings: tmpPorts, 206 | }, 207 | nil, 208 | nil, 209 | d.ContainerName, 210 | ) 211 | if err != nil { 212 | return err 213 | } 214 | d.ContainerId = resp.ID 215 | for _, warning := range resp.Warnings { 216 | d.logger.Warn(warning) 217 | } 218 | return nil 219 | } 220 | 221 | func (d *DockerService) Remove() error { 222 | running, err := d.Running() 223 | if err != nil { 224 | return err 225 | } 226 | if running { 227 | return errors.New("can't remove a running container") 228 | } 229 | client, err := d.getClient() 230 | if err != nil { 231 | return err 232 | } 233 | d.logger.Debug("removing container " + d.ContainerName) 234 | if err := client.ContainerRemove( 235 | context.Background(), 236 | d.ContainerId, 237 | container.RemoveOptions{}, 238 | ); err != nil { 239 | return err 240 | } 241 | return nil 242 | } 243 | 244 | func (d *DockerService) Logs( 245 | follow bool, 246 | tail string, 247 | stdoutWriter io.Writer, 248 | stderrWriter io.Writer, 249 | ) error { 250 | client, err := d.getClient() 251 | if err != nil { 252 | return err 253 | } 254 | logsOut, err := client.ContainerLogs( 255 | context.Background(), 256 | d.ContainerName, 257 | container.LogsOptions{ 258 | Follow: follow, 259 | Tail: tail, 260 | ShowStdout: true, 261 | ShowStderr: true, 262 | }, 263 | ) 264 | if err != nil { 265 | return err 266 | } 267 | defer logsOut.Close() 268 | if _, err := stdcopy.StdCopy(stdoutWriter, stderrWriter, logsOut); err != nil { 269 | if !errors.Is(err, io.EOF) { 270 | return err 271 | } 272 | } 273 | return nil 274 | } 275 | 276 | func (d *DockerService) pullImage() error { 277 | client, err := d.getClient() 278 | if err != nil { 279 | return err 280 | } 281 | out, err := client.ImagePull( 282 | context.Background(), 283 | d.Image, 284 | image.PullOptions{}, 285 | ) 286 | if err != nil { 287 | return err 288 | } 289 | defer out.Close() 290 | // Log pull progress 291 | scanner := bufio.NewScanner(out) 292 | for scanner.Scan() { 293 | var tmpStatus struct { 294 | Status string `json:"status"` 295 | ProgressDetail map[string]any `json:"progressDetail"` 296 | Id string `json:"id"` 297 | } 298 | line := scanner.Text() 299 | if err := json.Unmarshal([]byte(line), &tmpStatus); err != nil { 300 | d.logger.Warn( 301 | fmt.Sprintf( 302 | "failed to unmarshal docker image pull status update: %s", 303 | err, 304 | ), 305 | ) 306 | } 307 | // Skip progress update lines 308 | if len(tmpStatus.ProgressDetail) > 0 { 309 | continue 310 | } 311 | if tmpStatus.Id == "" { 312 | d.logger.Info(tmpStatus.Status) 313 | } else { 314 | d.logger.Info( 315 | fmt.Sprintf( 316 | "%s: %s", 317 | tmpStatus.Id, 318 | tmpStatus.Status, 319 | ), 320 | ) 321 | } 322 | } 323 | if err := scanner.Err(); err != nil { 324 | return err 325 | } 326 | return nil 327 | } 328 | 329 | func (d *DockerService) inspect() (container.InspectResponse, error) { 330 | client, err := d.getClient() 331 | if err != nil { 332 | return container.InspectResponse{}, err 333 | } 334 | containerResponse, err := client.ContainerInspect( 335 | context.Background(), 336 | d.ContainerId, 337 | ) 338 | if err != nil { 339 | return container.InspectResponse{}, err 340 | } 341 | return containerResponse, nil 342 | } 343 | 344 | func (d *DockerService) refresh() error { 345 | container, err := d.inspect() 346 | if err != nil { 347 | return err 348 | } 349 | d.ContainerName = strings.TrimPrefix(container.Name, `/`) 350 | d.Image = container.Config.Image 351 | d.Env = make(map[string]string) 352 | for _, tmpEnv := range container.Config.Env { 353 | envVarParts := strings.SplitN(tmpEnv, `=`, 2) 354 | if envVarParts != nil { 355 | envVarName, envVarValue := envVarParts[0], envVarParts[1] 356 | d.Env[envVarName] = envVarValue 357 | } 358 | } 359 | d.Command = container.Config.Entrypoint[:] 360 | d.Args = container.Config.Cmd[:] 361 | tmpBinds := []string{} 362 | for _, mount := range container.Mounts { 363 | if mount.Type != "bind" { 364 | continue 365 | } 366 | tmpRoRwFlag := "ro" 367 | if mount.RW { 368 | tmpRoRwFlag = "rw" 369 | } 370 | tmpBind := fmt.Sprintf( 371 | "%s:%s:%s", 372 | mount.Source, 373 | mount.Destination, 374 | tmpRoRwFlag, 375 | ) 376 | tmpBinds = append(tmpBinds, tmpBind) 377 | } 378 | d.Binds = tmpBinds[:] 379 | tmpPorts := []string{} 380 | for port, portBindings := range container.NetworkSettings.Ports { 381 | // Skip exposed container ports without a mapping 382 | if len(portBindings) == 0 { 383 | continue 384 | } 385 | tmpPort := fmt.Sprintf( 386 | "0.0.0.0:%s:%s", 387 | portBindings[0].HostPort, 388 | port.Port(), 389 | ) 390 | tmpPorts = append(tmpPorts, tmpPort) 391 | } 392 | d.Ports = tmpPorts[:] 393 | return nil 394 | } 395 | 396 | func (d *DockerService) getClient() (*client.Client, error) { 397 | if d.client == nil { 398 | tmpClient, err := NewDockerClient() 399 | if err != nil { 400 | return nil, err 401 | } 402 | d.client = tmpClient 403 | } 404 | return d.client, nil 405 | } 406 | 407 | func NewDockerClient() (*client.Client, error) { 408 | clientOpts := []client.Opt{ 409 | client.FromEnv, 410 | client.WithAPIVersionNegotiation(), 411 | } 412 | // Determine Docker socket path if env override isn't set 413 | if _, ok := os.LookupEnv("DOCKER_HOST"); !ok { 414 | // Determine fallback path for socket on Docker Desktop for Mac 415 | userHomeDir, err := os.UserHomeDir() 416 | if err != nil { 417 | return nil, err 418 | } 419 | fallbackSocketPath := filepath.Join( 420 | userHomeDir, 421 | ".docker", 422 | "run", 423 | "docker.sock", 424 | ) 425 | if _, err := os.Stat(client.DefaultDockerHost); err == nil { 426 | // Explicitly set the host to the default Docker socket path 427 | clientOpts = append( 428 | clientOpts, 429 | client.WithHost( 430 | `unix://`+client.DefaultDockerHost, 431 | ), 432 | ) 433 | } else if _, err := os.Stat(fallbackSocketPath); err == nil { 434 | // Set the host to the fallback socket path 435 | clientOpts = append( 436 | clientOpts, 437 | client.WithHost( 438 | `unix://`+fallbackSocketPath, 439 | ), 440 | ) 441 | } 442 | } 443 | tmpClient, err := client.NewClientWithOpts(clientOpts...) 444 | if err != nil { 445 | return nil, err 446 | } 447 | return tmpClient, nil 448 | } 449 | 450 | func CheckDockerConnectivity() error { 451 | if _, err := NewDockerClient(); err != nil { 452 | return errors.New(dockerInstallError) //nolint:staticcheck 453 | } 454 | return nil 455 | } 456 | 457 | func RemoveDockerImage(imageName string) error { 458 | client, err := NewDockerClient() 459 | if err != nil { 460 | return err 461 | } 462 | _, err = client.ImageRemove( 463 | context.Background(), 464 | imageName, 465 | image.RemoveOptions{}, 466 | ) 467 | if err != nil { 468 | return err 469 | } 470 | return nil 471 | } 472 | -------------------------------------------------------------------------------- /pkgmgr/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | ) 21 | 22 | // ErrOperationFailed is a placeholder error for operations that directly log errors. 23 | // It's used to signify when an operation has failed when the actual error message is 24 | // sent through the provided logger 25 | var ErrOperationFailed = errors.New("the operation has failed") 26 | 27 | // ErrMultipleInstallMethods is returned when a package's install steps specify more than one install method 28 | // on a single install step 29 | var ErrMultipleInstallMethods = errors.New( 30 | "only one install method may be specified in an install step", 31 | ) 32 | 33 | // ErrNoInstallMethods is returned when a package's install steps include an install step which has no 34 | // recognized install method specified 35 | var ErrNoInstallMethods = errors.New( 36 | "no supported install method specified on install step", 37 | ) 38 | 39 | // ErrContextNotExist is returned when trying to selecting/managing a context that does not exist 40 | var ErrContextNotExist = errors.New("context does not exist") 41 | 42 | // ErrContextAlreadyExists is returned when creating a context with a name that is already in use 43 | var ErrContextAlreadyExists = errors.New("specified context already exists") 44 | 45 | // ErrContextNoChangeNetwork is returned when updating a context with a network different than what was previously configured 46 | var ErrContextNoChangeNetwork = errors.New( 47 | "cannot change the configured network for a context", 48 | ) 49 | 50 | // ErrContextInstallNoNetwork is returned when performing an install with no network specified on the active context 51 | var ErrContextInstallNoNetwork = errors.New("no network specified for context") 52 | 53 | // ErrContextNoDeleteActive is returned when attempting to delete the active context 54 | var ErrContextNoDeleteActive = errors.New("cannot delete active context") 55 | 56 | // ErrContainerAlreadyExists is returned when creating a new container with a name that is already in use 57 | var ErrContainerAlreadyExists = errors.New("specified container already exists") 58 | 59 | // ErrContainerNotExists is returned when querying a container by name that doesn't exist 60 | var ErrContainerNotExists = errors.New("specified container does not exist") 61 | 62 | // ErrNoRegistryConfigured is returned when no registry is configured 63 | var ErrNoRegistryConfigured = errors.New("no package registry is configured") 64 | 65 | // ErrValidationFailed is returned when loading the package registry while doing package validation when a package failed to load 66 | var ErrValidationFailed = errors.New("validation failed") 67 | 68 | func NewUnknownNetworkError(networkName string) error { 69 | return fmt.Errorf( 70 | "unknown network %q", 71 | networkName, 72 | ) 73 | } 74 | 75 | func NewResolverPackageAlreadyInstalledError(pkgName string) error { 76 | return fmt.Errorf( 77 | "the package %q is already installed in the current context\n\nYou can use 'cardano-up context create' to create an empty context to install another instance of the package", 78 | pkgName, 79 | ) 80 | } 81 | 82 | func NewResolverNoAvailablePackageDependencyError(depSpec string) error { 83 | return fmt.Errorf( 84 | "no available package found for dependency: %s", 85 | depSpec, 86 | ) 87 | } 88 | 89 | func NewResolverNoAvailablePackage(pkgSpec string) error { 90 | return fmt.Errorf( 91 | "no available package found: %s", 92 | pkgSpec, 93 | ) 94 | } 95 | 96 | func NewResolverInstalledPackageNoMatchVersionSpecError( 97 | pkgName string, 98 | pkgVersion string, 99 | depSpec string, 100 | ) error { 101 | return fmt.Errorf( 102 | "installed package \"%s = %s\" does not match dependency: %s", 103 | pkgName, 104 | pkgVersion, 105 | depSpec, 106 | ) 107 | } 108 | 109 | func NewPackageNotInstalledError(pkgName string, context string) error { 110 | return fmt.Errorf( 111 | "package %q is not installed in context %q", 112 | pkgName, 113 | context, 114 | ) 115 | } 116 | 117 | func NewPackageUninstallWouldBreakDepsError( 118 | uninstallPkgName string, 119 | uninstallPkgVersion string, 120 | dependentPkgName string, 121 | dependentPkgVersion string, 122 | ) error { 123 | return fmt.Errorf( 124 | `uninstall of package "%s = %s" would break dependencies for package "%s = %s"`, 125 | uninstallPkgName, 126 | uninstallPkgVersion, 127 | dependentPkgName, 128 | dependentPkgVersion, 129 | ) 130 | } 131 | 132 | func NewNoPackageAvailableForUpgradeError(pkgSpec string) error { 133 | return fmt.Errorf( 134 | "no package available for upgrade: %s", 135 | pkgSpec, 136 | ) 137 | } 138 | 139 | func NewInstallStepConditionError(condition string, err error) error { 140 | return fmt.Errorf( 141 | "failure evaluating install step condition %q: %w", 142 | condition, 143 | err, 144 | ) 145 | } 146 | 147 | func NewNoServicesFoundError(pkgName string) error { 148 | return fmt.Errorf( 149 | "no services found for package %q", 150 | pkgName, 151 | ) 152 | } 153 | -------------------------------------------------------------------------------- /pkgmgr/installed_package.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "time" 19 | ) 20 | 21 | type InstalledPackage struct { 22 | Package Package 23 | InstalledTime time.Time 24 | Context string 25 | PostInstallNotes string 26 | Options map[string]bool 27 | Outputs map[string]string 28 | } 29 | 30 | func NewInstalledPackage( 31 | pkg Package, 32 | context string, 33 | postInstallNotes string, 34 | outputs map[string]string, 35 | options map[string]bool, 36 | ) InstalledPackage { 37 | return InstalledPackage{ 38 | Package: pkg, 39 | InstalledTime: time.Now(), 40 | Context: context, 41 | PostInstallNotes: postInstallNotes, 42 | Options: options, 43 | Outputs: outputs, 44 | } 45 | } 46 | 47 | func (i InstalledPackage) IsEmpty() bool { 48 | return i.InstalledTime.IsZero() 49 | } 50 | -------------------------------------------------------------------------------- /pkgmgr/package.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "io" 22 | "io/fs" 23 | "log/slog" 24 | "net/http" 25 | "net/url" 26 | "os" 27 | "os/exec" 28 | "path/filepath" 29 | "regexp" 30 | "runtime" 31 | "slices" 32 | "strings" 33 | 34 | "github.com/hashicorp/go-version" 35 | "gopkg.in/yaml.v3" 36 | ) 37 | 38 | type Package struct { 39 | Name string `yaml:"name,omitempty"` 40 | Version string `yaml:"version,omitempty"` 41 | Description string `yaml:"description,omitempty"` 42 | InstallSteps []PackageInstallStep `yaml:"installSteps,omitempty"` 43 | Dependencies []string `yaml:"dependencies,omitempty"` 44 | Tags []string `yaml:"tags,omitempty"` 45 | PreInstallScript string `yaml:"preInstallScript,omitempty"` 46 | PostInstallScript string `yaml:"postInstallScript,omitempty"` 47 | PreStartScript string `yaml:"preStartScript,omitempty"` 48 | PreStopScript string `yaml:"preStopScript,omitempty"` 49 | PreUninstallScript string `yaml:"preUninstallScript,omitempty"` 50 | PostUninstallScript string `yaml:"postUninstallScript,omitempty"` 51 | PostInstallNotes string `yaml:"postInstallNotes,omitempty"` 52 | Options []PackageOption `yaml:"options,omitempty"` 53 | Outputs []PackageOutput `yaml:"outputs,omitempty"` 54 | filePath string 55 | } 56 | 57 | type PackageOption struct { 58 | Name string `yaml:"name"` 59 | Description string `yaml:"description"` 60 | Default bool `yaml:"default"` 61 | } 62 | 63 | type PackageOutput struct { 64 | Name string `yaml:"name"` 65 | Description string `yaml:"description"` 66 | Value string `yaml:"value"` 67 | } 68 | 69 | func NewPackageFromFile(path string) (Package, error) { 70 | f, err := os.Open(path) 71 | if err != nil { 72 | return Package{}, err 73 | } 74 | defer f.Close() 75 | return NewPackageFromReader(f) 76 | } 77 | 78 | func NewPackageFromReader(r io.Reader) (Package, error) { 79 | var ret Package 80 | dec := yaml.NewDecoder(r) 81 | dec.KnownFields(true) 82 | if err := dec.Decode(&ret); err != nil { 83 | return Package{}, err 84 | } 85 | return ret, nil 86 | } 87 | 88 | func (p Package) IsEmpty() bool { 89 | return p.Name == "" && p.Version == "" 90 | } 91 | 92 | func (p Package) defaultOpts() map[string]bool { 93 | ret := make(map[string]bool) 94 | for _, opt := range p.Options { 95 | ret[opt.Name] = opt.Default 96 | } 97 | return ret 98 | } 99 | 100 | func (p Package) hasTags(tags []string) bool { 101 | for _, tag := range tags { 102 | foundTag := slices.Contains(p.Tags, tag) 103 | if !foundTag { 104 | return false 105 | } 106 | } 107 | return true 108 | } 109 | 110 | func (p Package) install( 111 | cfg Config, 112 | context string, 113 | opts map[string]bool, 114 | runHooks bool, 115 | ) (string, map[string]string, error) { 116 | // Update template vars 117 | pkgName := fmt.Sprintf("%s-%s-%s", p.Name, p.Version, context) 118 | pkgCacheDir := filepath.Join( 119 | cfg.CacheDir, 120 | pkgName, 121 | ) 122 | pkgContextDir := filepath.Join( 123 | cfg.DataDir, 124 | context, 125 | ) 126 | pkgDataDir := filepath.Join( 127 | cfg.DataDir, 128 | pkgName, 129 | ) 130 | cfg.Template = cfg.Template.WithVars( 131 | map[string]any{ 132 | "Package": map[string]any{ 133 | "Name": pkgName, 134 | "ShortName": p.Name, 135 | "Version": p.Version, 136 | "Options": opts, 137 | }, 138 | "Paths": map[string]string{ 139 | "BinDir": cfg.BinDir, 140 | "CacheDir": pkgCacheDir, 141 | "ContextDir": pkgContextDir, 142 | "DataDir": pkgDataDir, 143 | }, 144 | "System": map[string]string{ 145 | "OS": runtime.GOOS, 146 | "ARCH": runtime.GOARCH, 147 | }, 148 | }, 149 | ) 150 | // Run pre-flight checks 151 | for _, installStep := range p.InstallSteps { 152 | // Make sure only one install method is specified per install step 153 | if installStep.Docker != nil && 154 | installStep.File != nil { 155 | return "", nil, ErrMultipleInstallMethods 156 | } 157 | if installStep.Docker != nil { 158 | if err := installStep.Docker.preflight(cfg, pkgName); err != nil { 159 | return "", nil, fmt.Errorf("pre-flight check failed: %w", err) 160 | } 161 | } 162 | } 163 | // Pre-create dirs 164 | if err := os.MkdirAll(pkgCacheDir, fs.ModePerm); err != nil { 165 | return "", nil, err 166 | } 167 | if err := os.MkdirAll(pkgContextDir, fs.ModePerm); err != nil { 168 | return "", nil, err 169 | } 170 | if err := os.MkdirAll(pkgDataDir, fs.ModePerm); err != nil { 171 | return "", nil, err 172 | } 173 | // Run pre-install script 174 | if runHooks && p.PreInstallScript != "" { 175 | if err := p.runHookScript(cfg, p.PreInstallScript); err != nil { 176 | return "", nil, err 177 | } 178 | } 179 | // Perform install 180 | for _, installStep := range p.InstallSteps { 181 | // Evaluate condition if defined 182 | if installStep.Condition != "" { 183 | if ok, err := cfg.Template.EvaluateCondition(installStep.Condition, nil); err != nil { 184 | return "", nil, NewInstallStepConditionError( 185 | installStep.Condition, 186 | err, 187 | ) 188 | } else if !ok { 189 | cfg.Logger.Debug( 190 | "skipping install step due to condition: " + installStep.Condition, 191 | ) 192 | continue 193 | } 194 | } 195 | if installStep.Docker != nil { 196 | if err := installStep.Docker.install(cfg, pkgName); err != nil { 197 | return "", nil, err 198 | } 199 | } else if installStep.File != nil { 200 | if err := installStep.File.install(cfg, pkgName, p.filePath); err != nil { 201 | return "", nil, err 202 | } 203 | } else { 204 | return "", nil, ErrNoInstallMethods 205 | } 206 | } 207 | // Capture port details for output templates 208 | tmpPorts := map[string]map[string]string{} 209 | tmpServices, err := p.services(cfg, context) 210 | if err != nil { 211 | return "", nil, err 212 | } 213 | for _, svc := range tmpServices { 214 | shortContainerName := strings.TrimPrefix(svc.ContainerName, pkgName+`-`) 215 | tmpPortsContainer := make(map[string]string) 216 | for _, port := range svc.Ports { 217 | var containerPort, hostPort string 218 | portParts := strings.Split(port, ":") 219 | switch len(portParts) { 220 | case 1: 221 | containerPort = portParts[0] 222 | hostPort = portParts[0] 223 | case 2: 224 | containerPort = portParts[1] 225 | hostPort = portParts[0] 226 | case 3: 227 | containerPort = portParts[2] 228 | hostPort = portParts[1] 229 | } 230 | tmpPortsContainer[containerPort] = hostPort 231 | } 232 | tmpPorts[shortContainerName] = tmpPortsContainer 233 | } 234 | cfg.Template = cfg.Template.WithVars( 235 | map[string]any{ 236 | "Ports": tmpPorts, 237 | }, 238 | ) 239 | // Generate outputs 240 | retOutputs := make(map[string]string) 241 | for _, output := range p.Outputs { 242 | // Create key from package name and output name 243 | key := fmt.Sprintf( 244 | "%s_%s", 245 | p.Name, 246 | output.Name, 247 | ) 248 | // Replace all characters that won't work in an env var 249 | envRe := regexp.MustCompile(`[^A-Za-z0-9_]+`) 250 | key = string(envRe.ReplaceAll([]byte(key), []byte(`_`))) 251 | // Make uppercase 252 | key = strings.ToUpper(key) 253 | // Render value template 254 | val, err := cfg.Template.Render(output.Value, nil) 255 | if err != nil { 256 | return "", nil, err 257 | } 258 | retOutputs[key] = val 259 | } 260 | // Run post-install script 261 | if runHooks && p.PostInstallScript != "" { 262 | if err := p.runHookScript(cfg, p.PostInstallScript); err != nil { 263 | return "", nil, err 264 | } 265 | } 266 | // Render notes and return 267 | var retNotes string 268 | if p.PostInstallNotes != "" { 269 | tmpNotes, err := cfg.Template.Render(p.PostInstallNotes, nil) 270 | if err != nil { 271 | return "", nil, err 272 | } 273 | retNotes = tmpNotes 274 | } 275 | return retNotes, retOutputs, nil 276 | } 277 | 278 | func (p Package) uninstall( 279 | cfg Config, 280 | context string, 281 | keepData bool, 282 | runHooks bool, 283 | ) error { 284 | pkgName := fmt.Sprintf("%s-%s-%s", p.Name, p.Version, context) 285 | // Run pre-uninstall script 286 | if runHooks && p.PreUninstallScript != "" { 287 | if err := p.runHookScript(cfg, p.PreUninstallScript); err != nil { 288 | return err 289 | } 290 | } 291 | // Iterate over install steps in reverse 292 | for idx := len(p.InstallSteps) - 1; idx >= 0; idx-- { 293 | installStep := p.InstallSteps[idx] 294 | // Evaluate condition if defined 295 | if installStep.Condition != "" { 296 | if ok, err := cfg.Template.EvaluateCondition(installStep.Condition, nil); err != nil { 297 | return NewInstallStepConditionError(installStep.Condition, err) 298 | } else if !ok { 299 | cfg.Logger.Debug( 300 | "skipping uninstall step due to condition: " + installStep.Condition, 301 | ) 302 | continue 303 | } 304 | } 305 | // Make sure only one install method is specified per install step 306 | if installStep.Docker != nil && 307 | installStep.File != nil { 308 | return ErrMultipleInstallMethods 309 | } 310 | if installStep.Docker != nil { 311 | if err := installStep.Docker.uninstall(cfg, pkgName, keepData); err != nil { 312 | return err 313 | } 314 | } else if installStep.File != nil { 315 | if err := installStep.File.uninstall(cfg, pkgName); err != nil { 316 | return err 317 | } 318 | } else { 319 | return ErrNoInstallMethods 320 | } 321 | } 322 | if keepData { 323 | cfg.Logger.Debug( 324 | "skipping cleanup of package data/cache directories", 325 | ) 326 | } else { 327 | // Remove package cache dir 328 | pkgCacheDir := filepath.Join( 329 | cfg.CacheDir, 330 | pkgName, 331 | ) 332 | if err := os.RemoveAll(pkgCacheDir); err != nil { 333 | cfg.Logger.Warn( 334 | fmt.Sprintf( 335 | "failed to remove package cache directory %q: %s", 336 | pkgCacheDir, 337 | err, 338 | ), 339 | ) 340 | } else { 341 | cfg.Logger.Debug( 342 | fmt.Sprintf( 343 | "removed package cache directory %q", 344 | pkgCacheDir, 345 | ), 346 | ) 347 | } 348 | // Remove package data dir 349 | pkgDataDir := filepath.Join( 350 | cfg.DataDir, 351 | pkgName, 352 | ) 353 | if err := os.RemoveAll(pkgDataDir); err != nil { 354 | cfg.Logger.Warn( 355 | fmt.Sprintf( 356 | "failed to remove package data directory %q: %s", 357 | pkgDataDir, 358 | err, 359 | ), 360 | ) 361 | } else { 362 | cfg.Logger.Debug( 363 | fmt.Sprintf( 364 | "removed package data directory %q", 365 | pkgDataDir, 366 | ), 367 | ) 368 | } 369 | } 370 | // Run post-uninstall script 371 | if runHooks && p.PostUninstallScript != "" { 372 | if err := p.runHookScript(cfg, p.PostUninstallScript); err != nil { 373 | return err 374 | } 375 | } 376 | return nil 377 | } 378 | 379 | func (p Package) activate(cfg Config, context string) error { 380 | pkgName := fmt.Sprintf("%s-%s-%s", p.Name, p.Version, context) 381 | for _, installStep := range p.InstallSteps { 382 | // Evaluate condition if defined 383 | if installStep.Condition != "" { 384 | if ok, err := cfg.Template.EvaluateCondition(installStep.Condition, nil); err != nil { 385 | return NewInstallStepConditionError(installStep.Condition, err) 386 | } else if !ok { 387 | cfg.Logger.Debug( 388 | "skipping install step due to condition: " + installStep.Condition, 389 | ) 390 | continue 391 | } 392 | } 393 | if installStep.Docker != nil { 394 | if err := installStep.Docker.activate(cfg, pkgName); err != nil { 395 | return err 396 | } 397 | } else if installStep.File != nil { 398 | if err := installStep.File.activate(cfg, pkgName); err != nil { 399 | return err 400 | } 401 | } else { 402 | return ErrNoInstallMethods 403 | } 404 | } 405 | return nil 406 | } 407 | 408 | func (p Package) deactivate(cfg Config, context string) error { 409 | pkgName := fmt.Sprintf("%s-%s-%s", p.Name, p.Version, context) 410 | for _, installStep := range p.InstallSteps { 411 | // Evaluate condition if defined 412 | if installStep.Condition != "" { 413 | if ok, err := cfg.Template.EvaluateCondition(installStep.Condition, nil); err != nil { 414 | return NewInstallStepConditionError(installStep.Condition, err) 415 | } else if !ok { 416 | cfg.Logger.Debug( 417 | "skipping install step due to condition: " + installStep.Condition, 418 | ) 419 | continue 420 | } 421 | } 422 | if installStep.Docker != nil { 423 | if err := installStep.Docker.deactivate(cfg, pkgName); err != nil { 424 | return err 425 | } 426 | } else if installStep.File != nil { 427 | if err := installStep.File.deactivate(cfg, pkgName); err != nil { 428 | return err 429 | } 430 | } else { 431 | return ErrNoInstallMethods 432 | } 433 | } 434 | return nil 435 | } 436 | 437 | func (p Package) validate(cfg Config) error { 438 | // Check empty name 439 | if p.Name == "" { 440 | return errors.New("package name cannot be empty") 441 | } 442 | // Check name matches allowed characters 443 | reName := regexp.MustCompile(`^[-a-zA-Z0-9]+$`) 444 | if !reName.Match([]byte(p.Name)) { 445 | return fmt.Errorf("invalid package name: %s", p.Name) 446 | } 447 | // Check empty version 448 | if p.Version == "" { 449 | return errors.New("package version cannot be empty") 450 | } 451 | // Check version is well formed 452 | if _, err := version.NewVersion(p.Version); err != nil { 453 | return fmt.Errorf("package version is malformed: %w", err) 454 | } 455 | // Check if package path matches package name/version 456 | expectedFilePath := filepath.Join( 457 | p.Name, 458 | fmt.Sprintf( 459 | "%s-%s.yaml", 460 | p.Name, 461 | p.Version, 462 | ), 463 | ) 464 | if !strings.HasSuffix(p.filePath, expectedFilePath) { 465 | return fmt.Errorf( 466 | "package did not have expected file path: %s", 467 | expectedFilePath, 468 | ) 469 | } 470 | // Validate install steps 471 | for _, installStep := range p.InstallSteps { 472 | // Evaluate condition if defined 473 | if installStep.Condition != "" { 474 | if _, err := cfg.Template.EvaluateCondition(installStep.Condition, nil); err != nil { 475 | return NewInstallStepConditionError(installStep.Condition, err) 476 | } 477 | } 478 | if installStep.Docker != nil { 479 | if err := installStep.Docker.validate(cfg); err != nil { 480 | return err 481 | } 482 | } else if installStep.File != nil { 483 | if err := installStep.File.validate(cfg); err != nil { 484 | return err 485 | } 486 | } else { 487 | return ErrNoInstallMethods 488 | } 489 | } 490 | return nil 491 | } 492 | 493 | func (p Package) startService(cfg Config, context string) error { 494 | pkgName := fmt.Sprintf("%s-%s-%s", p.Name, p.Version, context) 495 | 496 | // Run pre-start script 497 | if p.PreStartScript != "" { 498 | if err := p.runHookScript(cfg, p.PreStartScript); err != nil { 499 | return fmt.Errorf("pre-start hook failed: %w", err) 500 | } 501 | } 502 | var startErrors []string 503 | for _, step := range p.InstallSteps { 504 | if step.Docker != nil { 505 | if step.Docker.PullOnly { 506 | continue 507 | } 508 | containerName := fmt.Sprintf( 509 | "%s-%s", 510 | pkgName, 511 | step.Docker.ContainerName, 512 | ) 513 | dockerService, err := NewDockerServiceFromContainerName( 514 | containerName, 515 | cfg.Logger, 516 | ) 517 | if err != nil { 518 | startErrors = append( 519 | startErrors, 520 | fmt.Sprintf( 521 | "error initializing Docker service for container %s: %v", 522 | containerName, 523 | err, 524 | ), 525 | ) 526 | continue 527 | } 528 | // Start the Docker container if it's not running 529 | slog.Info( 530 | "Starting Docker container " + containerName, 531 | ) 532 | if err := dockerService.Start(); err != nil { 533 | startErrors = append( 534 | startErrors, 535 | fmt.Sprintf( 536 | "failed to start Docker container %s: %v", 537 | containerName, 538 | err, 539 | ), 540 | ) 541 | } 542 | } 543 | } 544 | 545 | if len(startErrors) > 0 { 546 | slog.Error(strings.Join(startErrors, "\n")) 547 | return ErrOperationFailed 548 | } 549 | 550 | return nil 551 | } 552 | 553 | func (p Package) stopService(cfg Config, context string) error { 554 | pkgName := fmt.Sprintf("%s-%s-%s", p.Name, p.Version, context) 555 | 556 | // Run pre-stop script 557 | if p.PreStopScript != "" { 558 | if err := p.runHookScript(cfg, p.PreStopScript); err != nil { 559 | return fmt.Errorf("pre-stop hook failed: %w", err) 560 | } 561 | } 562 | 563 | var stopErrors []string 564 | for _, step := range p.InstallSteps { 565 | if step.Docker != nil { 566 | if step.Docker.PullOnly { 567 | continue 568 | } 569 | containerName := fmt.Sprintf( 570 | "%s-%s", 571 | pkgName, 572 | step.Docker.ContainerName, 573 | ) 574 | dockerService, err := NewDockerServiceFromContainerName( 575 | containerName, 576 | cfg.Logger, 577 | ) 578 | if err != nil { 579 | stopErrors = append( 580 | stopErrors, 581 | fmt.Sprintf( 582 | "error initializing Docker service for container %s: %v", 583 | containerName, 584 | err, 585 | ), 586 | ) 587 | continue 588 | } 589 | // Stop the Docker container 590 | slog.Info("Stopping container " + containerName) 591 | if err := dockerService.Stop(); err != nil { 592 | stopErrors = append( 593 | stopErrors, 594 | fmt.Sprintf( 595 | "failed to stop Docker container %s: %v", 596 | containerName, 597 | err, 598 | ), 599 | ) 600 | } 601 | } 602 | } 603 | 604 | if len(stopErrors) > 0 { 605 | slog.Error(strings.Join(stopErrors, "\n")) 606 | return ErrOperationFailed 607 | } 608 | 609 | return nil 610 | } 611 | 612 | func (p Package) services( 613 | cfg Config, 614 | context string, 615 | ) ([]*DockerService, error) { 616 | var ret []*DockerService 617 | pkgName := fmt.Sprintf("%s-%s-%s", p.Name, p.Version, context) 618 | for _, step := range p.InstallSteps { 619 | if step.Docker != nil { 620 | if step.Docker.PullOnly { 621 | continue 622 | } 623 | containerName := fmt.Sprintf( 624 | "%s-%s", 625 | pkgName, 626 | step.Docker.ContainerName, 627 | ) 628 | dockerService, err := NewDockerServiceFromContainerName( 629 | containerName, 630 | cfg.Logger, 631 | ) 632 | if err != nil { 633 | cfg.Logger.Error( 634 | fmt.Sprintf( 635 | "error initializing Docker service for container %s: %v", 636 | containerName, 637 | err, 638 | ), 639 | ) 640 | return ret, ErrOperationFailed 641 | } 642 | ret = append(ret, dockerService) 643 | } 644 | } 645 | return ret, nil 646 | } 647 | 648 | func (p Package) runHookScript(cfg Config, hookScript string) error { 649 | renderedScript, err := cfg.Template.Render(hookScript, nil) 650 | if err != nil { 651 | return fmt.Errorf("failed to render hook script template: %w", err) 652 | } 653 | cmd := exec.Command("/bin/sh", "-c", renderedScript) 654 | cmd.Stdout = os.Stdout 655 | cmd.Stderr = os.Stderr 656 | // We won't be reading or writing, so throw away the PTY file 657 | err = cmd.Start() 658 | if err != nil { 659 | return fmt.Errorf("failed to run hook script: %w", err) 660 | } 661 | err = cmd.Wait() 662 | if err != nil { 663 | return fmt.Errorf("run hook script exited with error: %w", err) 664 | } 665 | return nil 666 | } 667 | 668 | type PackageInstallStep struct { 669 | Condition string `yaml:"condition,omitempty"` 670 | Docker *PackageInstallStepDocker `yaml:"docker,omitempty"` 671 | File *PackageInstallStepFile `yaml:"file,omitempty"` 672 | } 673 | 674 | type PackageInstallStepDocker struct { 675 | ContainerName string `yaml:"containerName"` 676 | Image string `yaml:"image,omitempty"` 677 | Env map[string]string `yaml:"env,omitempty"` 678 | Command []string `yaml:"command,omitempty"` 679 | Args []string `yaml:"args,omitempty"` 680 | Binds []string `yaml:"binds,omitempty"` 681 | Ports []string `yaml:"ports,omitempty"` 682 | PullOnly bool `yaml:"pullOnly"` 683 | } 684 | 685 | func (p *PackageInstallStepDocker) validate(cfg Config) error { 686 | if p.Image == "" { 687 | cfg.Logger.Debug("docker image missing") 688 | return errors.New("docker image must be provided") 689 | } 690 | // TODO: add more checks 691 | return nil 692 | } 693 | 694 | func (p *PackageInstallStepDocker) preflight(cfg Config, pkgName string) error { 695 | if err := CheckDockerConnectivity(); err != nil { 696 | return err 697 | } 698 | containerName := fmt.Sprintf("%s-%s", pkgName, p.ContainerName) 699 | if _, err := NewDockerServiceFromContainerName(containerName, cfg.Logger); err != nil { 700 | if errors.Is(err, ErrContainerNotExists) { 701 | // Container does not exist (we want this) 702 | return nil 703 | } else { 704 | return err 705 | } 706 | } 707 | return ErrContainerAlreadyExists 708 | } 709 | 710 | func (p *PackageInstallStepDocker) install(cfg Config, pkgName string) error { 711 | containerName := fmt.Sprintf("%s-%s", pkgName, p.ContainerName) 712 | extraVars := map[string]any{ 713 | "Container": map[string]any{ 714 | "Name": containerName, 715 | }, 716 | } 717 | tmpImage, err := cfg.Template.Render(p.Image, extraVars) 718 | if err != nil { 719 | return err 720 | } 721 | tmpEnv := make(map[string]string) 722 | for k, v := range p.Env { 723 | tmplVal, err := cfg.Template.Render(v, extraVars) 724 | if err != nil { 725 | return err 726 | } 727 | tmpEnv[k] = tmplVal 728 | } 729 | //nolint:prealloc 730 | var tmpCommand []string 731 | for _, cmd := range p.Command { 732 | tmpCmd, err := cfg.Template.Render(cmd, extraVars) 733 | if err != nil { 734 | return err 735 | } 736 | tmpCommand = append(tmpCommand, tmpCmd) 737 | } 738 | //nolint:prealloc 739 | var tmpArgs []string 740 | for _, arg := range p.Args { 741 | tmpArg, err := cfg.Template.Render(arg, extraVars) 742 | if err != nil { 743 | return err 744 | } 745 | tmpArgs = append(tmpArgs, tmpArg) 746 | } 747 | //nolint:prealloc 748 | var tmpBinds []string 749 | for _, bind := range p.Binds { 750 | tmpBind, err := cfg.Template.Render(bind, extraVars) 751 | if err != nil { 752 | return err 753 | } 754 | tmpBinds = append(tmpBinds, tmpBind) 755 | // Precreate any host paths for container bind mounts. This is necessary to retain non-root ownership 756 | bindParts := strings.SplitN(tmpBind, ":", 2) 757 | if bindParts != nil { 758 | hostPath := bindParts[0] 759 | if err := os.MkdirAll(hostPath, fs.ModePerm); err != nil { 760 | return err 761 | } 762 | cfg.Logger.Debug( 763 | fmt.Sprintf( 764 | "precreating host path for container bind mount: %q", 765 | hostPath, 766 | ), 767 | ) 768 | } 769 | } 770 | //nolint:prealloc 771 | var tmpPorts []string 772 | for _, port := range p.Ports { 773 | tmpPort, err := cfg.Template.Render(port, extraVars) 774 | if err != nil { 775 | return err 776 | } 777 | tmpPorts = append(tmpPorts, tmpPort) 778 | } 779 | svc := DockerService{ 780 | logger: cfg.Logger, 781 | ContainerName: containerName, 782 | Image: tmpImage, 783 | Env: tmpEnv, 784 | Command: tmpCommand, 785 | Args: tmpArgs, 786 | Binds: tmpBinds, 787 | Ports: tmpPorts, 788 | } 789 | if p.PullOnly { 790 | if err := svc.pullImage(); err != nil { 791 | return err 792 | } 793 | } else { 794 | if err := svc.Create(); err != nil { 795 | return err 796 | } 797 | if err := svc.Start(); err != nil { 798 | return err 799 | } 800 | } 801 | return nil 802 | } 803 | 804 | func (p *PackageInstallStepDocker) uninstall( 805 | cfg Config, 806 | pkgName string, 807 | keepData bool, 808 | ) error { 809 | if !p.PullOnly { 810 | containerName := fmt.Sprintf("%s-%s", pkgName, p.ContainerName) 811 | svc, err := NewDockerServiceFromContainerName(containerName, cfg.Logger) 812 | if err != nil { 813 | if errors.Is(err, ErrContainerNotExists) { 814 | cfg.Logger.Debug( 815 | "container missing on uninstall: " + containerName, 816 | ) 817 | } else { 818 | return err 819 | } 820 | } else { 821 | if running, _ := svc.Running(); running { 822 | if err := svc.Stop(); err != nil { 823 | return err 824 | } 825 | } 826 | if err := svc.Remove(); err != nil { 827 | return err 828 | } 829 | } 830 | } 831 | if keepData { 832 | cfg.Logger.Debug( 833 | fmt.Sprintf( 834 | "skipping deletion of docker image %q", 835 | p.Image, 836 | ), 837 | ) 838 | } else { 839 | if err := RemoveDockerImage(p.Image); err != nil { 840 | cfg.Logger.Debug( 841 | fmt.Sprintf( 842 | "failed to delete image %q: %s", 843 | p.Image, 844 | err, 845 | ), 846 | ) 847 | } else { 848 | cfg.Logger.Debug( 849 | fmt.Sprintf( 850 | "removed unused image %q", 851 | p.Image, 852 | ), 853 | ) 854 | } 855 | } 856 | return nil 857 | } 858 | 859 | func (p *PackageInstallStepDocker) activate(cfg Config, pkgName string) error { 860 | // Nothing to do 861 | return nil 862 | } 863 | 864 | func (p *PackageInstallStepDocker) deactivate( 865 | cfg Config, 866 | pkgName string, 867 | ) error { 868 | // Nothing to do 869 | return nil 870 | } 871 | 872 | type PackageInstallStepFile struct { 873 | Binary bool `yaml:"binary"` 874 | Filename string `yaml:"filename"` 875 | Source string `yaml:"source"` 876 | Content string `yaml:"content"` 877 | Url string `yaml:"url"` 878 | Mode fs.FileMode `yaml:"mode,omitempty"` 879 | } 880 | 881 | func (p *PackageInstallStepFile) validate(cfg Config) error { 882 | // TODO: add checks 883 | return nil 884 | } 885 | 886 | func (p *PackageInstallStepFile) install( 887 | cfg Config, 888 | pkgName string, 889 | packagePath string, 890 | ) error { 891 | tmpFilePath, err := cfg.Template.Render(p.Filename, nil) 892 | if err != nil { 893 | return err 894 | } 895 | filePath := filepath.Join( 896 | cfg.DataDir, 897 | pkgName, 898 | tmpFilePath, 899 | ) 900 | parentDir := filepath.Dir(filePath) 901 | if err := os.MkdirAll(parentDir, fs.ModePerm); err != nil { 902 | return err 903 | } 904 | fileMode := fs.ModePerm 905 | if p.Mode > 0 { 906 | fileMode = p.Mode 907 | } 908 | var fileContent []byte 909 | if p.Content != "" { 910 | tmpContent, err := cfg.Template.Render(p.Content, nil) 911 | if err != nil { 912 | return err 913 | } 914 | fileContent = []byte(tmpContent) 915 | } else if p.Source != "" { 916 | fullSourcePath := filepath.Join( 917 | filepath.Dir(packagePath), 918 | p.Source, 919 | ) 920 | tmpContentBytes, err := os.ReadFile(fullSourcePath) 921 | if err != nil { 922 | return err 923 | } 924 | tmpContent, err := cfg.Template.Render(string(tmpContentBytes), nil) 925 | if err != nil { 926 | return err 927 | } 928 | fileContent = []byte(tmpContent) 929 | } else if p.Url != "" { 930 | // Validate URL 931 | u, err := url.Parse(p.Url) 932 | if err != nil { 933 | return err 934 | } 935 | if u.Scheme == "" || u.Host == "" { 936 | return errors.New("invalid URL given") 937 | } 938 | 939 | // Fetch data 940 | ctx := context.Background() 941 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.Url, nil) 942 | if err != nil { 943 | return err 944 | } 945 | resp, err := http.DefaultClient.Do(req) 946 | if err != nil { 947 | return err 948 | } 949 | if resp == nil { 950 | return fmt.Errorf("nil response for URL: %s", p.Url) 951 | } 952 | respBody, err := io.ReadAll(resp.Body) 953 | if err != nil { 954 | return err 955 | } 956 | resp.Body.Close() 957 | 958 | fileContent = respBody 959 | } else { 960 | return errors.New("packages must provide content, source, or url for file install types") 961 | } 962 | if err := os.WriteFile(filePath, fileContent, fileMode); err != nil { 963 | return err 964 | } 965 | cfg.Logger.Debug("wrote file " + filePath) 966 | return nil 967 | } 968 | 969 | func (p *PackageInstallStepFile) uninstall(cfg Config, pkgName string) error { 970 | filePath := filepath.Join( 971 | cfg.DataDir, 972 | pkgName, 973 | p.Filename, 974 | ) 975 | cfg.Logger.Debug("deleting file " + filePath) 976 | if err := os.Remove(filePath); err != nil { 977 | if !errors.Is(err, fs.ErrNotExist) { 978 | cfg.Logger.Warn("failed to remove file " + filePath) 979 | } 980 | } 981 | return nil 982 | } 983 | 984 | func (p *PackageInstallStepFile) activate(cfg Config, pkgName string) error { 985 | if p.Binary { 986 | tmpFilePath, err := cfg.Template.Render(p.Filename, nil) 987 | if err != nil { 988 | return err 989 | } 990 | filePath := filepath.Join( 991 | cfg.DataDir, 992 | pkgName, 993 | p.Filename, 994 | ) 995 | binPath := filepath.Join( 996 | cfg.BinDir, 997 | tmpFilePath, 998 | ) 999 | parentDir := filepath.Dir(binPath) 1000 | if err := os.MkdirAll(parentDir, fs.ModePerm); err != nil { 1001 | return err 1002 | } 1003 | // Check for existing file at symlink location 1004 | if stat, err := os.Lstat(binPath); err != nil { 1005 | if !errors.Is(err, fs.ErrNotExist) { 1006 | return err 1007 | } 1008 | } else { 1009 | if (stat.Mode() & fs.ModeSymlink) > 0 { 1010 | // Remove existing symlink 1011 | if err := os.Remove(binPath); err != nil { 1012 | if !errors.Is(err, fs.ErrNotExist) { 1013 | return err 1014 | } 1015 | } 1016 | cfg.Logger.Debug( 1017 | fmt.Sprintf("removed existing symlink %q", binPath), 1018 | ) 1019 | } else { 1020 | return fmt.Errorf("will not overwrite existing file %q with symlink", binPath) 1021 | } 1022 | } 1023 | if err := os.Symlink(filePath, binPath); err != nil { 1024 | return err 1025 | } 1026 | cfg.Logger.Debug( 1027 | fmt.Sprintf("wrote symlink from %s to %s", binPath, filePath), 1028 | ) 1029 | } 1030 | return nil 1031 | } 1032 | 1033 | func (p *PackageInstallStepFile) deactivate(cfg Config, pkgName string) error { 1034 | if p.Binary { 1035 | tmpFilePath, err := cfg.Template.Render(p.Filename, nil) 1036 | if err != nil { 1037 | return err 1038 | } 1039 | binPath := filepath.Join( 1040 | cfg.BinDir, 1041 | tmpFilePath, 1042 | ) 1043 | parentDir := filepath.Dir(binPath) 1044 | if err := os.MkdirAll(parentDir, fs.ModePerm); err != nil { 1045 | return err 1046 | } 1047 | if err := os.Remove(binPath); err != nil { 1048 | if !errors.Is(err, fs.ErrNotExist) { 1049 | return err 1050 | } 1051 | } 1052 | cfg.Logger.Debug("removed symlink " + binPath + "for " + pkgName) 1053 | } 1054 | return nil 1055 | } 1056 | -------------------------------------------------------------------------------- /pkgmgr/package_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "os" 19 | "path/filepath" 20 | "reflect" 21 | "runtime" 22 | "strings" 23 | "testing" 24 | "text/template" 25 | 26 | "gopkg.in/yaml.v3" 27 | ) 28 | 29 | var packageTestDefs = []struct { 30 | yaml string 31 | packageObj Package 32 | }{ 33 | { 34 | yaml: "name: foo\nversion: 1.2.3", 35 | packageObj: Package{ 36 | Name: "foo", 37 | Version: "1.2.3", 38 | }, 39 | }, 40 | } 41 | 42 | func TestNewPackageFromReader(t *testing.T) { 43 | for _, testDef := range packageTestDefs { 44 | r := strings.NewReader(testDef.yaml) 45 | p, err := NewPackageFromReader(r) 46 | if err != nil { 47 | t.Fatalf("unexpected error: %s", err) 48 | } 49 | if !reflect.DeepEqual(p, testDef.packageObj) { 50 | t.Fatalf( 51 | "did not get expected package object\n got: %#v\n expected: %#v", 52 | p, 53 | testDef.packageObj, 54 | ) 55 | } 56 | } 57 | } 58 | 59 | func TestPackageToYaml(t *testing.T) { 60 | for _, testDef := range packageTestDefs { 61 | data, err := yaml.Marshal(&testDef.packageObj) 62 | if err != nil { 63 | t.Fatalf("unexpected error: %s", err) 64 | } 65 | trimmedData := strings.TrimRight(string(data), "\r\n") 66 | if trimmedData != testDef.yaml { 67 | t.Fatalf( 68 | "did not get expected package YAML\n got: %s\n expected: %s", 69 | trimmedData, 70 | testDef.yaml, 71 | ) 72 | } 73 | } 74 | } 75 | 76 | func TestOSAndARCH(t *testing.T) { 77 | expectOS := runtime.GOOS 78 | expectARCH := runtime.GOARCH 79 | 80 | // Initialized a config object 81 | tempCacheDir := t.TempDir() 82 | tempDataDir := t.TempDir() 83 | cfg := Config{ 84 | CacheDir: tempCacheDir, 85 | DataDir: tempDataDir, 86 | BinDir: "/usr/local/bin", 87 | Template: &Template{ 88 | tmpl: template.New("test"), 89 | baseVars: make(map[string]any), 90 | }, 91 | } 92 | 93 | cfg.Template = cfg.Template.WithVars( 94 | map[string]any{ 95 | "System": map[string]string{ 96 | "OS": runtime.GOOS, 97 | "ARCH": runtime.GOARCH, 98 | }, 99 | }, 100 | ) 101 | 102 | // Defined a test package 103 | pkg := Package{} 104 | pkg.Name = "test-package" 105 | pkg.Version = "1.0.0" 106 | opts := make(map[string]bool) 107 | 108 | _, _, err := pkg.install(cfg, "test", opts, false) 109 | if err != nil { 110 | t.Errorf("Installation failed: %v", err) 111 | } 112 | 113 | // Verify if OS and ARCH are injected into the config template 114 | actualOS, err := cfg.Template.Render("{{ .System.OS }}", nil) 115 | if err != nil { 116 | t.Errorf("Template rendering for OS failed: %v", err) 117 | } else if actualOS != expectOS { 118 | t.Errorf("Expected OS: %s and rendered OS: %s are not same", expectOS, actualOS) 119 | } else { 120 | t.Logf("Expected OS matched with rendered OS") 121 | } 122 | 123 | actualARCH, err := cfg.Template.Render("{{ .System.ARCH }}", nil) 124 | if err != nil { 125 | t.Errorf("Template rendering for ARCH failed: %v", err) 126 | } else if actualARCH != expectARCH { 127 | t.Errorf("Expected ARCH: %s and rendered ARCH: %s are not same", expectARCH, actualARCH) 128 | } else { 129 | t.Logf("Expected ARCH matched with rendered ARCH") 130 | } 131 | 132 | if actualOS == expectOS && actualARCH == expectARCH { 133 | t.Logf( 134 | "Test is successful and OS, ARCH values are correctly injected to config template", 135 | ) 136 | } 137 | } 138 | 139 | func TestServiceHooks_PreStartAndPreStop(t *testing.T) { 140 | tmpDir := t.TempDir() 141 | 142 | // Create preStart script 143 | preStartLog := filepath.Join(tmpDir, "prestart.log") 144 | preStartScript := filepath.Join(tmpDir, "prestart.sh") 145 | preStartContent := "#!/bin/sh\necho 'prestart executed' > " + preStartLog 146 | if err := os.WriteFile(preStartScript, []byte(preStartContent), 0755); err != nil { 147 | t.Fatalf("failed to write preStart script: %v", err) 148 | } 149 | 150 | // Create preStop script 151 | preStopLog := filepath.Join(tmpDir, "prestop.log") 152 | preStopScript := filepath.Join(tmpDir, "prestop.sh") 153 | preStopContent := "#!/bin/sh\necho 'prestop executed' > " + preStopLog 154 | if err := os.WriteFile(preStopScript, []byte(preStopContent), 0755); err != nil { 155 | t.Fatalf("failed to write preStop script: %v", err) 156 | } 157 | 158 | // Initialize a config object 159 | cfg := Config{ 160 | CacheDir: tmpDir, 161 | DataDir: tmpDir, 162 | BinDir: tmpDir, 163 | Template: &Template{ 164 | tmpl: template.New("test"), 165 | baseVars: make(map[string]any), 166 | }, 167 | } 168 | // Define a test package 169 | pkg := Package{ 170 | Name: "mypkg", 171 | Version: "1.0.0", 172 | PreStartScript: preStartScript, 173 | PreStopScript: preStopScript, 174 | InstallSteps: []PackageInstallStep{}, 175 | } 176 | 177 | // Execute startService and expect preStartScript to run 178 | if err := pkg.startService(cfg, "testctx"); err != nil { 179 | t.Fatalf("startService failed: %v", err) 180 | } 181 | 182 | // Validate preStart script output 183 | preStartOutput, err := os.ReadFile(preStartLog) 184 | if err != nil { 185 | t.Fatalf("preStart log file not found: %v", err) 186 | } 187 | if string(preStartOutput) != "prestart executed\n" { 188 | t.Errorf( 189 | "unexpected preStart output: got %q, want %q", 190 | string(preStartOutput), 191 | "prestart executed\n", 192 | ) 193 | } 194 | 195 | // Execute stopService and expect preStopScript to run 196 | if err := pkg.stopService(cfg, "testctx"); err != nil { 197 | t.Fatalf("stopService failed: %v", err) 198 | } 199 | 200 | // Validate preStop script output 201 | preStopOutput, err := os.ReadFile(preStopLog) 202 | if err != nil { 203 | t.Fatalf("preStop log file not found: %v", err) 204 | } 205 | if string(preStopOutput) != "prestop executed\n" { 206 | t.Errorf( 207 | "unexpected preStop output: got %q, want %q", 208 | string(preStopOutput), 209 | "prestop executed\n", 210 | ) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /pkgmgr/pkgmgr.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "io" 21 | "maps" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | 26 | ouroboros "github.com/blinklabs-io/gouroboros" 27 | ) 28 | 29 | type PackageManager struct { 30 | config Config 31 | state *State 32 | availablePackages []Package 33 | } 34 | 35 | func NewPackageManager(cfg Config) (*PackageManager, error) { 36 | // Make sure that a logger was provided, since we use it for pretty much all feedback 37 | if cfg.Logger == nil { 38 | return nil, errors.New("you must provide a logger") 39 | } 40 | p := &PackageManager{ 41 | config: cfg, 42 | state: NewState(cfg), 43 | } 44 | if err := p.init(); err != nil { 45 | return nil, err 46 | } 47 | return p, nil 48 | } 49 | 50 | func NewDefaultPackageManager() (*PackageManager, error) { 51 | pmCfg, err := NewDefaultConfig() 52 | if err != nil { 53 | return nil, err 54 | } 55 | return NewPackageManager(pmCfg) 56 | } 57 | 58 | func (p *PackageManager) init() error { 59 | if err := p.state.Load(); err != nil { 60 | return fmt.Errorf("failed to load state: %w", err) 61 | } 62 | // Setup templating 63 | p.initTemplate() 64 | return nil 65 | } 66 | 67 | func (p *PackageManager) initTemplate() { 68 | activeContextName, activeContext := p.ActiveContext() 69 | tmplVars := map[string]any{ 70 | "Context": map[string]any{ 71 | "Name": activeContextName, 72 | "Network": activeContext.Network, 73 | "NetworkMagic": activeContext.NetworkMagic, 74 | }, 75 | "Env": p.ContextEnv(), 76 | } 77 | tmpConfig := p.config 78 | if tmpConfig.Template == nil { 79 | tmpConfig.Template = NewTemplate(tmplVars) 80 | } else { 81 | tmpConfig.Template = tmpConfig.Template.WithVars(tmplVars) 82 | } 83 | p.config = tmpConfig 84 | } 85 | 86 | func (p *PackageManager) loadPackageRegistry(validate bool) error { 87 | var retErr error 88 | registryPkgs, err := registryPackages(p.config, validate) 89 | if err != nil { 90 | if errors.Is(err, ErrValidationFailed) { 91 | // We want to pass along the validation error, but only after we record the packages 92 | retErr = err 93 | } else { 94 | return err 95 | } 96 | } 97 | p.availablePackages = registryPkgs[:] 98 | return retErr 99 | } 100 | 101 | func (p *PackageManager) AvailablePackages() []Package { 102 | var ret []Package 103 | if p.availablePackages == nil { 104 | if err := p.loadPackageRegistry(false); err != nil { 105 | p.config.Logger.Warn( 106 | fmt.Sprintf("failed to load packages: %s", err), 107 | ) 108 | } 109 | } 110 | for _, pkg := range p.availablePackages { 111 | if pkg.hasTags(p.config.RequiredPackageTags) { 112 | ret = append(ret, pkg) 113 | } 114 | } 115 | return ret 116 | } 117 | 118 | func (p *PackageManager) Up() error { 119 | // Find installed packages 120 | installedPackages := p.InstalledPackages() 121 | for _, tmpPackage := range installedPackages { 122 | err := tmpPackage.Package.startService(p.config, tmpPackage.Context) 123 | if err != nil { 124 | return err 125 | } 126 | } 127 | return nil 128 | } 129 | 130 | func (p *PackageManager) Down() error { 131 | // Find installed packages 132 | installedPackages := p.InstalledPackages() 133 | for _, tmpPackage := range installedPackages { 134 | err := tmpPackage.Package.stopService(p.config, tmpPackage.Context) 135 | if err != nil { 136 | return err 137 | } 138 | } 139 | return nil 140 | } 141 | 142 | func (p *PackageManager) InstalledPackages() []InstalledPackage { 143 | var ret []InstalledPackage 144 | for _, pkg := range p.state.InstalledPackages { 145 | if pkg.Context == p.state.ActiveContext { 146 | ret = append(ret, pkg) 147 | } 148 | } 149 | return ret 150 | } 151 | 152 | func (p *PackageManager) InstalledPackagesAllContexts() []InstalledPackage { 153 | return p.state.InstalledPackages 154 | } 155 | 156 | func (p *PackageManager) Install(pkgs ...string) error { 157 | // Check context for network 158 | activeContextName, activeContext := p.ActiveContext() 159 | if activeContext.Network == "" { 160 | return ErrContextInstallNoNetwork 161 | } 162 | resolver, err := NewResolver( 163 | p.InstalledPackages(), 164 | p.AvailablePackages(), 165 | activeContextName, 166 | p.config.Logger, 167 | ) 168 | if err != nil { 169 | return err 170 | } 171 | installPkgs, err := resolver.Install(pkgs...) 172 | if err != nil { 173 | return err 174 | } 175 | installedPkgs := []string{} 176 | var notesOutput string 177 | for _, installPkg := range installPkgs { 178 | p.config.Logger.Info( 179 | fmt.Sprintf( 180 | "Installing package %s (= %s)", 181 | installPkg.Install.Name, 182 | installPkg.Install.Version, 183 | ), 184 | ) 185 | // Build package options 186 | tmpPkgOpts := installPkg.Install.defaultOpts() 187 | maps.Copy(tmpPkgOpts, installPkg.Options) 188 | // Install package 189 | notes, outputs, err := installPkg.Install.install( 190 | p.config, 191 | activeContextName, 192 | tmpPkgOpts, 193 | true, 194 | ) 195 | if err != nil { 196 | return err 197 | } 198 | installedPkg := NewInstalledPackage( 199 | installPkg.Install, 200 | activeContextName, 201 | notes, 202 | outputs, 203 | tmpPkgOpts, 204 | ) 205 | p.state.InstalledPackages = append( 206 | p.state.InstalledPackages, 207 | installedPkg, 208 | ) 209 | if err := p.state.Save(); err != nil { 210 | return err 211 | } 212 | installedPkgs = append(installedPkgs, installPkg.Install.Name) 213 | if notes != "" { 214 | notesOutput += fmt.Sprintf( 215 | "\nPost-install notes for %s (= %s):\n\n%s\n", 216 | installPkg.Install.Name, 217 | installPkg.Install.Version, 218 | notes, 219 | ) 220 | } 221 | // Activate package 222 | if err := installPkg.Install.activate(p.config, activeContextName); err != nil { 223 | p.config.Logger.Warn( 224 | fmt.Sprintf("failed to activate package: %s", err), 225 | ) 226 | } 227 | } 228 | // Display post-install notes 229 | if notesOutput != "" { 230 | p.config.Logger.Info(notesOutput) 231 | } 232 | p.config.Logger.Info( 233 | fmt.Sprintf( 234 | "Successfully installed package(s) in context %q: %s", 235 | activeContextName, 236 | strings.Join(installedPkgs, ", "), 237 | ), 238 | ) 239 | return nil 240 | } 241 | 242 | func (p *PackageManager) Upgrade(pkgs ...string) error { 243 | activeContextName, _ := p.ActiveContext() 244 | resolver, err := NewResolver( 245 | p.InstalledPackages(), 246 | p.AvailablePackages(), 247 | activeContextName, 248 | p.config.Logger, 249 | ) 250 | if err != nil { 251 | return err 252 | } 253 | upgradePkgs, err := resolver.Upgrade(pkgs...) 254 | if err != nil { 255 | return err 256 | } 257 | installedPkgs := []string{} 258 | var notesOutput string 259 | for _, upgradePkg := range upgradePkgs { 260 | p.config.Logger.Info( 261 | fmt.Sprintf( 262 | "Upgrading package %s (%s => %s)", 263 | upgradePkg.Installed.Package.Name, 264 | upgradePkg.Installed.Package.Version, 265 | upgradePkg.Upgrade.Version, 266 | ), 267 | ) 268 | // Capture options from existing package 269 | pkgOpts := upgradePkg.Installed.Options 270 | // Deactivate old package 271 | if err := upgradePkg.Installed.Package.deactivate(p.config, activeContextName); err != nil { 272 | p.config.Logger.Warn( 273 | fmt.Sprintf("failed to deactivate package: %s", err), 274 | ) 275 | } 276 | // Uninstall old version 277 | if err := p.uninstallPackage(upgradePkg.Installed, true, false); err != nil { 278 | return err 279 | } 280 | // Install new version 281 | notes, outputs, err := upgradePkg.Upgrade.install( 282 | p.config, 283 | activeContextName, 284 | pkgOpts, 285 | false, 286 | ) 287 | if err != nil { 288 | return err 289 | } 290 | installedPkg := NewInstalledPackage( 291 | upgradePkg.Upgrade, 292 | activeContextName, 293 | notes, 294 | outputs, 295 | pkgOpts, 296 | ) 297 | p.state.InstalledPackages = append( 298 | p.state.InstalledPackages, 299 | installedPkg, 300 | ) 301 | if err := p.state.Save(); err != nil { 302 | return err 303 | } 304 | installedPkgs = append(installedPkgs, upgradePkg.Upgrade.Name) 305 | if notes != "" { 306 | notesOutput += fmt.Sprintf( 307 | "\nPost-install notes for %s (= %s):\n\n%s\n", 308 | upgradePkg.Upgrade.Name, 309 | upgradePkg.Upgrade.Version, 310 | notes, 311 | ) 312 | } 313 | if err := p.state.Save(); err != nil { 314 | return err 315 | } 316 | // Activate new package 317 | if err := upgradePkg.Upgrade.activate(p.config, activeContextName); err != nil { 318 | p.config.Logger.Warn( 319 | fmt.Sprintf("failed to activate package: %s", err), 320 | ) 321 | } 322 | } 323 | // Display post-install notes 324 | if notesOutput != "" { 325 | p.config.Logger.Info(notesOutput) 326 | } 327 | p.config.Logger.Info( 328 | fmt.Sprintf( 329 | "Successfully upgraded/installed package(s) in context %q: %s", 330 | activeContextName, 331 | strings.Join(installedPkgs, ", "), 332 | ), 333 | ) 334 | return nil 335 | } 336 | 337 | func (p *PackageManager) Uninstall( 338 | pkgName string, 339 | keepData bool, 340 | force bool, 341 | ) error { 342 | // Find installed packages 343 | activeContextName, _ := p.ActiveContext() 344 | installedPackages := p.InstalledPackages() 345 | var uninstallPkgs []InstalledPackage 346 | foundPackage := false 347 | for _, tmpPackage := range installedPackages { 348 | if tmpPackage.Package.Name == pkgName { 349 | foundPackage = true 350 | uninstallPkgs = append( 351 | uninstallPkgs, 352 | tmpPackage, 353 | ) 354 | break 355 | } 356 | } 357 | if !foundPackage { 358 | return NewPackageNotInstalledError(pkgName, activeContextName) 359 | } 360 | if !force { 361 | // Resolve dependencies 362 | resolver, err := NewResolver( 363 | p.InstalledPackages(), 364 | p.AvailablePackages(), 365 | activeContextName, 366 | p.config.Logger, 367 | ) 368 | if err != nil { 369 | return err 370 | } 371 | if err := resolver.Uninstall(uninstallPkgs...); err != nil { 372 | return err 373 | } 374 | } 375 | for _, uninstallPkg := range uninstallPkgs { 376 | // Deactivate package 377 | if err := uninstallPkg.Package.deactivate(p.config, activeContextName); err != nil { 378 | p.config.Logger.Warn( 379 | fmt.Sprintf("failed to deactivate package: %s", err), 380 | ) 381 | } 382 | if err := p.uninstallPackage(uninstallPkg, keepData, true); err != nil { 383 | return err 384 | } 385 | if err := p.state.Save(); err != nil { 386 | return err 387 | } 388 | p.config.Logger.Info( 389 | fmt.Sprintf( 390 | "Successfully uninstalled package %s (= %s) from context %q", 391 | uninstallPkg.Package.Name, 392 | uninstallPkg.Package.Version, 393 | activeContextName, 394 | ), 395 | ) 396 | } 397 | return nil 398 | } 399 | 400 | func (p *PackageManager) Logs( 401 | pkgName string, 402 | follow bool, 403 | tail string, 404 | stdoutWriter io.Writer, 405 | stderrWriter io.Writer, 406 | ) error { 407 | // Find installed packages 408 | activeContextName, _ := p.ActiveContext() 409 | installedPackages := p.InstalledPackages() 410 | var logsPkg InstalledPackage 411 | foundPackage := false 412 | for _, tmpPackage := range installedPackages { 413 | if tmpPackage.Package.Name == pkgName { 414 | foundPackage = true 415 | logsPkg = tmpPackage 416 | break 417 | } 418 | } 419 | if !foundPackage { 420 | return NewPackageNotInstalledError(pkgName, activeContextName) 421 | } 422 | services, err := logsPkg.Package.services(p.config, activeContextName) 423 | if err != nil { 424 | return err 425 | } 426 | if len(services) == 0 { 427 | return NewNoServicesFoundError(pkgName) 428 | } 429 | // TODO: account for more than one service in a package 430 | tmpSvc := services[0] 431 | if err := tmpSvc.Logs(follow, tail, stdoutWriter, stderrWriter); err != nil { 432 | return err 433 | } 434 | return nil 435 | } 436 | 437 | func (p *PackageManager) Info(pkgs ...string) error { 438 | // Find installed packages 439 | activeContextName, _ := p.ActiveContext() 440 | installedPackages := p.InstalledPackages() 441 | var infoPkgs []InstalledPackage 442 | for _, pkg := range pkgs { 443 | foundPackage := false 444 | for _, tmpPackage := range installedPackages { 445 | if tmpPackage.Package.Name == pkg { 446 | foundPackage = true 447 | infoPkgs = append( 448 | infoPkgs, 449 | tmpPackage, 450 | ) 451 | break 452 | } 453 | } 454 | if !foundPackage { 455 | return NewPackageNotInstalledError(pkg, activeContextName) 456 | } 457 | } 458 | var infoOutput string 459 | for idx, infoPkg := range infoPkgs { 460 | infoOutput += fmt.Sprintf( 461 | "Name: %s\nVersion: %s\nContext: %s", 462 | infoPkg.Package.Name, 463 | infoPkg.Package.Version, 464 | activeContextName, 465 | ) 466 | if infoPkg.PostInstallNotes != "" { 467 | infoOutput += "\n\nPost-install notes:\n\n" + infoPkg.PostInstallNotes 468 | } 469 | // Gather package services 470 | services, err := infoPkg.Package.services(p.config, infoPkg.Context) 471 | if err != nil { 472 | return err 473 | } 474 | // Build service status and port output 475 | var statusOutput string 476 | var portOutput string 477 | for _, svc := range services { 478 | running, err := svc.Running() 479 | if err != nil { 480 | return err 481 | } 482 | if running { 483 | statusOutput += fmt.Sprintf( 484 | "%-60s RUNNING\n", 485 | svc.ContainerName, 486 | ) 487 | } else { 488 | statusOutput += fmt.Sprintf( 489 | "%-60s NOT RUNNING\n", 490 | svc.ContainerName, 491 | ) 492 | } 493 | for _, port := range svc.Ports { 494 | var containerPort, hostPort string 495 | portParts := strings.Split(port, ":") 496 | switch len(portParts) { 497 | case 1: 498 | containerPort = portParts[0] 499 | hostPort = portParts[0] 500 | case 2: 501 | containerPort = portParts[1] 502 | hostPort = portParts[0] 503 | case 3: 504 | containerPort = portParts[2] 505 | hostPort = portParts[1] 506 | } 507 | portOutput += fmt.Sprintf( 508 | "%-5s (host) => %-5s (container)\n", 509 | hostPort, 510 | containerPort, 511 | ) 512 | } 513 | } 514 | if statusOutput != "" { 515 | infoOutput += "\n\nServices:\n\n" + strings.TrimSuffix( 516 | statusOutput, 517 | "\n", 518 | ) 519 | } 520 | if portOutput != "" { 521 | infoOutput += "\n\nMapped ports:\n\n" + strings.TrimSuffix( 522 | portOutput, 523 | "\n", 524 | ) 525 | } 526 | if idx < len(infoPkgs)-1 { 527 | infoOutput += "\n\n---\n\n" 528 | } 529 | } 530 | p.config.Logger.Info(infoOutput) 531 | return nil 532 | } 533 | 534 | func (p *PackageManager) uninstallPackage( 535 | uninstallPkg InstalledPackage, 536 | keepData bool, 537 | runHooks bool, 538 | ) error { 539 | // Uninstall package 540 | if err := uninstallPkg.Package.uninstall(p.config, uninstallPkg.Context, keepData, runHooks); err != nil { 541 | return err 542 | } 543 | // Remove package from installed packages 544 | tmpInstalledPackages := []InstalledPackage{} 545 | for _, tmpInstalledPkg := range p.state.InstalledPackages { 546 | if tmpInstalledPkg.Context == uninstallPkg.Context && 547 | tmpInstalledPkg.Package.Name == uninstallPkg.Package.Name && 548 | tmpInstalledPkg.Package.Version == uninstallPkg.Package.Version { 549 | continue 550 | } 551 | tmpInstalledPackages = append(tmpInstalledPackages, tmpInstalledPkg) 552 | } 553 | p.state.InstalledPackages = tmpInstalledPackages[:] 554 | return nil 555 | } 556 | 557 | func (p *PackageManager) Contexts() map[string]Context { 558 | return p.state.Contexts 559 | } 560 | 561 | func (p *PackageManager) ActiveContext() (string, Context) { 562 | return p.state.ActiveContext, p.state.Contexts[p.state.ActiveContext] 563 | } 564 | 565 | func (p *PackageManager) AddContext(name string, context Context) error { 566 | if _, ok := p.state.Contexts[name]; ok { 567 | return ErrContextAlreadyExists 568 | } 569 | // Create dummy context entry 570 | p.state.Contexts[name] = Context{} 571 | // Update dummy context 572 | if err := p.updateContext(name, context); err != nil { 573 | return err 574 | } 575 | return nil 576 | } 577 | 578 | func (p *PackageManager) DeleteContext(name string) error { 579 | if name == p.state.ActiveContext { 580 | return ErrContextNoDeleteActive 581 | } 582 | if _, ok := p.state.Contexts[name]; !ok { 583 | return ErrContextNotExist 584 | } 585 | delete(p.state.Contexts, name) 586 | if err := p.state.Save(); err != nil { 587 | return err 588 | } 589 | return nil 590 | } 591 | 592 | func (p *PackageManager) SetActiveContext(name string) error { 593 | if _, ok := p.state.Contexts[name]; !ok { 594 | return ErrContextNotExist 595 | } 596 | // Deactivate packages in current context 597 | activeContextName, _ := p.ActiveContext() 598 | for _, pkg := range p.InstalledPackages() { 599 | if err := pkg.Package.deactivate(p.config, activeContextName); err != nil { 600 | p.config.Logger.Warn( 601 | fmt.Sprintf("failed to deactivate package: %s", err), 602 | ) 603 | } 604 | } 605 | p.state.ActiveContext = name 606 | if err := p.state.Save(); err != nil { 607 | return err 608 | } 609 | // Update templating values 610 | p.initTemplate() 611 | // Activate packages in new context 612 | for _, pkg := range p.InstalledPackages() { 613 | if err := pkg.Package.activate(p.config, name); err != nil { 614 | p.config.Logger.Warn( 615 | fmt.Sprintf("failed to activate package: %s", err), 616 | ) 617 | } 618 | } 619 | return nil 620 | } 621 | 622 | func (p *PackageManager) UpdateContext(name string, context Context) error { 623 | if err := p.updateContext(name, context); err != nil { 624 | return err 625 | } 626 | return nil 627 | } 628 | 629 | func (p *PackageManager) updateContext(name string, newContext Context) error { 630 | // Get current state of named context 631 | curContext, ok := p.state.Contexts[name] 632 | if !ok { 633 | return ErrContextNotExist 634 | } 635 | if curContext.Network != "" { 636 | // Check that we're not changing the network once configured 637 | if newContext.Network != curContext.Network { 638 | return ErrContextNoChangeNetwork 639 | } 640 | } else { 641 | // Check network name if setting it for new/empty context 642 | if newContext.Network != "" { 643 | tmpNetwork, ok := ouroboros.NetworkByName(newContext.Network) 644 | if !ok { 645 | return NewUnknownNetworkError(newContext.Network) 646 | } 647 | newContext.NetworkMagic = tmpNetwork.NetworkMagic 648 | } 649 | } 650 | p.state.Contexts[name] = newContext 651 | if err := p.state.Save(); err != nil { 652 | return err 653 | } 654 | // Update templating values 655 | p.initTemplate() 656 | return nil 657 | } 658 | 659 | func (p *PackageManager) ContextEnv() map[string]string { 660 | ret := make(map[string]string) 661 | for _, pkg := range p.InstalledPackages() { 662 | maps.Copy(ret, pkg.Outputs) 663 | } 664 | return ret 665 | } 666 | 667 | func (p *PackageManager) UpdatePackages() error { 668 | // Clear out existing cache files 669 | cachePath := filepath.Join( 670 | p.config.CacheDir, 671 | "registry", 672 | ) 673 | if err := os.RemoveAll(cachePath); err != nil { 674 | return err 675 | } 676 | // (Re)load the package registry 677 | if err := p.loadPackageRegistry(false); err != nil { 678 | return err 679 | } 680 | return nil 681 | } 682 | 683 | func (p *PackageManager) ValidatePackages() error { 684 | foundError := false 685 | if len(p.availablePackages) == 0 { 686 | if err := p.loadPackageRegistry(true); err != nil { 687 | if errors.Is(err, ErrValidationFailed) { 688 | // Record error for later failure 689 | // The error(s) will have already been output to the console 690 | foundError = true 691 | } else { 692 | return err 693 | } 694 | } 695 | } 696 | for _, pkg := range p.availablePackages { 697 | if pkg.filePath == "" { 698 | continue 699 | } 700 | p.config.Logger.Debug( 701 | "checking package " + pkg.filePath, 702 | ) 703 | if err := pkg.validate(p.config); err != nil { 704 | foundError = true 705 | p.config.Logger.Warn( 706 | fmt.Sprintf( 707 | "validation failed: %s: %s", 708 | pkg.filePath, 709 | err.Error(), 710 | ), 711 | ) 712 | } 713 | } 714 | if foundError { 715 | return ErrOperationFailed 716 | } 717 | return nil 718 | } 719 | -------------------------------------------------------------------------------- /pkgmgr/registry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "archive/zip" 19 | "bytes" 20 | "context" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "io/fs" 25 | "net/http" 26 | "os" 27 | "path/filepath" 28 | "strings" 29 | "time" 30 | ) 31 | 32 | func registryPackages(cfg Config, validate bool) ([]Package, error) { 33 | if cfg.RegistryDir != "" { 34 | return registryPackagesDir(cfg, validate) 35 | } else if cfg.RegistryUrl != "" { 36 | return registryPackagesUrl(cfg, validate) 37 | } else { 38 | return nil, ErrNoRegistryConfigured 39 | } 40 | } 41 | 42 | func registryPackagesDir(cfg Config, validate bool) ([]Package, error) { 43 | tmpFs := os.DirFS(cfg.RegistryDir).(fs.ReadFileFS) 44 | return registryPackagesFs(cfg, tmpFs, validate) 45 | } 46 | 47 | func registryPackagesFs( 48 | cfg Config, 49 | filesystem fs.ReadFileFS, 50 | validate bool, 51 | ) ([]Package, error) { 52 | var ret []Package 53 | var retErr error 54 | absRegistryDir, err := filepath.Abs(cfg.RegistryDir) 55 | if err != nil { 56 | return nil, err 57 | } 58 | err = fs.WalkDir( 59 | filesystem, 60 | `.`, 61 | func(path string, d fs.DirEntry, err error) error { 62 | // Replacing leading dot with registry dir 63 | fullPath := filepath.Join( 64 | absRegistryDir, 65 | path, 66 | ) 67 | if err != nil { 68 | return err 69 | } 70 | // Skip dirs 71 | if d.IsDir() { 72 | // Skip all files inside dot-dirs 73 | if strings.HasPrefix(d.Name(), `.`) && d.Name() != `.` { 74 | return fs.SkipDir 75 | } 76 | return nil 77 | } 78 | // Skip non-YAML files based on file extension 79 | if filepath.Ext(path) != ".yaml" && filepath.Ext(path) != ".yml" { 80 | return nil 81 | } 82 | // Try to parse YAML file as package 83 | fileReader, err := filesystem.Open(path) 84 | if err != nil { 85 | return err 86 | } 87 | tmpPkg, err := NewPackageFromReader(fileReader) 88 | if err != nil { 89 | if validate { 90 | // Record error for deferred failure 91 | retErr = ErrValidationFailed 92 | } 93 | cfg.Logger.Warn( 94 | fmt.Sprintf( 95 | "failed to load %q as package: %s", 96 | fullPath, 97 | err, 98 | ), 99 | ) 100 | return nil 101 | } 102 | // Skip "empty" packages 103 | if tmpPkg.Name == "" || tmpPkg.Version == "" { 104 | return nil 105 | } 106 | // Record on-disk path for package file 107 | // This is used for relative paths for external file references 108 | tmpPkg.filePath = fullPath 109 | // Add package to results 110 | ret = append(ret, tmpPkg) 111 | return nil 112 | }, 113 | ) 114 | if err != nil { 115 | return nil, err 116 | } 117 | return ret, retErr 118 | } 119 | 120 | func registryPackagesUrl(cfg Config, validate bool) ([]Package, error) { 121 | cachePath := filepath.Join( 122 | cfg.CacheDir, 123 | "registry", 124 | ) 125 | // Check age of existing cache 126 | stat, err := os.Stat(cachePath) 127 | if err != nil { 128 | if !errors.Is(err, fs.ErrNotExist) { 129 | return nil, err 130 | } 131 | } 132 | // Fetch and extract registry ZIP into cache if it doesn't exist or is too old 133 | if errors.Is(err, fs.ErrNotExist) || 134 | stat == nil || 135 | stat.ModTime().Before(time.Now().Add(-24*time.Hour)) { 136 | // Fetch registry ZIP 137 | cfg.Logger.Info( 138 | "Fetching package registry " + cfg.RegistryUrl, 139 | ) 140 | ctx := context.Background() 141 | req, err := http.NewRequestWithContext( 142 | ctx, 143 | http.MethodGet, 144 | cfg.RegistryUrl, 145 | nil, 146 | ) 147 | if err != nil { 148 | return nil, err 149 | } 150 | resp, err := http.DefaultClient.Do(req) 151 | if err != nil { 152 | return nil, err 153 | } 154 | if resp == nil { 155 | return nil, fmt.Errorf("empty response from %s", cfg.RegistryUrl) 156 | } 157 | 158 | defer resp.Body.Close() 159 | respBody, err := io.ReadAll(resp.Body) 160 | if err != nil { 161 | return nil, err 162 | } 163 | zipData := bytes.NewReader(respBody) 164 | zipReader, err := zip.NewReader( 165 | zipData, 166 | int64(zipData.Len()), 167 | ) 168 | if err != nil { 169 | return nil, err 170 | } 171 | // Clear out existing cache files 172 | if err := os.RemoveAll(cachePath); err != nil { 173 | return nil, err 174 | } 175 | if err := os.MkdirAll(cachePath, fs.ModePerm); err != nil { 176 | return nil, err 177 | } 178 | // Extract files from ZIP into cache path 179 | for _, zipFile := range zipReader.File { 180 | // Skip directory entries 181 | if (zipFile.Mode() & fs.ModeDir) > 0 { 182 | continue 183 | } 184 | // Ensure there are no parent dir references in path 185 | if strings.Contains(zipFile.Name, "..") { 186 | return nil, errors.New("parent path reference in zip name") 187 | } 188 | // #nosec G305 189 | outPath := filepath.Join( 190 | cachePath, 191 | zipFile.Name, 192 | ) 193 | // Ensure our path is sane to prevent the gosec issue above 194 | if !strings.HasPrefix(outPath, filepath.Clean(cachePath)) { 195 | return nil, errors.New("zip extraction path mismatch") 196 | } 197 | // Create parent dir(s) 198 | if err := os.MkdirAll(filepath.Dir(outPath), fs.ModePerm); err != nil { 199 | return nil, err 200 | } 201 | // Read file bytes 202 | zf, err := zipFile.Open() 203 | if err != nil { 204 | return nil, err 205 | } 206 | zfData, err := io.ReadAll(zf) 207 | if err != nil { 208 | return nil, err 209 | } 210 | zf.Close() 211 | // Write file 212 | if err := os.WriteFile(outPath, zfData, fs.ModePerm); err != nil { 213 | return nil, err 214 | } 215 | } 216 | } 217 | // Process cache dir 218 | cfg.RegistryDir = cachePath 219 | return registryPackagesDir(cfg, validate) 220 | } 221 | -------------------------------------------------------------------------------- /pkgmgr/registry_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "log/slog" 19 | "reflect" 20 | "testing" 21 | "testing/fstest" 22 | ) 23 | 24 | func TestRegistryPackagesFs(t *testing.T) { 25 | testRegistryDir := "test/registry/dir" 26 | testFs := fstest.MapFS{ 27 | // This file is outside the registry dir and should not get processed 28 | "some/random/file.yaml": {}, 29 | "test/registry/dir/packageA/packageA-1.2.3.yaml": { 30 | Data: []byte("name: packageA\nversion: 1.2.3"), 31 | }, 32 | "test/registry/dir/packageA/packageA-2.3.4.yaml": { 33 | Data: []byte("name: packageA\nversion: 2.3.4"), 34 | }, 35 | "test/registry/dir/packageB/packageB-3.4.5.yml": { 36 | Data: []byte("name: packageB\nversion: 3.4.5"), 37 | }, 38 | // This file should get ignored without a YAML extension 39 | "test/registry/dir/some.file": { 40 | Data: []byte("name: packageC\nversion: 4.5.6"), 41 | }, 42 | } 43 | testExpectedPkgs := []Package{ 44 | { 45 | Name: "packageA", 46 | Version: "1.2.3", 47 | }, 48 | { 49 | Name: "packageA", 50 | Version: "2.3.4", 51 | }, 52 | { 53 | Name: "packageB", 54 | Version: "3.4.5", 55 | }, 56 | } 57 | cfg := Config{ 58 | RegistryDir: testRegistryDir, 59 | Logger: slog.Default(), 60 | } 61 | pkgs, err := registryPackagesFs(cfg, testFs, false) 62 | if err != nil { 63 | t.Fatalf("unexpected error: %s", err) 64 | } 65 | // Remove filePath for easier comparison 66 | for idx, pkg := range pkgs { 67 | pkg.filePath = "" 68 | pkgs[idx] = pkg 69 | } 70 | if !reflect.DeepEqual(pkgs, testExpectedPkgs) { 71 | t.Fatalf( 72 | "did not get expected packages\n got: %#v\n expected: %#v", 73 | pkgs, 74 | testExpectedPkgs, 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pkgmgr/resolver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "fmt" 19 | "log/slog" 20 | "strings" 21 | 22 | "github.com/hashicorp/go-version" 23 | ) 24 | 25 | type Resolver struct { 26 | context string 27 | logger *slog.Logger 28 | installedPkgs []InstalledPackage 29 | availablePkgs []Package 30 | installedConstraints map[string]version.Constraints 31 | } 32 | 33 | type ResolverInstallSet struct { 34 | Install Package 35 | Options map[string]bool 36 | Selected bool 37 | } 38 | 39 | type ResolverUpgradeSet struct { 40 | Installed InstalledPackage 41 | Upgrade Package 42 | Options map[string]bool 43 | } 44 | 45 | func NewResolver( 46 | installedPkgs []InstalledPackage, 47 | availablePkgs []Package, 48 | context string, 49 | logger *slog.Logger, 50 | ) (*Resolver, error) { 51 | r := &Resolver{ 52 | context: context, 53 | logger: logger, 54 | installedPkgs: installedPkgs[:], 55 | availablePkgs: availablePkgs[:], 56 | installedConstraints: make(map[string]version.Constraints), 57 | } 58 | // Calculate package constraints from installed packages 59 | for _, installedPkg := range installedPkgs { 60 | // Add constraint for each explicit dependency 61 | for _, dep := range installedPkg.Package.Dependencies { 62 | depPkgName, depPkgVersionSpec, _ := r.splitPackage(dep) 63 | tmpConstraints, err := version.NewConstraint(depPkgVersionSpec) 64 | if err != nil { 65 | return nil, err 66 | } 67 | r.installedConstraints[depPkgName] = append( 68 | r.installedConstraints[depPkgName], 69 | tmpConstraints..., 70 | ) 71 | logger.Debug( 72 | fmt.Sprintf( 73 | "added constraint for installed package %q dependency: %q: %s", 74 | installedPkg.Package.Name, 75 | depPkgName, 76 | tmpConstraints.String(), 77 | ), 78 | ) 79 | } 80 | } 81 | return r, nil 82 | } 83 | 84 | func (r *Resolver) Install(pkgs ...string) ([]ResolverInstallSet, error) { 85 | ret := []ResolverInstallSet{} 86 | for _, pkg := range pkgs { 87 | pkgName, pkgVersionSpec, pkgOpts := r.splitPackage(pkg) 88 | if pkg, err := r.findInstalled(pkgName, ""); err != nil { 89 | return nil, err 90 | } else if !pkg.IsEmpty() { 91 | return nil, NewResolverPackageAlreadyInstalledError(pkgName) 92 | } 93 | latestPkg, err := r.latestAvailablePackage(pkgName, pkgVersionSpec, nil) 94 | if err != nil { 95 | return nil, err 96 | } 97 | if latestPkg.IsEmpty() { 98 | return nil, NewResolverNoAvailablePackage(pkg) 99 | } 100 | // Calculate dependencies 101 | neededPkgs, err := r.getNeededDeps(latestPkg) 102 | if err != nil { 103 | return nil, err 104 | } 105 | ret = append(ret, neededPkgs...) 106 | // Add selected package 107 | ret = append( 108 | ret, 109 | ResolverInstallSet{ 110 | Install: latestPkg, 111 | Selected: true, 112 | Options: pkgOpts, 113 | }, 114 | ) 115 | } 116 | return ret, nil 117 | } 118 | 119 | func (r *Resolver) Upgrade(pkgs ...string) ([]ResolverUpgradeSet, error) { 120 | ret := []ResolverUpgradeSet{} 121 | for _, pkg := range pkgs { 122 | pkgName, pkgVersionSpec, pkgOpts := r.splitPackage(pkg) 123 | installedPkg, err := r.findInstalled(pkgName, "") 124 | if err != nil { 125 | return nil, err 126 | } else if installedPkg.IsEmpty() { 127 | return nil, NewPackageNotInstalledError(pkgName, r.context) 128 | } 129 | latestPkg, err := r.latestAvailablePackage(pkgName, pkgVersionSpec, nil) 130 | if err != nil { 131 | return nil, err 132 | } 133 | if latestPkg.Version == "" || 134 | latestPkg.Version == installedPkg.Package.Version { 135 | return nil, NewNoPackageAvailableForUpgradeError(pkg) 136 | } 137 | ret = append( 138 | ret, 139 | ResolverUpgradeSet{ 140 | Installed: installedPkg, 141 | Upgrade: latestPkg, 142 | Options: pkgOpts, 143 | }, 144 | ) 145 | // Calculate dependencies 146 | neededPkgs, err := r.getNeededDeps(latestPkg) 147 | if err != nil { 148 | return nil, err 149 | } 150 | for _, neededPkg := range neededPkgs { 151 | tmpInstalled, err := r.findInstalled(neededPkg.Install.Name, "") 152 | if err != nil { 153 | return nil, err 154 | } 155 | ret = append( 156 | ret, 157 | ResolverUpgradeSet{ 158 | Installed: tmpInstalled, 159 | Upgrade: neededPkg.Install, 160 | Options: neededPkg.Options, 161 | }, 162 | ) 163 | } 164 | } 165 | return ret, nil 166 | } 167 | 168 | func (r *Resolver) Uninstall(pkgs ...InstalledPackage) error { 169 | for _, pkg := range pkgs { 170 | pkgVersion, err := version.NewVersion(pkg.Package.Version) 171 | if err != nil { 172 | return err 173 | } 174 | for _, installedPkg := range r.installedPkgs { 175 | for _, dep := range installedPkg.Package.Dependencies { 176 | depPkgName, depPkgVersionSpec, _ := r.splitPackage(dep) 177 | // Skip installed package if it doesn't match dep package name 178 | if pkg.Package.Name != depPkgName { 179 | continue 180 | } 181 | // Skip installed packages that don't match the specified dep version constraint 182 | if depPkgVersionSpec != "" { 183 | constraints, err := version.NewConstraint(depPkgVersionSpec) 184 | if err != nil { 185 | return err 186 | } 187 | if !constraints.Check(pkgVersion) { 188 | continue 189 | } 190 | } 191 | return NewPackageUninstallWouldBreakDepsError( 192 | pkg.Package.Name, 193 | pkg.Package.Version, 194 | installedPkg.Package.Name, 195 | installedPkg.Package.Version, 196 | ) 197 | } 198 | } 199 | } 200 | return nil 201 | } 202 | 203 | func (r *Resolver) getNeededDeps(pkg Package) ([]ResolverInstallSet, error) { 204 | // NOTE: this function is very naive and only works for a single level of dependencies 205 | ret := []ResolverInstallSet{} 206 | for _, dep := range pkg.Dependencies { 207 | depPkgName, depPkgVersionSpec, depPkgOpts := r.splitPackage(dep) 208 | // Check if we already have an installed package that satisfies the dependency 209 | if pkg, err := r.findInstalled(depPkgName, depPkgVersionSpec); err != nil { 210 | return nil, err 211 | } else if !pkg.IsEmpty() { 212 | continue 213 | } 214 | // Check if we already have any installed version of the package 215 | if pkg, err := r.findInstalled(depPkgName, depPkgVersionSpec); err != nil { 216 | return nil, err 217 | } else if !pkg.IsEmpty() { 218 | return nil, NewResolverInstalledPackageNoMatchVersionSpecError(pkg.Package.Name, pkg.Package.Version, dep) 219 | } 220 | availablePkgs, err := r.findAvailable( 221 | depPkgName, 222 | depPkgVersionSpec, 223 | nil, 224 | ) 225 | if err != nil { 226 | return nil, err 227 | } 228 | if len(availablePkgs) == 0 { 229 | return nil, NewResolverNoAvailablePackageDependencyError(dep) 230 | } 231 | latestPkg, err := r.latestPackage(availablePkgs, nil) 232 | if err != nil { 233 | return nil, err 234 | } 235 | ret = append( 236 | ret, 237 | ResolverInstallSet{ 238 | Install: latestPkg, 239 | Options: depPkgOpts, 240 | }, 241 | ) 242 | } 243 | return ret, nil 244 | } 245 | 246 | func (r *Resolver) splitPackage(pkg string) (string, string, map[string]bool) { 247 | var pkgName, pkgVersionSpec string 248 | pkgOpts := make(map[string]bool) 249 | // Extract any package option flags 250 | optsOpenIdx := strings.Index(pkg, `[`) 251 | optsCloseIdx := strings.Index(pkg, `]`) 252 | if optsOpenIdx > 0 && optsCloseIdx > optsOpenIdx { 253 | pkgName = pkg[:optsOpenIdx] 254 | tmpOpts := pkg[optsOpenIdx+1 : optsCloseIdx] 255 | tmpFlags := strings.Split(tmpOpts, `,`) 256 | for _, tmpFlag := range tmpFlags { 257 | flagVal := true 258 | if strings.HasPrefix(tmpFlag, `-`) { 259 | flagVal = false 260 | tmpFlag = tmpFlag[1:] 261 | } 262 | pkgOpts[tmpFlag] = flagVal 263 | } 264 | } 265 | // Extract version spec 266 | versionSpecIdx := strings.IndexAny(pkg, ` <>=~!`) 267 | if versionSpecIdx > 0 { 268 | if pkgName == "" { 269 | pkgName = pkg[:versionSpecIdx] 270 | } 271 | pkgVersionSpec = strings.TrimSpace(pkg[versionSpecIdx:]) 272 | } 273 | // Use the original package name if we don't already have one from above 274 | if pkgName == "" { 275 | pkgName = pkg 276 | } 277 | return pkgName, pkgVersionSpec, pkgOpts 278 | } 279 | 280 | func (r *Resolver) findInstalled( 281 | pkgName string, 282 | pkgVersionSpec string, 283 | ) (InstalledPackage, error) { 284 | constraints := version.Constraints{} 285 | if pkgVersionSpec != "" { 286 | tmpConstraints, err := version.NewConstraint(pkgVersionSpec) 287 | if err != nil { 288 | return InstalledPackage{}, err 289 | } 290 | constraints = tmpConstraints 291 | } 292 | for _, installedPkg := range r.installedPkgs { 293 | if installedPkg.Package.Name != pkgName { 294 | continue 295 | } 296 | if pkgVersionSpec != "" { 297 | installedPkgVer, err := version.NewVersion( 298 | installedPkg.Package.Version, 299 | ) 300 | if err != nil { 301 | return InstalledPackage{}, err 302 | } 303 | if !constraints.Check(installedPkgVer) { 304 | continue 305 | } 306 | } 307 | return installedPkg, nil 308 | } 309 | return InstalledPackage{}, nil 310 | } 311 | 312 | func (r *Resolver) findAvailable( 313 | pkgName string, 314 | pkgVersionSpec string, 315 | extraConstraints version.Constraints, 316 | ) ([]Package, error) { 317 | var constraints version.Constraints 318 | // Filter to versions matching our version spec 319 | if pkgVersionSpec != "" { 320 | tmpConstraints, err := version.NewConstraint(pkgVersionSpec) 321 | if err != nil { 322 | return nil, err 323 | } 324 | constraints = tmpConstraints 325 | } 326 | // Use installed package constraints if none provided 327 | if extraConstraints == nil { 328 | if r.installedConstraints != nil { 329 | if pkgConstraints, ok := r.installedConstraints[pkgName]; ok { 330 | extraConstraints = pkgConstraints 331 | } 332 | } 333 | } 334 | // Filter to versions matching provided constraints 335 | if extraConstraints != nil { 336 | constraints = append( 337 | constraints, 338 | extraConstraints..., 339 | ) 340 | } 341 | ret := []Package{} 342 | for _, availablePkg := range r.availablePkgs { 343 | if availablePkg.Name != pkgName { 344 | continue 345 | } 346 | if constraints != nil { 347 | availablePkgVer, err := version.NewVersion(availablePkg.Version) 348 | if err != nil { 349 | return nil, err 350 | } 351 | if !constraints.Check(availablePkgVer) { 352 | r.logger.Debug( 353 | fmt.Sprintf( 354 | "excluding available package \"%s = %s\" due to constraint: %s", 355 | availablePkg.Name, 356 | availablePkg.Version, 357 | constraints.String(), 358 | ), 359 | ) 360 | continue 361 | } 362 | } 363 | ret = append(ret, availablePkg) 364 | } 365 | return ret, nil 366 | } 367 | 368 | func (r *Resolver) latestAvailablePackage( 369 | pkgName string, 370 | pkgVersionSpec string, 371 | constraints version.Constraints, 372 | ) (Package, error) { 373 | pkgs, err := r.findAvailable(pkgName, pkgVersionSpec, constraints) 374 | if err != nil { 375 | return Package{}, err 376 | } 377 | return r.latestPackage(pkgs, constraints) 378 | } 379 | 380 | func (r *Resolver) latestPackage( 381 | pkgs []Package, 382 | constraints version.Constraints, 383 | ) (Package, error) { 384 | var ret Package 385 | var retVer *version.Version 386 | for _, pkg := range pkgs { 387 | pkgVer, err := version.NewVersion(pkg.Version) 388 | if err != nil { 389 | return ret, err 390 | } 391 | // Skip package if it doesn't match provided constraints 392 | if len(constraints) > 0 { 393 | if !constraints.Check(pkgVer) { 394 | continue 395 | } 396 | } 397 | // Set this package as the latest if none set or newer than previous set 398 | if retVer == nil || pkgVer.GreaterThan(retVer) { 399 | ret = pkg 400 | retVer = pkgVer 401 | } 402 | } 403 | return ret, nil 404 | } 405 | -------------------------------------------------------------------------------- /pkgmgr/resolver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "reflect" 19 | "testing" 20 | ) 21 | 22 | func TestSplitPackage(t *testing.T) { 23 | testDefs := []struct { 24 | Package string 25 | Name string 26 | VersionSpec string 27 | Options map[string]bool 28 | }{ 29 | { 30 | Package: "test-packageB[foo,-bar] >= 1.2.3", 31 | Name: "test-packageB", 32 | VersionSpec: ">= 1.2.3", 33 | Options: map[string]bool{ 34 | "foo": true, 35 | "bar": false, 36 | }, 37 | }, 38 | { 39 | Package: "test-package<1.2.4", 40 | Name: "test-package", 41 | VersionSpec: "<1.2.4", 42 | }, 43 | { 44 | Package: "test-package", 45 | Name: "test-package", 46 | }, 47 | { 48 | Package: "test-package[foo", 49 | Name: "test-package[foo", 50 | }, 51 | } 52 | for _, testDef := range testDefs { 53 | tmpResolver := &Resolver{} 54 | pkgName, pkgVersionSpec, pkgOpts := tmpResolver.splitPackage( 55 | testDef.Package, 56 | ) 57 | if pkgName != testDef.Name { 58 | t.Fatalf( 59 | "did not get expected package name: got %q, expected %q", 60 | pkgName, 61 | testDef.Name, 62 | ) 63 | } 64 | if pkgVersionSpec != testDef.VersionSpec { 65 | t.Fatalf( 66 | "did not get expected package version spec: got %q, expected %q", 67 | pkgVersionSpec, 68 | testDef.VersionSpec, 69 | ) 70 | } 71 | if len(pkgOpts) > 0 && len(testDef.Options) > 0 { 72 | if !reflect.DeepEqual(pkgOpts, testDef.Options) { 73 | t.Fatalf( 74 | "did not get expected package options\n got: %#v\n expected: %#v", 75 | pkgOpts, 76 | testDef.Options, 77 | ) 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkgmgr/state.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "os" 19 | "path/filepath" 20 | 21 | "gopkg.in/yaml.v3" 22 | ) 23 | 24 | const ( 25 | contextsFilename = "contexts.yaml" 26 | activeContextFilename = "active_context.yaml" 27 | installedPackagesFilename = "installed_packages.yaml" 28 | ) 29 | 30 | type State struct { 31 | config Config 32 | ActiveContext string 33 | Contexts map[string]Context 34 | InstalledPackages []InstalledPackage 35 | } 36 | 37 | func NewState(cfg Config) *State { 38 | return &State{ 39 | config: cfg, 40 | Contexts: make(map[string]Context), 41 | } 42 | } 43 | 44 | func (s *State) Load() error { 45 | if err := s.loadContexts(); err != nil { 46 | return err 47 | } 48 | if err := s.loadActiveContext(); err != nil { 49 | return err 50 | } 51 | if err := s.loadInstalledPackages(); err != nil { 52 | return err 53 | } 54 | return nil 55 | } 56 | 57 | func (s *State) Save() error { 58 | if err := s.saveContexts(); err != nil { 59 | return err 60 | } 61 | if err := s.saveActiveContext(); err != nil { 62 | return err 63 | } 64 | if err := s.saveInstalledPackages(); err != nil { 65 | return err 66 | } 67 | return nil 68 | } 69 | 70 | func (s *State) loadFile(filename string, dest any) error { 71 | tmpPath := filepath.Join( 72 | s.config.ConfigDir, 73 | filename, 74 | ) 75 | // Check if the file exists and we can access it 76 | if _, err := os.Stat(tmpPath); err != nil { 77 | // Treat no file like an empty file 78 | if os.IsNotExist(err) { 79 | return nil 80 | } 81 | return err 82 | } 83 | content, err := os.ReadFile(tmpPath) 84 | if err != nil { 85 | return err 86 | } 87 | if err := yaml.Unmarshal(content, dest); err != nil { 88 | return err 89 | } 90 | return nil 91 | } 92 | 93 | func (s *State) saveFile(filename string, src any) error { 94 | // Create parent directory if it doesn't exist 95 | if _, err := os.Stat(s.config.ConfigDir); err != nil { 96 | if os.IsNotExist(err) { 97 | if err := os.MkdirAll(s.config.ConfigDir, 0o700); err != nil { 98 | return err 99 | } 100 | } 101 | } 102 | tmpPath := filepath.Join( 103 | s.config.ConfigDir, 104 | filename, 105 | ) 106 | yamlContent, err := yaml.Marshal(src) 107 | if err != nil { 108 | return err 109 | } 110 | if err := os.WriteFile(tmpPath, yamlContent, 0o600); err != nil { 111 | return err 112 | } 113 | return nil 114 | } 115 | 116 | func (s *State) loadContexts() error { 117 | if err := s.loadFile(contextsFilename, &(s.Contexts)); err != nil { 118 | return err 119 | } 120 | if len(s.Contexts) == 0 { 121 | s.Contexts[defaultContextName] = defaultContext 122 | } 123 | return nil 124 | } 125 | 126 | func (s *State) saveContexts() error { 127 | return s.saveFile(contextsFilename, &(s.Contexts)) 128 | } 129 | 130 | func (s *State) loadActiveContext() error { 131 | if err := s.loadFile(activeContextFilename, &(s.ActiveContext)); err != nil { 132 | return err 133 | } 134 | if s.ActiveContext == "" { 135 | s.ActiveContext = defaultContextName 136 | } 137 | return nil 138 | } 139 | 140 | func (s *State) saveActiveContext() error { 141 | return s.saveFile(activeContextFilename, &(s.ActiveContext)) 142 | } 143 | 144 | func (s *State) loadInstalledPackages() error { 145 | return s.loadFile(installedPackagesFilename, &(s.InstalledPackages)) 146 | } 147 | 148 | func (s *State) saveInstalledPackages() error { 149 | return s.saveFile(installedPackagesFilename, &(s.InstalledPackages)) 150 | } 151 | -------------------------------------------------------------------------------- /pkgmgr/template.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Blink Labs Software 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkgmgr 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "maps" 21 | "text/template" 22 | 23 | "github.com/Masterminds/sprig/v3" 24 | ) 25 | 26 | type Template struct { 27 | tmpl *template.Template 28 | baseVars map[string]any 29 | } 30 | 31 | func NewTemplate(baseVars map[string]any) *Template { 32 | return &Template{ 33 | tmpl: template.New("main").Funcs(sprig.FuncMap()), 34 | baseVars: baseVars, 35 | } 36 | } 37 | 38 | func (t *Template) Render( 39 | tmplBody string, 40 | extraVars map[string]any, 41 | ) (string, error) { 42 | // Build our vars 43 | tmpVars := map[string]any{} 44 | maps.Copy(tmpVars, t.baseVars) 45 | maps.Copy(tmpVars, extraVars) 46 | // Parse template body 47 | tmpl, err := t.tmpl.Parse(tmplBody) 48 | if err != nil { 49 | return "", err 50 | } 51 | // Render template 52 | outBuffer := bytes.NewBuffer(nil) 53 | if err := tmpl.Execute(outBuffer, tmpVars); err != nil { 54 | return "", err 55 | } 56 | return outBuffer.String(), nil 57 | } 58 | 59 | // WithVars creates a copy of the Template with the extra variables added to the original base variables 60 | func (t *Template) WithVars(extraVars map[string]any) *Template { 61 | tmpVars := map[string]any{} 62 | maps.Copy(tmpVars, t.baseVars) 63 | maps.Copy(tmpVars, extraVars) 64 | tmpl := NewTemplate(tmpVars) 65 | return tmpl 66 | } 67 | 68 | func (t *Template) EvaluateCondition( 69 | condition string, 70 | extraVars map[string]any, 71 | ) (bool, error) { 72 | tmpl := fmt.Sprintf( 73 | `{{ if %s }}true{{ else }}false{{ end }}`, 74 | condition, 75 | ) 76 | rendered, err := t.Render(tmpl, extraVars) 77 | if err != nil { 78 | return false, err 79 | } 80 | if rendered == `true` { 81 | return true, nil 82 | } 83 | return false, nil 84 | } 85 | --------------------------------------------------------------------------------