diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..fa24f06 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,26 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + time: "06:00" + timezone: "America/Chicago" + labels: [] + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + time: "06:00" + timezone: "America/Chicago" + labels: [] + open-pull-requests-limit: 15 + groups: + x: + patterns: + - "golang.org/x/*" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2986b33..5f7f4f0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -13,10 +13,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: "~1.19" + go-version: "~1.22" - name: Get Go cache paths id: go-cache-paths @@ -25,20 +25,20 @@ jobs: echo "::set-output name=go-mod::$(go env GOMODCACHE)" - name: Go build cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-release-go-build-${{ hashFiles('**/go.sum') }} - name: Go mod cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} key: ${{ runner.os }}-release-go-mod-${{ hashFiles('**/go.sum') }} - run: make build - - uses: softprops/action-gh-release@v1 + - uses: softprops/action-gh-release@v2 with: draft: true files: ./bin/* diff --git a/.github/workflows/cla.yaml b/.github/workflows/cla.yaml new file mode 100644 index 0000000..71c2e90 --- /dev/null +++ b/.github/workflows/cla.yaml @@ -0,0 +1,26 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened,closed,synchronize] + +jobs: + CLAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + uses: contributor-assistant/github-action@v2.6.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # the below token should have repo scope and must be manually added by you in the repository's secret + PERSONAL_ACCESS_TOKEN : ${{ secrets.CDRCOMMUNITY_GITHUB_TOKEN }} + with: + remote-organization-name: 'coder' + remote-repository-name: 'cla' + path-to-signatures: 'v2022-09-04/signatures.json' + path-to-document: 'https://github.com/coder/cla/blob/main/README.md' + # branch should not be protected + branch: 'main' + allowlist: dependabot* diff --git a/.github/workflows/helm.yaml b/.github/workflows/helm.yaml index 679a78f..824a108 100644 --- a/.github/workflows/helm.yaml +++ b/.github/workflows/helm.yaml @@ -24,8 +24,8 @@ jobs: timeout-minutes: 5 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: azure/setup-helm@v3.3 + - uses: actions/checkout@v4 + - uses: azure/setup-helm@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - run: helm lint --strict ./helm diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d8d648c..551f9d0 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -6,10 +6,12 @@ on: - main paths: - "**.go" + - "go.sum" - ".github/workflows/lint.yaml" pull_request: paths: - "**.go" + - "go.sum" - ".github/workflows/lint.yaml" workflow_dispatch: @@ -24,11 +26,11 @@ jobs: timeout-minutes: 5 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: "~1.19" + go-version: 1.24.2 - name: golangci-lint - uses: golangci/golangci-lint-action@v3.2.0 + uses: golangci/golangci-lint-action@v7.0.0 with: - version: v1.48.0 + version: v2.1.2 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 5809659..839a122 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -18,22 +18,22 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: robinraju/release-downloader@v1.5 + - uses: actions/checkout@v4 + - uses: robinraju/release-downloader@v1.12 with: repository: "coder/code-marketplace" tag: ${{ github.event.inputs.version || github.ref_name }} fileName: "code-marketplace-linux-*" out-file-path: "bin" - - uses: docker/login-action@v2 + - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/setup-qemu-action@v2 - - uses: docker/setup-buildx-action@v2 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 - run: docker buildx bake -f ./docker-bake.hcl --push env: VERSION: ${{ github.event.inputs.version || github.ref_name }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ebbe48c..e8bca42 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,10 +6,12 @@ on: - main paths: - "**.go" + - "go.sum" - ".github/workflows/test.yaml" pull_request: paths: - "**.go" + - "go.sum" - ".github/workflows/test.yaml" workflow_dispatch: @@ -30,10 +32,10 @@ jobs: - macos-latest - windows-2022 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: "~1.19" + go-version: "~1.22" - name: Echo Go Cache Paths id: go-cache-paths @@ -42,23 +44,19 @@ jobs: echo "::set-output name=go-mod::$(go env GOMODCACHE)" - name: Go Build Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.**', '**.go') }} - name: Go Mod Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Install gotestsum - uses: jaxxstorm/action-install-gh-release@v1.7.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - repo: gotestyourself/gotestsum - tag: v1.7.0 + shell: bash + run: go install gotest.tools/gotestsum@latest - run: make test diff --git a/.gitignore b/.gitignore index 7a0c72b..92996d1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ bin coverage extensions +.idea diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..387c5e4 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,4 @@ +version: "2" +linters: + disable: + - errcheck diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6fd4a..e44b5ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,94 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [2.3.1](https://github.com/coder/code-marketplace/releases/tag/v2.3.1) - 2025-03-06 + +### Changed + +- Updated several dependencies with CVEs. + +## [2.3.0](https://github.com/coder/code-marketplace/releases/tag/v2.3.0) - 2024-12-20 + +### Added + +- Add empty signatures when starting the server with --sign. This will not work + with VS Code on Windows and macOS as we do not have the key required, but it + will work for open source versions of VS Code (VSCodium, code-server) and VS + Code on Linux where signatures must exist but are not actually checked. + +### Changed + +- Ignore extensions without a manifest. This is not expected in normal use, but + could happen if, for example, a manifest temporarily failed to download, which + would then crash the entire process with a segfault. + +## [2.2.1](https://github.com/coder/code-marketplace/releases/tag/v2.2.1) - 2024-08-14 + +### Fixed + +- The "attempt to download manually" URL in VS Code will now work. + +## [2.2.0](https://github.com/coder/code-marketplace/releases/tag/v2.2.0) - 2024-07-17 + +### Changed + +- Default max page size increased from 50 to 200. + +### Added + +- New `server` sub-command flag `--max-page-size` for setting the max page size. + +## [2.1.0](https://github.com/coder/code-marketplace/releases/tag/v2.1.0) - 2023-12-21 + +### Added + +- New `server` sub-command flag `--list-cache-duration` for setting the duration + of the cache used when listing and searching extensions. The default is still + one minute. +- Local storage will also use a cache for listing/searching extensions + (previously only Artifactory storage used a cache). + +## [2.0.1](https://github.com/coder/code-marketplace/releases/tag/v2.0.1) - 2023-12-08 + +### Fixed + +- Extensions with problematic UTF-8 characters will no longer cause a panic. +- Preview extensions will now show up as such. + +## [2.0.0](https://github.com/coder/code-marketplace/releases/tag/v2.0.0) - 2023-10-11 + +### Breaking changes + +- When removing extensions, the version is now delineated by `@` instead of `-` + (for example `remove vscodevim.vim@1.0.0`). This fixes being unable to remove + extensions with `-` in their names. Removal is the only backwards-incompatible + change; extensions are still added, stored, and queried the same way. + +### Added + +- Support for platform-specific extensions. Previously all versions would have + been treated as universal and overwritten each other but now versions for + different platforms will be stored separately and show up separately in the + API response. If there are platform-specific versions that have already been + added, they will continue to be treated as universal versions so these should + be removed and re-added to be properly registered as platform-specific. + +## [1.2.2](https://github.com/coder/code-marketplace/releases/tag/v1.2.2) - 2023-05-30 + +### Changed + +- Help/usage outputs the binary name as `code-marketplace` instead of + `marketplace` to be consistent with documentation. +- Binary is symlinked into /usr/local/bin in the Docker image so it can be + invoked as simply `code-marketplace`. + +## [1.2.1](https://github.com/coder/code-marketplace/releases/tag/v1.2.1) - 2022-10-31 + +### Fixed + +- Adding extensions from a URL. This broke in 1.2.0 with the addition of bulk + adding. + ## [1.2.0](https://github.com/coder/code-marketplace/releases/tag/v1.2.0) - 2022-10-20 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a0193fa --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing + +## Development + +```console +mkdir extensions +go run ./cmd/marketplace/main.go server [flags] +``` + +When you make a change that affects people deploying the marketplace please +update the changelog as part of your PR. + +You can use `make gen` to generate a mock `extensions` directory for testing and +`make upload` to upload them to an Artifactory repository. + +## Tests + +To run the tests: + +``` +make test +``` + +To run the Artifactory tests against a real repository instead of a mock: + +``` +export ARTIFACTORY_URI=myuri +export ARTIFACTORY_REPO=myrepo +export ARTIFACTORY_TOKEN=mytoken +make test +``` + +See the readme for using the marketplace with code-server. + +When testing with code-server you may run into issues with content security +policy if the marketplace runs on a different domain over HTTP; in this case you +will need to disable content security policy in your browser or manually edit +the policy in code-server's source. + +## Releasing + +1. Check that the changelog lists all the important changes. +2. Update the changelog with the release date. +3. Push a tag with the new version. +4. Update the resulting draft release with the changelog contents. +5. Publish the draft release. +6. Bump the Helm chart version once the Docker images have published. diff --git a/Dockerfile b/Dockerfile index dfa85a4..c32772f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,5 +6,6 @@ COPY ./bin/code-marketplace-linux-$TARGETARCH /opt/code-marketplace FROM alpine:latest COPY --chmod=755 --from=binaries /opt/code-marketplace /opt +RUN ln -s /opt/code-marketplace /usr/local/bin/code-marketplace -ENTRYPOINT [ "/opt/code-marketplace", "server" ] +ENTRYPOINT [ "code-marketplace", "server" ] diff --git a/README.md b/README.md index f24a4e9..3c6c2d2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,12 @@ The Code Extension Marketplace is an open-source alternative to the VS Code Marketplace for use in editors like -[code-server](https://github.com/coder/code-server). +[code-server](https://github.com/coder/code-server) or [VSCodium](https://github.com/VSCodium/vscodium). + +It is maintained by [Coder](https://www.coder.com) and is used by our enterprise +customers in regulated and security-conscious industries like banking, asset +management, military, and intelligence where they deploy Coder in an air-gapped +network and accessing an internet-hosted marketplace is not allowed. This marketplace reads extensions from file storage and provides an API for editors to consume. It does not have a frontend or any mechanisms for extension @@ -58,7 +63,7 @@ export ARTIFACTORY_TOKEN="my-token" ./code-marketplace [command] --artifactory http://artifactory.server/artifactory --repo extensions ``` -The token will be used as the `Authorization` header with the value `Bearer +The token will be used in the `Authorization` header with the value `Bearer `. ### Exposing the marketplace @@ -75,7 +80,7 @@ One way to test this is to make a query and check one of the URLs in the response: ```console -$ curl 'https://example.com/api/extensionquery' -H 'Accept: application/json;api-version=3.0-preview.1' --compressed -H 'Content-Type: application/json' --data-raw '{"filters":[{"criteria":[{"filterType":8,"value":"Microsoft.VisualStudio.Code"}],"pageSize":1}],"flags":439}' | jq .results[0].extensions[0].versions[0].assetUri +curl 'https://example.com/api/extensionquery' -H 'Accept: application/json;api-version=3.0-preview.1' --compressed -H 'Content-Type: application/json' --data-raw '{"filters":[{"criteria":[{"filterType":8,"value":"Microsoft.VisualStudio.Code"}],"pageSize":1}],"flags":439}' | jq .results[0].extensions[0].versions[0].assetUri "https://example.com/assets/vscodevim/vim/1.24.1" ``` @@ -116,22 +121,69 @@ For example to add the Python extension from Open VSX: Or the Vim extension from GitHub: ```console -./code-marketplace add -https://github.com/VSCodeVim/Vim/releases/download/v1.24.1/vim-1.24.1.vsix [flags] +./code-marketplace add https://github.com/VSCodeVim/Vim/releases/download/v1.24.1/vim-1.24.1.vsix [flags] ``` ## Removing extensions -Extensions can be removed from the marketplace by ID and version (or use `--all` -to remove all versions). +Extensions can be removed from the marketplace by ID and version or `--all` to +remove all versions. ```console -./code-marketplace remove ms-python.python-2022.14.0 [flags] +./code-marketplace remove ms-python.python@2022.14.0 [flags] ./code-marketplace remove ms-python.python --all [flags] ``` +## Scanning frequency and caching + +The marketplace does not utilize a database. When an extension query is made, +the marketplace scans the local file system or queries Artifactory on demand to +find all the available extensions. + +However, for Artifactory in particular this can be slow, so this full list of +extensions is cached in memory for a default of one minute and reused for any +subsequent requests that fall within that duration. This duration can be +configured or disabled with `--list-cache-duration` and applies to both storage +backends. + +This means that when you add or remove an extension, depending on when the last +request was made, it can take a duration between zero and +`--list-cache-duration` for the query response to reflect that change. + +Artifactory storage also uses a second in-memory cache for extension manifests, +which are referenced in extension queries (for things like categories). This +cache is initially populated with all available extension manifests on startup. +Extensions added after the server is running are added to the cache on-demand +the next time extensions are scanned. + +The manifest cache has no expiration and never evicts manifests because it was +expected that extensions are typically only ever added and individual extension +version manifests never change; however we would like to implement evicting +manifests of extensions that have been removed. + +With local storage, manifests are read directly from the file system on +demand. Requests for other extension assets (such as icons) for both storage +backends have no cache and are read/proxied directly from the file system or +Artifactory since they are not in the extension query hot path. + ## Usage in code-server +You can point code-server to your marketplace by setting the +`EXTENSIONS_GALLERY` environment variable. + +The value of this variable is a JSON blob that specifies the service URL, item +URL, and resource URL template. + +- `serviceURL`: specifies the location of the API (`https:///api`). +- `itemURL`: the frontend for extensions which is currently just a mostly blank + page that says "not supported" (`https:///item`) +- `resourceURLTemplate`: used to download web extensions like Vim; code-server + itself will replace the `{publisher}`, `{name}`, `{version}`, and `{path}` + template variables so use them verbatim + (`https:///files/{publisher}/{name}/{version}/{path}`). + +For example (replace `` with your marketplace's domain): + ```console export EXTENSIONS_GALLERY='{"serviceUrl":"https:///api", "itemUrl":"https:///item", "resourceUrlTemplate": "https:///files/{publisher}/{name}/{version}/{path}"}' code-server @@ -140,40 +192,39 @@ code-server If code-server reports content security policy errors ensure that the marketplace is running behind an https URL. -## Development +### Custom certificate authority -```console -mkdir extensions -go run ./cmd/marketplace/main.go server [flags] -``` +If you are using a custom certificate authority or a self-signed certificate and +get errors like "unable to verify the first certificate", you may need to set +the [NODE_EXTRA_CA_CERTS](https://nodejs.org/api/cli.html#node_extra_ca_certsfile) +environment variable for code-server to find your certificates bundle. -When testing with code-server you may run into issues with content security -policy if the marketplace runs on a different domain over HTTP; in this case you -will need to disable content security policy in your browser or manually edit -the policy in code-server's source. +Make sure your bundle contains the full certificate chain. This can be necessary +because Node does not read system certificates by default and while VS Code has +code for reading them, it appears not to work or be enabled for the web version. -When you make a change that affects people deploying the marketplace please -update the changelog as part of your PR. +Some so-called "web" extensions (like `vscodevim.vim`) are installed in the +browser, and extension searches are also performed from the browser, so your +certificate bundle may also need to be installed on the client machine in +addition to the remote machine. -You can use `make gen` to generate a mock `extensions` directory for testing and -`make upload` to upload them to an Artifactory repository. +## Usage in VS Code & VSCodium -### Tests +Although not officially supported, you can follow the examples below to start +using code-marketplace with VS Code and VSCodium: -To run the tests: +- [VS Code](https://github.com/eclipse/openvsx/wiki/Using-Open-VSX-in-VS-Code) -``` -make test -``` + Extension signing may have to be disabled in VS Code. -To run the Artifactory tests against a real repository instead of a mock: +- [VSCodium](https://github.com/VSCodium/vscodium/blob/master/docs/index.md#howto-switch-marketplace) -``` -export ARTIFACTORY_URI=myuri -export ARTIFACTORY_REPO=myrepo -export ARTIFACTORY_TOKEN=mytoken -make test -``` + ``` + export VSCODE_GALLERY_SERVICE_URL="https:///api + export VSCODE_GALLERY_ITEM_URL="https:///item" + # Or set a product.json file in `~/.config/VSCodium/product.json` + codium + ``` ## Missing features @@ -190,7 +241,6 @@ make test ## Planned work -- Bulk add. - Bulk add from one Artifactory repository to another (or to itself). - Optional database to speed up queries. - Progress indicators when adding/removing extensions. diff --git a/api/api.go b/api/api.go index fa369c8..50f07c5 100644 --- a/api/api.go +++ b/api/api.go @@ -4,10 +4,10 @@ import ( "encoding/json" "net/http" "os" + "strconv" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" - "github.com/go-chi/cors" "cdr.dev/slog" "github.com/coder/code-marketplace/api/httpapi" @@ -16,6 +16,8 @@ import ( "github.com/coder/code-marketplace/storage" ) +const MaxPageSizeDefault int = 200 + // QueryRequest implements an untyped object. It is the data sent to the API to // query for extensions. // https://github.com/microsoft/vscode/blob/a69f95fdf3dc27511517eef5ff62b21c7a418015/src/vs/platform/extensionManagement/common/extensionGalleryService.ts#L338-L342 @@ -56,14 +58,16 @@ type Options struct { Database database.Database Logger slog.Logger // Set to <0 to disable. - RateLimit int - Storage storage.Storage + RateLimit int + Storage storage.Storage + MaxPageSize int } type API struct { - Database database.Database - Handler http.Handler - Logger slog.Logger + Database database.Database + Handler http.Handler + Logger slog.Logger + MaxPageSize int } // New creates a new API server. @@ -72,18 +76,14 @@ func New(options *Options) *API { options.RateLimit = 512 } - r := chi.NewRouter() + if options.MaxPageSize == 0 { + options.MaxPageSize = MaxPageSizeDefault + } - cors := cors.New(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"POST", "GET", "OPTIONS"}, - AllowedHeaders: []string{"*"}, - AllowCredentials: true, - MaxAge: 300, - }) + r := chi.NewRouter() r.Use( - cors.Handler, + httpmw.Cors(), httpmw.RateLimitPerMinute(options.RateLimit), middleware.GetHead, httpmw.AttachRequestID, @@ -93,9 +93,10 @@ func New(options *Options) *API { ) api := &API{ - Database: options.Database, - Handler: r, - Logger: options.Logger, + Database: options.Database, + Handler: r, + Logger: options.Logger, + MaxPageSize: options.MaxPageSize, } r.Get("/", func(rw http.ResponseWriter, r *http.Request) { @@ -119,8 +120,13 @@ func New(options *Options) *API { r.Get("/assets/{publisher}/{extension}/{version}/{type}", api.assetRedirect) // This is the "download manually" URL, which like /assets is hardcoded and - // ignores the VSIX asset URL provided to VS Code in the response. + // ignores the VSIX asset URL provided to VS Code in the response. We provide + // it at /publishers for backwards compatibility since that is where we + // originally had it, but VS Code appends to the service URL which means the + // path VS Code actually uses is /api/publishers. + // https://github.com/microsoft/vscode/blob/c727b5484ebfbeff1e1d29654cae5c17af1c826f/build/lib/extensions.ts#L228 r.Get("/publishers/{publisher}/vsextensions/{extension}/{version}/{type}", api.assetRedirect) + r.Get("/api/publishers/{publisher}/vsextensions/{extension}/{version}/{type}", api.assetRedirect) // This is the URL you get taken to when you click the extension's names, // ratings, etc from the extension details page. @@ -172,10 +178,10 @@ func (api *API) extensionQuery(rw http.ResponseWriter, r *http.Request) { }) } for _, filter := range query.Filters { - if filter.PageSize < 0 || filter.PageSize > 50 { + if filter.PageSize < 0 || filter.PageSize > api.MaxPageSize { httpapi.Write(rw, http.StatusBadRequest, httpapi.ErrorResponse{ - Message: "Invalid page size", - Detail: "Check that the page size is between zero and fifty", + Message: "The page size must be between 0 and " + strconv.Itoa(api.MaxPageSize), + Detail: "Contact an administrator to increase the page size", RequestID: httpmw.RequestID(r), }) } @@ -217,17 +223,20 @@ func (api *API) extensionQuery(rw http.ResponseWriter, r *http.Request) { } func (api *API) assetRedirect(rw http.ResponseWriter, r *http.Request) { - // TODO: Asset URIs can contain a targetPlatform query variable. baseURL := httpapi.RequestBaseURL(r, "/") assetType := storage.AssetType(chi.URLParam(r, "type")) if assetType == "vspackage" { assetType = storage.VSIXAssetType } + version := storage.VersionFromString(chi.URLParam(r, "version")) + if version.TargetPlatform == "" { + version.TargetPlatform = storage.Platform(r.URL.Query().Get("targetPlatform")) + } url, err := api.Database.GetExtensionAssetPath(r.Context(), &database.Asset{ Extension: chi.URLParam(r, "extension"), Publisher: chi.URLParam(r, "publisher"), Type: assetType, - Version: chi.URLParam(r, "version"), + Version: version, }, baseURL) if err != nil && os.IsNotExist(err) { httpapi.Write(rw, http.StatusNotFound, httpapi.ErrorResponse{ diff --git a/api/api_test.go b/api/api_test.go index f5941f0..86bd1ab 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -36,6 +36,7 @@ func TestAPI(t *testing.T) { Request any Response any Status int + Method string }{ { Name: "Root", @@ -119,8 +120,8 @@ func TestAPI(t *testing.T) { }}, }, Response: &httpapi.ErrorResponse{ - Message: "Invalid page size", - Detail: "Check that the page size is between zero and fifty", + Message: "The page size must be between 0 and 200", + Detail: "Contact an administrator to increase the page size", }, }, { @@ -170,7 +171,7 @@ func TestAPI(t *testing.T) { Response: "foobar", }, { - Name: "FileAPI", + Name: "FileAPINotExists", Path: "/files/nonexistent", Status: http.StatusNotFound, }, @@ -198,6 +199,25 @@ func TestAPI(t *testing.T) { Status: http.StatusMovedPermanently, Response: "/files/publisher/extension/version/foo", }, + { + Name: "AssetOKPlatform", + Path: "/assets/publisher/extension/version@linux-x64/type", + Status: http.StatusMovedPermanently, + Response: "/files/publisher/extension/version@linux-x64/foo", + }, + { + Name: "AssetOKPlatformQuery", + Path: "/assets/publisher/extension/version/type?targetPlatform=linux-x64", + Status: http.StatusMovedPermanently, + Response: "/files/publisher/extension/version@linux-x64/foo", + }, + { + Name: "AssetOKDuplicatedPlatformQuery", + Path: "/assets/publisher/extension/version@darwin-x64/type?targetPlatform=linux-x64", + Status: http.StatusMovedPermanently, + Response: "/files/publisher/extension/version@darwin-x64/foo", + }, + // Old vspackage path, for backwards compatibility. { Name: "DownloadNotExist", Path: "/publishers/notexist/vsextensions/extension/version/vspackage", @@ -213,6 +233,24 @@ func TestAPI(t *testing.T) { Status: http.StatusMovedPermanently, Response: "/files/publisher/extension/version/extension.vsix", }, + // The vspackage path currently generated by VS Code. + { + Name: "APIDownloadNotExist", + Path: "/api/publishers/notexist/vsextensions/extension/version/vspackage", + Status: http.StatusNotFound, + Response: &httpapi.ErrorResponse{ + Message: "Extension asset does not exist", + Detail: "Please check the asset path", + }, + Method: http.MethodGet, + }, + { + Name: "APIDownloadOK", + Path: "/api/publishers/publisher/vsextensions/extension/version/vspackage", + Status: http.StatusMovedPermanently, + Response: "/files/publisher/extension/version/extension.vsix", + Method: http.MethodGet, + }, { Name: "Item", Path: "/item", @@ -237,9 +275,10 @@ func TestAPI(t *testing.T) { logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) apiServer := api.New(&api.Options{ - Database: testutil.NewMockDB(exts), - Storage: testutil.NewMockStorage(), - Logger: logger, + Database: testutil.NewMockDB(exts), + Storage: testutil.NewMockStorage(), + Logger: logger, + MaxPageSize: api.MaxPageSizeDefault, }) server := httptest.NewServer(apiServer.Handler) @@ -254,9 +293,20 @@ func TestAPI(t *testing.T) { }, } + // Most /api calls are POSTs, the rest are GETs. + var method = c.Method + if method == "" { + if strings.HasPrefix(c.Path, "/api") { + method = http.MethodPost + } else { + method = http.MethodGet + } + } + var resp *http.Response var err error - if strings.HasPrefix(c.Path, "/api") { + switch method { + case http.MethodPost: var body []byte if str, ok := c.Request.(string); ok { body = []byte(str) @@ -265,8 +315,10 @@ func TestAPI(t *testing.T) { require.NoError(t, err) } resp, err = client.Post(url, "application/json", bytes.NewReader(body)) - } else { + case http.MethodGet: resp, err = client.Get(url) + default: + t.Fatal(method + " is not handled in the test yet, please add it now") } require.NoError(t, err) require.Equal(t, c.Status, resp.StatusCode) diff --git a/api/httpmw/cors.go b/api/httpmw/cors.go new file mode 100644 index 0000000..e0a2469 --- /dev/null +++ b/api/httpmw/cors.go @@ -0,0 +1,38 @@ +package httpmw + +import ( + "net/http" + + "github.com/go-chi/cors" +) + +const ( + // Server headers. + AccessControlAllowOriginHeader = "Access-Control-Allow-Origin" + AccessControlAllowCredentialsHeader = "Access-Control-Allow-Credentials" + AccessControlAllowMethodsHeader = "Access-Control-Allow-Methods" + AccessControlAllowHeadersHeader = "Access-Control-Allow-Headers" + VaryHeader = "Vary" + + // Client headers. + OriginHeader = "Origin" + AccessControlRequestMethodHeader = "Access-Control-Request-Method" + AccessControlRequestHeadersHeader = "Access-Control-Request-Headers" +) + +func Cors() func(next http.Handler) http.Handler { + return cors.Handler(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowedHeaders: []string{"*"}, + AllowCredentials: true, + MaxAge: 300, + }) +} diff --git a/api/httpmw/cors_test.go b/api/httpmw/cors_test.go new file mode 100644 index 0000000..37aeba1 --- /dev/null +++ b/api/httpmw/cors_test.go @@ -0,0 +1,127 @@ +package httpmw_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/code-marketplace/api/httpmw" +) + +func TestCors(t *testing.T) { + t.Parallel() + + methods := []string{ + http.MethodOptions, + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + } + + tests := []struct { + name string + origin string + allowedOrigin string + headers string + allowedHeaders string + }{ + { + name: "LocalHTTP", + origin: "http://localhost:3000", + allowedOrigin: "*", + }, + { + name: "LocalHTTPS", + origin: "https://localhost:3000", + allowedOrigin: "*", + }, + { + name: "HTTP", + origin: "http://code-server.domain.tld", + allowedOrigin: "*", + }, + { + name: "HTTPS", + origin: "https://code-server.domain.tld", + allowedOrigin: "*", + }, + { + // VS Code appears to use this origin. + name: "VSCode", + origin: "vscode-file://vscode-app", + allowedOrigin: "*", + }, + { + name: "NoOrigin", + allowedOrigin: "", + }, + { + name: "Headers", + origin: "foobar", + allowedOrigin: "*", + headers: "X-TEST,X-TEST2", + allowedHeaders: "X-Test, X-Test2", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + for _, method := range methods { + method := method + t.Run(method, func(t *testing.T) { + t.Parallel() + + r := httptest.NewRequest(method, "http://dev.coder.com", nil) + if test.origin != "" { + r.Header.Set(httpmw.OriginHeader, test.origin) + } + + // OPTIONS requests need to know what method will be requested, or + // go-chi/cors will error. Both request headers and methods should be + // ignored for regular requests even if they are set, although that is + // not tested here. + if method == http.MethodOptions { + r.Header.Set(httpmw.AccessControlRequestMethodHeader, http.MethodGet) + if test.headers != "" { + r.Header.Set(httpmw.AccessControlRequestHeadersHeader, test.headers) + } + } + + rw := httptest.NewRecorder() + handler := httpmw.Cors()(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusNoContent) + })) + handler.ServeHTTP(rw, r) + + // Should always set some kind of allowed origin, if allowed. + require.Equal(t, test.allowedOrigin, rw.Header().Get(httpmw.AccessControlAllowOriginHeader)) + + // OPTIONS should echo back the request method and headers (if there + // is an origin header set) and we should never get to our handler as + // the middleware short-circuits with a 200. + if method == http.MethodOptions && test.origin == "" { + require.Equal(t, "", rw.Header().Get(httpmw.AccessControlAllowMethodsHeader)) + require.Equal(t, "", rw.Header().Get(httpmw.AccessControlAllowHeadersHeader)) + require.Equal(t, http.StatusOK, rw.Code) + } else if method == http.MethodOptions { + require.Equal(t, http.MethodGet, rw.Header().Get(httpmw.AccessControlAllowMethodsHeader)) + require.Equal(t, test.allowedHeaders, rw.Header().Get(httpmw.AccessControlAllowHeadersHeader)) + require.Equal(t, http.StatusOK, rw.Code) + } else { + require.Equal(t, "", rw.Header().Get(httpmw.AccessControlAllowMethodsHeader)) + require.Equal(t, "", rw.Header().Get(httpmw.AccessControlAllowHeadersHeader)) + require.Equal(t, http.StatusNoContent, rw.Code) + } + }) + } + }) + } +} diff --git a/api/httpmw/logger.go b/api/httpmw/logger.go index c3ecb51..28d500f 100644 --- a/api/httpmw/logger.go +++ b/api/httpmw/logger.go @@ -15,6 +15,7 @@ func Logger(log slog.Logger) func(next http.Handler) http.Handler { sw := &httpapi.StatusWriter{ResponseWriter: w} httplog := log.With( + slog.F("host", r.Host), slog.F("path", r.URL.Path), slog.F("remote_addr", r.RemoteAddr), slog.F("client_id", r.Header.Get("x-market-client-id")), diff --git a/cli/add.go b/cli/add.go index eb85243..1bf17d4 100644 --- a/cli/add.go +++ b/cli/add.go @@ -10,20 +10,12 @@ import ( "github.com/spf13/cobra" "golang.org/x/xerrors" - "cdr.dev/slog" - "cdr.dev/slog/sloggers/sloghuman" - "github.com/coder/code-marketplace/storage" "github.com/coder/code-marketplace/util" ) func add() *cobra.Command { - var ( - artifactory string - extdir string - repo string - ) - + addFlags, opts := serverFlags() cmd := &cobra.Command{ Use: "add ", Short: "Add an extension to the marketplace", @@ -37,33 +29,23 @@ func add() *cobra.Command { ctx, cancel := context.WithCancel(cmd.Context()) defer cancel() - verbose, err := cmd.Flags().GetBool("verbose") - if err != nil { - return err - } - logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr())) - if verbose { - logger = logger.Leveled(slog.LevelDebug) - } - - store, err := storage.NewStorage(ctx, &storage.Options{ - Artifactory: artifactory, - ExtDir: extdir, - Logger: logger, - Repo: repo, - }) + store, err := storage.NewStorage(ctx, opts) if err != nil { return err } - stat, err := os.Stat(args[0]) - if err != nil { - return err + // The source might be a local directory with extensions. + isDir := false + if !strings.HasPrefix(args[0], "http://") && !strings.HasPrefix(args[0], "https://") { + stat, err := os.Stat(args[0]) + if err != nil { + return err + } + isDir = stat.IsDir() } - var summary []string var failed []string - if stat.IsDir() { + if isDir { files, err := os.ReadDir(args[0]) if err != nil { return err @@ -71,34 +53,30 @@ func add() *cobra.Command { for _, file := range files { s, err := doAdd(ctx, filepath.Join(args[0], file.Name()), store) if err != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Failed to unpack %s: %s\n", file.Name(), err.Error()) failed = append(failed, file.Name()) - summary = append(summary, fmt.Sprintf("Failed to unpack %s: %s", file.Name(), err.Error())) } else { - summary = append(summary, s...) + _, _ = fmt.Fprintln(cmd.OutOrStdout(), strings.Join(s, "\n")) } } } else { - summary, err = doAdd(ctx, args[0], store) + s, err := doAdd(ctx, args[0], store) if err != nil { return err } + _, _ = fmt.Fprintln(cmd.OutOrStdout(), strings.Join(s, "\n")) } - _, err = fmt.Fprintln(cmd.OutOrStdout(), strings.Join(summary, "\n")) - failedCount := len(failed) - if failedCount > 0 { + if len(failed) > 0 { return xerrors.Errorf( "Failed to add %s: %s", - util.Plural(failedCount, "extension", ""), + util.Plural(len(failed), "extension", ""), strings.Join(failed, ", ")) } - return err + return nil }, } - - cmd.Flags().StringVar(&extdir, "extensions-dir", "", "The path to extensions.") - cmd.Flags().StringVar(&artifactory, "artifactory", "", "Artifactory server URL.") - cmd.Flags().StringVar(&repo, "repo", "", "Artifactory repository.") + addFlags(cmd) return cmd } diff --git a/cli/add_test.go b/cli/add_test.go index 97b90a3..8349abb 100644 --- a/cli/add_test.go +++ b/cli/add_test.go @@ -3,6 +3,8 @@ package cli_test import ( "bytes" "fmt" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" @@ -39,6 +41,8 @@ func TestAdd(t *testing.T) { extensions []testutil.Extension // name is the name of the test. name string + // platforms to add for the latest version of each extension. + platforms []storage.Platform // vsixes contains raw bytes of extensions to add. Use for failure cases. vsixes [][]byte }{ @@ -46,10 +50,21 @@ func TestAdd(t *testing.T) { name: "OK", extensions: []testutil.Extension{testutil.Extensions[0]}, }, + { + name: "OKPlatforms", + extensions: []testutil.Extension{testutil.Extensions[0]}, + platforms: []storage.Platform{ + storage.PlatformUnknown, + storage.PlatformWin32X64, + storage.PlatformLinuxX64, + storage.PlatformDarwinX64, + storage.PlatformWeb, + }, + }, { name: "InvalidVSIX", error: "not a valid zip", - vsixes: [][]byte{[]byte{}}, + vsixes: [][]byte{{}}, }, { name: "BulkOK", @@ -70,7 +85,7 @@ func TestAdd(t *testing.T) { testutil.Extensions[3], }, vsixes: [][]byte{ - []byte{}, + {}, []byte("foo"), }, }, @@ -93,46 +108,72 @@ func TestAdd(t *testing.T) { create(vsix) } for _, ext := range test.extensions { - create(testutil.CreateVSIXFromExtension(t, ext)) + if len(test.platforms) > 0 { + for _, platform := range test.platforms { + create(testutil.CreateVSIXFromExtension(t, ext, storage.Version{Version: ext.LatestVersion, TargetPlatform: platform})) + } + } else { + create(testutil.CreateVSIXFromExtension(t, ext, storage.Version{Version: ext.LatestVersion})) + } } // With multiple extensions use bulk add by pointing to the directory - // otherwise point to the vsix file. - source := extdir + // otherwise point to the vsix file. When not using bulk add also test + // from HTTP. + sources := []string{extdir} if count == 1 { - source = filepath.Join(extdir, "0.vsix") + sources = []string{filepath.Join(extdir, "0.vsix")} + + handler := func(rw http.ResponseWriter, r *http.Request) { + var vsix []byte + if test.vsixes == nil { + vsix = testutil.CreateVSIXFromExtension(t, test.extensions[0], storage.Version{Version: test.extensions[0].LatestVersion}) + } else { + vsix = test.vsixes[0] + } + _, err := rw.Write(vsix) + require.NoError(t, err) + } + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + + sources = append(sources, server.URL) } - cmd := cli.Root() - args := []string{"add", source, "--extensions-dir", extdir} - cmd.SetArgs(args) - buf := new(bytes.Buffer) - cmd.SetOutput(buf) + for _, source := range sources { + cmd := cli.Root() + args := []string{"add", source, "--extensions-dir", extdir} + cmd.SetArgs(args) + buf := new(bytes.Buffer) + cmd.SetOut(buf) - err := cmd.Execute() - output := buf.String() + err := cmd.Execute() + output := buf.String() - if test.error != "" { - require.Error(t, err) - require.Regexp(t, test.error, err.Error()) - } else { - require.NoError(t, err) - } - // Should list all the extensions that worked. - for _, ext := range test.extensions { - // Should exist on disk. - dest := filepath.Join(extdir, ext.Publisher, ext.Name, ext.LatestVersion) - _, err := os.Stat(dest) - require.NoError(t, err) - // Should tell you where it went. - id := storage.ExtensionID(ext.Publisher, ext.Name, ext.LatestVersion) - require.Contains(t, output, fmt.Sprintf("Unpacked %s to %s", id, dest)) - // Should mention the dependencies and pack. - require.Contains(t, output, fmt.Sprintf("%s has %d dep", id, len(ext.Dependencies))) - if len(ext.Pack) > 0 { - require.Contains(t, output, fmt.Sprintf("%s is in a pack with %d other", id, len(ext.Pack))) + if test.error != "" { + require.Error(t, err) + require.Regexp(t, test.error, err.Error()) } else { - require.Contains(t, output, fmt.Sprintf("%s is not in a pack", id)) + require.NoError(t, err) + require.NotContains(t, output, "Failed to add") + } + + // Should list all the extensions that worked. + for _, ext := range test.extensions { + // Should exist on disk. + dest := filepath.Join(extdir, ext.Publisher, ext.Name, ext.LatestVersion) + _, err := os.Stat(dest) + require.NoError(t, err) + // Should tell you where it went. + id := storage.ExtensionID(ext.Publisher, ext.Name, ext.LatestVersion) + require.Contains(t, output, fmt.Sprintf("Unpacked %s to %s", id, dest)) + // Should mention the dependencies and pack. + require.Contains(t, output, fmt.Sprintf("%s has %d dep", id, len(ext.Dependencies))) + if len(ext.Pack) > 0 { + require.Contains(t, output, fmt.Sprintf("%s is in a pack with %d other", id, len(ext.Pack))) + } else { + require.Contains(t, output, fmt.Sprintf("%s is not in a pack", id)) + } } } }) diff --git a/cli/remove.go b/cli/remove.go index 1c52df7..45aa686 100644 --- a/cli/remove.go +++ b/cli/remove.go @@ -10,26 +10,21 @@ import ( "github.com/spf13/cobra" "golang.org/x/xerrors" - "cdr.dev/slog" - "cdr.dev/slog/sloggers/sloghuman" - "github.com/coder/code-marketplace/storage" "github.com/coder/code-marketplace/util" ) func remove() *cobra.Command { var ( - all bool - artifactory string - extdir string - repo string + all bool ) + addFlags, opts := serverFlags() cmd := &cobra.Command{ Use: "remove ", Short: "Remove an extension from the marketplace", Example: strings.Join([]string{ - " marketplace remove publisher.extension-1.0.0 --extensions-dir ./extensions", + " marketplace remove publisher.extension@1.0.0 --extensions-dir ./extensions", " marketplace remove publisher.extension --all --artifactory http://artifactory.server/artifactory --repo extensions", }, "\n"), Args: cobra.ExactArgs(1), @@ -37,32 +32,19 @@ func remove() *cobra.Command { ctx, cancel := context.WithCancel(cmd.Context()) defer cancel() - verbose, err := cmd.Flags().GetBool("verbose") + store, err := storage.NewStorage(ctx, opts) if err != nil { return err } - logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr())) - if verbose { - logger = logger.Leveled(slog.LevelDebug) - } - store, err := storage.NewStorage(ctx, &storage.Options{ - Artifactory: artifactory, - ExtDir: extdir, - Logger: logger, - Repo: repo, - }) + targetId := args[0] + publisher, name, versionStr, err := storage.ParseExtensionID(targetId) if err != nil { return err } - id := args[0] - publisher, name, version, err := storage.ParseExtensionID(id) - if err != nil { - return err - } - - if version != "" && all { + version := storage.Version{Version: versionStr} + if version.Version != "" && all { return xerrors.Errorf("cannot specify both --all and version %s", version) } @@ -72,43 +54,54 @@ func remove() *cobra.Command { } versionCount := len(allVersions) - if !all && version != "" && !util.Contains(allVersions, version) { - return xerrors.Errorf("%s does not exist", id) - } else if versionCount == 0 { - return xerrors.Errorf("%s.%s has no versions to delete", publisher, name) - } else if version == "" && !all { + if version.Version == "" && !all { return xerrors.Errorf( - "use %s- to target a specific version or pass --all to delete %s of %s", - id, + "use %s@ to target a specific version or pass --all to delete %s of %s", + targetId, util.Plural(versionCount, "version", ""), - id, + targetId, ) } - err = store.RemoveExtension(ctx, publisher, name, version) - if err != nil { - return err - } - summary := []string{} + // TODO: Allow deleting by platform as well? + var toDelete []storage.Version if all { - removedCount := len(allVersions) - summary = append(summary, fmt.Sprintf("Removed %s", util.Plural(removedCount, "version", ""))) - for _, version := range allVersions { - summary = append(summary, fmt.Sprintf(" - %s", version)) - } + toDelete = allVersions } else { - summary = append(summary, fmt.Sprintf("Removed %s", version)) + for _, sv := range allVersions { + if version.Version == sv.Version { + toDelete = append(toDelete, sv) + } + } + } + if len(toDelete) == 0 { + return xerrors.Errorf("%s does not exist", targetId) + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Removing %s...\n", util.Plural(len(toDelete), "version", "")) + var failed []string + for _, delete := range toDelete { + err = store.RemoveExtension(ctx, publisher, name, delete) + if err != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), " - %s (%s)\n", delete, err) + failed = append(failed, delete.String()) + } else { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), " - %s\n", delete) + } } - _, err = fmt.Fprintln(cmd.OutOrStdout(), strings.Join(summary, "\n")) - return err + if len(failed) > 0 { + return xerrors.Errorf( + "Failed to remove %s: %s", + util.Plural(len(failed), "version", ""), + strings.Join(failed, ", ")) + } + return nil }, } cmd.Flags().BoolVar(&all, "all", false, "Whether to delete all versions of the extension.") - cmd.Flags().StringVar(&extdir, "extensions-dir", "", "The path to extensions.") - cmd.Flags().StringVar(&artifactory, "artifactory", "", "Artifactory server URL.") - cmd.Flags().StringVar(&repo, "repo", "", "Artifactory repository.") + addFlags(cmd) return cmd } diff --git a/cli/remove_test.go b/cli/remove_test.go index 90c1e7f..b9b3770 100644 --- a/cli/remove_test.go +++ b/cli/remove_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/code-marketplace/cli" + "github.com/coder/code-marketplace/storage" "github.com/coder/code-marketplace/testutil" ) @@ -34,10 +35,12 @@ func TestRemove(t *testing.T) { tests := []struct { // all means to pass --all. all bool - // error is the expected error. + // error is the expected error, if any. error string - // extension is the extension to remove. testutil.Extensions[0] will be - // added with versions a, b, and c before each test. + // expected contains the versions should have been deleted, if any. + expected []storage.Version + // extension is the extension to remove. Every version of + // testutil.Extensions[0] will be added before each test. extension testutil.Extension // name is the name of the test. name string @@ -47,12 +50,41 @@ func TestRemove(t *testing.T) { { name: "RemoveOne", extension: testutil.Extensions[0], + version: "2.0.0", + expected: []storage.Version{ + {Version: "2.0.0"}, + }, + }, + { + name: "RemovePlatforms", + extension: testutil.Extensions[0], version: testutil.Extensions[0].LatestVersion, + expected: []storage.Version{ + {Version: "3.0.0"}, + {Version: "3.0.0", TargetPlatform: storage.PlatformAlpineX64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformDarwinX64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformLinuxArm64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformLinuxX64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformWin32X64}, + }, }, { name: "All", extension: testutil.Extensions[0], all: true, + expected: []storage.Version{ + {Version: "1.0.0"}, + {Version: "1.0.0", TargetPlatform: storage.PlatformWin32X64}, + {Version: "1.5.2"}, + {Version: "2.0.0"}, + {Version: "2.2.2"}, + {Version: "3.0.0"}, + {Version: "3.0.0", TargetPlatform: storage.PlatformAlpineX64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformDarwinX64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformLinuxArm64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformLinuxX64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformWin32X64}, + }, }, { name: "MissingTarget", @@ -61,7 +93,7 @@ func TestRemove(t *testing.T) { }, { name: "MissingTargetNoVersions", - error: "has no versions", + error: "target a specific version or pass --all", extension: testutil.Extensions[1], }, { @@ -85,10 +117,19 @@ func TestRemove(t *testing.T) { }, { name: "AllNoVersions", - error: "has no versions", + error: "does not exist", extension: testutil.Extensions[1], all: true, }, + { + // Cannot target specific platforms at the moment. If we wanted this + // we would likely need to use a `--platform` flag since we already use @ + // to delineate the version. + name: "NoPlatformTarget", + error: "does not exist", + extension: testutil.Extensions[0], + version: "1.0.0@win32-x64", + }, } for _, test := range tests { @@ -99,7 +140,7 @@ func TestRemove(t *testing.T) { extdir := t.TempDir() ext := testutil.Extensions[0] for _, version := range ext.Versions { - manifestPath := filepath.Join(extdir, ext.Publisher, ext.Name, version, "extension.vsixmanifest") + manifestPath := filepath.Join(extdir, ext.Publisher, ext.Name, version.String(), "extension.vsixmanifest") err := os.MkdirAll(filepath.Dir(manifestPath), 0o755) require.NoError(t, err) err = os.WriteFile(manifestPath, testutil.ConvertExtensionToManifestBytes(t, ext, version), 0o644) @@ -108,7 +149,7 @@ func TestRemove(t *testing.T) { id := fmt.Sprintf("%s.%s", test.extension.Publisher, test.extension.Name) if test.version != "" { - id = fmt.Sprintf("%s-%s", id, test.version) + id = fmt.Sprintf("%s@%s", id, test.version) } cmd := cli.Root() @@ -118,7 +159,7 @@ func TestRemove(t *testing.T) { } cmd.SetArgs(args) buf := new(bytes.Buffer) - cmd.SetOutput(buf) + cmd.SetOut(buf) err := cmd.Execute() output := buf.String() @@ -128,13 +169,18 @@ func TestRemove(t *testing.T) { require.Regexp(t, test.error, err.Error()) } else { require.NoError(t, err) - if test.all { - require.Contains(t, output, fmt.Sprintf("Removed %d versions", len(test.extension.Versions))) - for _, version := range test.extension.Versions { - require.Contains(t, output, fmt.Sprintf(" - %s", version)) - } - } else { - require.Contains(t, output, fmt.Sprintf("Removed %s", test.version)) + require.NotContains(t, output, "Failed to remove") + } + + // Should list all the extensions that were able to be removed. + if len(test.expected) > 0 { + require.Contains(t, output, fmt.Sprintf("Removing %d version", len(test.expected))) + for _, version := range test.expected { + // Should not exist on disk. + dest := filepath.Join(extdir, test.extension.Publisher, test.extension.Name, version.String()) + _, err := os.Stat(dest) + require.Error(t, err) + require.Contains(t, output, fmt.Sprintf(" - %s\n", version)) } } }) diff --git a/cli/root.go b/cli/root.go index 53347d8..b6638c3 100644 --- a/cli/root.go +++ b/cli/root.go @@ -1,22 +1,23 @@ package cli import ( - "github.com/spf13/cobra" "strings" + + "github.com/spf13/cobra" ) func Root() *cobra.Command { cmd := &cobra.Command{ - Use: "marketplace", + Use: "code-marketplace", SilenceErrors: true, SilenceUsage: true, Long: "Code extension marketplace", Example: strings.Join([]string{ - " marketplace server --extensions-dir ./extensions", + " code-marketplace server --extensions-dir ./extensions", }, "\n"), } - cmd.AddCommand(add(), remove(), server(), version()) + cmd.AddCommand(add(), remove(), server(), version(), signature()) cmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose output") diff --git a/cli/server.go b/cli/server.go index a77b5ee..e6bf344 100644 --- a/cli/server.go +++ b/cli/server.go @@ -15,19 +15,61 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" - "github.com/coder/code-marketplace/api" "github.com/coder/code-marketplace/database" "github.com/coder/code-marketplace/storage" ) +func serverFlags() (addFlags func(cmd *cobra.Command), opts *storage.Options) { + opts = &storage.Options{} + return func(cmd *cobra.Command) { + cmd.Flags().StringVar(&opts.ExtDir, "extensions-dir", "", "The path to extensions.") + cmd.Flags().StringVar(&opts.Artifactory, "artifactory", "", "Artifactory server URL.") + cmd.Flags().StringVar(&opts.Repo, "repo", "", "Artifactory repository.") + + if cmd.Use == "server" { + // Server only flags + cmd.Flags().BoolVar(&opts.IncludeEmptySignatures, "sign", false, "Includes an empty signature for all extensions.") + cmd.Flags().DurationVar(&opts.ListCacheDuration, "list-cache-duration", time.Minute, "The duration of the extension cache.") + } + + var before func(cmd *cobra.Command, args []string) error + if cmd.PreRunE != nil { + before = cmd.PreRunE + } + if cmd.PreRun != nil { + beforeNoE := cmd.PreRun + before = func(cmd *cobra.Command, args []string) error { + beforeNoE(cmd, args) + return nil + } + } + + cmd.PreRunE = func(cmd *cobra.Command, args []string) error { + opts.Logger = cmdLogger(cmd) + if before != nil { + return before(cmd, args) + } + return nil + } + }, opts +} + +func cmdLogger(cmd *cobra.Command) slog.Logger { + verbose, _ := cmd.Flags().GetBool("verbose") + logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr())) + if verbose { + logger = logger.Leveled(slog.LevelDebug) + } + return logger +} + func server() *cobra.Command { var ( address string - artifactory string - extdir string - repo string + maxpagesize int ) + addFlags, opts := serverFlags() cmd := &cobra.Command{ Use: "server", @@ -39,25 +81,12 @@ func server() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithCancel(cmd.Context()) defer cancel() + logger := opts.Logger notifyCtx, notifyStop := signal.NotifyContext(ctx, interruptSignals...) defer notifyStop() - verbose, err := cmd.Flags().GetBool("verbose") - if err != nil { - return err - } - logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr())) - if verbose { - logger = logger.Leveled(slog.LevelDebug) - } - - store, err := storage.NewStorage(ctx, &storage.Options{ - Artifactory: artifactory, - ExtDir: extdir, - Logger: logger, - Repo: repo, - }) + store, err := storage.NewStorage(ctx, opts) if err != nil { return err } @@ -83,9 +112,10 @@ func server() *cobra.Command { // Start the API server. mapi := api.New(&api.Options{ - Database: database, - Storage: store, - Logger: logger, + Database: database, + Storage: store, + Logger: logger, + MaxPageSize: maxpagesize, }) server := &http.Server{ Handler: mapi.Handler, @@ -133,10 +163,9 @@ func server() *cobra.Command { }, } - cmd.Flags().StringVar(&extdir, "extensions-dir", "", "The path to extensions.") - cmd.Flags().StringVar(&artifactory, "artifactory", "", "Artifactory server URL.") - cmd.Flags().StringVar(&repo, "repo", "", "Artifactory repository.") + cmd.Flags().IntVar(&maxpagesize, "max-page-size", api.MaxPageSizeDefault, "The maximum number of pages to request") cmd.Flags().StringVar(&address, "address", "127.0.0.1:3001", "The address on which to serve the marketplace API.") + addFlags(cmd) return cmd } diff --git a/cli/signature.go b/cli/signature.go new file mode 100644 index 0000000..432a14a --- /dev/null +++ b/cli/signature.go @@ -0,0 +1,63 @@ +package cli + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "golang.org/x/xerrors" + + "github.com/coder/code-marketplace/extensionsign" +) + +func signature() *cobra.Command { + cmd := &cobra.Command{ + Use: "signature", + Short: "Commands for debugging and working with signatures.", + Hidden: true, // Debugging tools + Aliases: []string{"sig", "sigs", "signatures"}, + } + cmd.AddCommand(compareSignatureSigZips()) + return cmd +} + +func compareSignatureSigZips() *cobra.Command { + cmd := &cobra.Command{ + Use: "compare", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + decode := func(path string) (extensionsign.SignatureManifest, error) { + data, err := os.ReadFile(path) + if err != nil { + return extensionsign.SignatureManifest{}, xerrors.Errorf("read %q: %w", args[0], err) + } + + sig, err := extensionsign.ExtractSignatureManifest(data) + if err != nil { + return extensionsign.SignatureManifest{}, xerrors.Errorf("unmarshal %q: %w", path, err) + } + return sig, nil + } + + a, err := decode(args[0]) + if err != nil { + return err + } + b, err := decode(args[1]) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(os.Stdout, "Signature A:%s\n", a) + _, _ = fmt.Fprintf(os.Stdout, "Signature B:%s\n", b) + err = a.Equal(b) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(os.Stdout, "Signatures are equal\n") + return nil + }, + } + return cmd +} diff --git a/database/database.go b/database/database.go index ed84160..e164495 100644 --- a/database/database.go +++ b/database/database.go @@ -123,13 +123,12 @@ type ExtPublisher struct { // ExtVersion implements IRawGalleryExtensionVersion. // https://github.com/microsoft/vscode/blob/29234f0219bdbf649d6107b18651a1038d6357ac/src/vs/platform/extensionManagement/common/extensionGalleryService.ts#L42-L50 type ExtVersion struct { - Version string `json:"version"` + storage.Version LastUpdated time.Time `json:"lastUpdated"` AssetURI string `json:"assetUri"` FallbackAssetURI string `json:"fallbackAssetUri"` Files []ExtFile `json:"files"` Properties []ExtProperty `json:"properties,omitempty"` - TargetPlatform string `json:"targetPlatform,omitempty"` } // ExtFile implements IRawGalleryExtensionFile. @@ -157,7 +156,7 @@ type Asset struct { Extension string Publisher string Type storage.AssetType - Version string + Version storage.Version } type Database interface { diff --git a/database/database_test.go b/database/database_test.go index fbb4405..74e2d5d 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -11,6 +11,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/code-marketplace/database" + "github.com/coder/code-marketplace/storage" "github.com/coder/code-marketplace/testutil" ) @@ -28,45 +29,87 @@ func TestGetExtensionAssetPath(t *testing.T) { } t.Run("NoExtension", func(t *testing.T) { + t.Parallel() + _, err := db.GetExtensionAssetPath(context.Background(), &database.Asset{ Publisher: "publisher", Extension: "extension", Type: "type", - Version: "version", + Version: storage.Version{Version: "version"}, }, *baseURL) require.Error(t, err) }) t.Run("NoAsset", func(t *testing.T) { + t.Parallel() + _, err := db.GetExtensionAssetPath(context.Background(), &database.Asset{ Publisher: "foo", Extension: "zany", Type: "nope", - Version: "1.0.0", + Version: storage.Version{Version: "1.0.0"}, + }, *baseURL) + require.Error(t, err) + + _, err = db.GetExtensionAssetPath(context.Background(), &database.Asset{ + Publisher: "foo", + Extension: "zany", + Type: "nope", + Version: storage.Version{Version: "1.0.0", TargetPlatform: storage.PlatformDarwinX64}, }, *baseURL) require.Error(t, err) }) t.Run("UnaddressableAsset", func(t *testing.T) { + t.Parallel() + _, err := db.GetExtensionAssetPath(context.Background(), &database.Asset{ Publisher: "foo", Extension: "zany", Type: "Unaddressable", - Version: "1.0.0", + Version: storage.Version{Version: "1.0.0"}, }, *baseURL) require.Error(t, err) }) t.Run("GetAsset", func(t *testing.T) { + t.Parallel() + path, err := db.GetExtensionAssetPath(context.Background(), &database.Asset{ Publisher: "foo", Extension: "zany", Type: "Microsoft.VisualStudio.Services.Icons.Default", - Version: "1.0.0", + Version: storage.Version{Version: "1.0.0"}, }, *baseURL) require.NoError(t, err) require.Equal(t, fmt.Sprintf("%s/files/foo/zany/1.0.0/icon.png", base), path) }) + + t.Run("GetAssetWithPlatform", func(t *testing.T) { + t.Parallel() + + path, err := db.GetExtensionAssetPath(context.Background(), &database.Asset{ + Publisher: "foo", + Extension: "zany", + Type: "Microsoft.VisualStudio.Services.Icons.Default", + Version: storage.Version{Version: "1.0.0", TargetPlatform: storage.PlatformWin32X64}, + }, *baseURL) + require.NoError(t, err) + require.Equal(t, fmt.Sprintf("%s/files/foo/zany/1.0.0@win32-x64/icon.png", base), path) + + // All these do not append platform strings. + platforms := []storage.Platform{storage.PlatformUniversal, storage.PlatformUnknown, storage.PlatformUndefined} + for _, platform := range platforms { + path, err = db.GetExtensionAssetPath(context.Background(), &database.Asset{ + Publisher: "foo", + Extension: "zany", + Type: "Microsoft.VisualStudio.Services.Icons.Default", + Version: storage.Version{Version: "1.0.0", TargetPlatform: platform}, + }, *baseURL) + require.NoError(t, err) + require.Equal(t, fmt.Sprintf("%s/files/foo/zany/1.0.0/icon.png", base), path) + } + }) } type checkFunc func(t *testing.T, ext *database.Extension) @@ -383,7 +426,7 @@ func TestGetExtensions(t *testing.T) { CheckFunc: func(t *testing.T, ext *database.Extension) { require.Empty(t, ext.Categories, "categories") require.Empty(t, ext.Tags, "tags") - require.Len(t, ext.Versions, 5, "versions") + require.Len(t, ext.Versions, 11, "versions") for _, version := range ext.Versions { require.Empty(t, version.Files, "files") require.Empty(t, version.Properties, "properties") @@ -403,11 +446,11 @@ func TestGetExtensions(t *testing.T) { CheckFunc: func(t *testing.T, ext *database.Extension) { require.Empty(t, ext.Categories, "categories") require.Empty(t, ext.Tags, "tags") - require.Len(t, ext.Versions, 5, "versions") + require.Len(t, ext.Versions, 11, "versions") for _, version := range ext.Versions { // Should ignore non-addressable files. require.Len(t, version.Files, 1, "files") - require.Equal(t, fmt.Sprintf("%s/files/foo/zany/%s/icon.png", base, version.Version), version.Files[0].Source) + require.Equal(t, fmt.Sprintf("%s/files/foo/zany/%s/icon.png", base, version), version.Files[0].Source) require.Empty(t, version.Properties, "properties") } }, @@ -425,11 +468,11 @@ func TestGetExtensions(t *testing.T) { CheckFunc: func(t *testing.T, ext *database.Extension) { require.Empty(t, ext.Categories, "categories") require.Empty(t, ext.Tags, "tags") - require.Len(t, ext.Versions, 5, "versions") + require.Len(t, ext.Versions, 11, "versions") for _, version := range ext.Versions { require.Empty(t, version.Files, "files") require.Empty(t, version.Properties, "properties") - require.Equal(t, fmt.Sprintf("%s/assets/foo/zany/%s", base, version.Version), version.AssetURI) + require.Equal(t, fmt.Sprintf("%s/assets/foo/zany/%s", base, version), version.AssetURI) require.Equal(t, version.AssetURI, version.FallbackAssetURI) } }, @@ -463,7 +506,7 @@ func TestGetExtensions(t *testing.T) { CheckFunc: func(t *testing.T, ext *database.Extension) { require.Empty(t, ext.Categories, "categories") require.Empty(t, ext.Tags, "tags") - require.Len(t, ext.Versions, 5, "versions") + require.Len(t, ext.Versions, 11, "versions") for _, version := range ext.Versions { require.Empty(t, version.Files, "files") require.Len(t, version.Properties, 2, "properties") @@ -483,8 +526,9 @@ func TestGetExtensions(t *testing.T) { CheckFunc: func(t *testing.T, ext *database.Extension) { require.Empty(t, ext.Categories, "categories") require.Empty(t, ext.Tags, "tags") - require.Len(t, ext.Versions, 1, "versions") + require.Len(t, ext.Versions, 6, "versions") // One for each platform. for _, version := range ext.Versions { + require.Equal(t, "3.0.0", version.Version.Version) require.Empty(t, version.Files, "files") require.Empty(t, version.Properties, "properties") } @@ -503,10 +547,10 @@ func TestGetExtensions(t *testing.T) { CheckFunc: func(t *testing.T, ext *database.Extension) { require.Len(t, ext.Categories, 1, "categories") require.Len(t, ext.Tags, 1, "tags") - require.Len(t, ext.Versions, 5, "versions") + require.Len(t, ext.Versions, 11, "versions") for _, version := range ext.Versions { require.Len(t, version.Files, 1, "files") - require.Equal(t, fmt.Sprintf("%s/files/foo/zany/%s/icon.png", base, version.Version), version.Files[0].Source) + require.Equal(t, fmt.Sprintf("%s/files/foo/zany/%s/icon.png", base, version), version.Files[0].Source) require.Len(t, version.Properties, 2, "properties") require.Equal(t, version.AssetURI, version.FallbackAssetURI) } diff --git a/database/nodb.go b/database/nodb.go index bb8971b..75da4cb 100644 --- a/database/nodb.go +++ b/database/nodb.go @@ -40,7 +40,7 @@ func (db *NoDB) GetExtensionAssetPath(ctx context.Context, asset *Asset, baseURL "files", asset.Publisher, asset.Extension, - asset.Version), + asset.Version.String()), }).String() for _, a := range manifest.Assets.Asset { @@ -56,7 +56,7 @@ func (db *NoDB) GetExtensions(ctx context.Context, filter Filter, flags Flag, ba vscodeExts := []*noDBExtension{} start := time.Now() - err := db.Storage.WalkExtensions(ctx, func(manifest *storage.VSIXManifest, versions []string) error { + err := db.Storage.WalkExtensions(ctx, func(manifest *storage.VSIXManifest, versions []storage.Version) error { vscodeExt := convertManifestToExtension(manifest) if matched, distances := getMatches(vscodeExt, filter); matched { vscodeExt.versions = versions @@ -315,15 +315,26 @@ func (db *NoDB) getVersions(ctx context.Context, ext *noDBExtension, flags Flag, slog.F("publisher", ext.Publisher.PublisherName), slog.F("extension", ext.Name)) - versionStrs := ext.versions + var storageVers []storage.Version if flags&IncludeLatestVersionOnly != 0 { - versionStrs = []string{ext.versions[0]} + // There might be multiple platforms for this version so find all the ones + // that match. Since they are sorted we can bail once one does not match. + latestVersion := ext.versions[0].Version + for _, version := range ext.versions { + if version.Version == latestVersion { + storageVers = append(storageVers, version) + } else { + break + } + } + } else { + storageVers = ext.versions } versions := []ExtVersion{} - for _, versionStr := range versionStrs { - ctx := slog.With(ctx, slog.F("version", versionStr)) - manifest, err := db.Storage.Manifest(ctx, ext.Publisher.PublisherName, ext.Name, versionStr) + for _, storageVer := range storageVers { + ctx := slog.With(ctx, slog.F("version", storageVer)) + manifest, err := db.Storage.Manifest(ctx, ext.Publisher.PublisherName, ext.Name, storageVer) if err != nil && errors.Is(err, context.Canceled) { return nil, err } else if err != nil { @@ -332,9 +343,8 @@ func (db *NoDB) getVersions(ctx context.Context, ext *noDBExtension, flags Flag, } version := ExtVersion{ - Version: versionStr, + Version: storageVer, // LastUpdated: time.Now(), // TODO: Use modified time? - TargetPlatform: manifest.Metadata.Identity.TargetPlatform, } if flags&IncludeFiles != 0 { @@ -346,7 +356,7 @@ func (db *NoDB) getVersions(ctx context.Context, ext *noDBExtension, flags Flag, "/files", ext.Publisher.PublisherName, ext.Name, - versionStr), + version.String()), }).String() for _, asset := range manifest.Assets.Asset { if asset.Addressable != "true" { @@ -378,7 +388,7 @@ func (db *NoDB) getVersions(ctx context.Context, ext *noDBExtension, flags Flag, "assets", ext.Publisher.PublisherName, ext.Name, - versionStr), + version.String()), }).String() version.FallbackAssetURI = version.AssetURI } @@ -394,7 +404,7 @@ type noDBExtension struct { // Used internally for ranking. Lower means more relevant. distances []int `json:"-"` // Used internally to avoid reading and sorting versions twice. - versions []string `json:"-"` + versions []storage.Version `json:"-"` } func convertManifestToExtension(manifest *storage.VSIXManifest) *noDBExtension { diff --git a/extensionsign/doc.go b/extensionsign/doc.go new file mode 100644 index 0000000..14ec5b5 --- /dev/null +++ b/extensionsign/doc.go @@ -0,0 +1,2 @@ +// Package extensionsign provides utilities for working with extension signatures. +package extensionsign diff --git a/extensionsign/sigmanifest.go b/extensionsign/sigmanifest.go new file mode 100644 index 0000000..3ff67e6 --- /dev/null +++ b/extensionsign/sigmanifest.go @@ -0,0 +1,114 @@ +package extensionsign + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "io" + + "golang.org/x/xerrors" + + "github.com/coder/code-marketplace/storage/easyzip" +) + +// SignatureManifest should be serialized to JSON before being signed. +type SignatureManifest struct { + Package File + // Entries is base64(filepath) -> File + Entries map[string]File +} + +func (a SignatureManifest) String() string { + return fmt.Sprintf("Package %q with Entries: %d", a.Package.Digests.SHA256, len(a.Entries)) +} + +// Equal is helpful for debugging to know if two manifests are equal. +// They can change if any file is removed/added/edited to an extension. +func (a SignatureManifest) Equal(b SignatureManifest) error { + var errs []error + if err := a.Package.Equal(b.Package); err != nil { + errs = append(errs, xerrors.Errorf("package: %w", err)) + } + + if len(a.Entries) != len(b.Entries) { + errs = append(errs, xerrors.Errorf("entry count mismatch: %d != %d", len(a.Entries), len(b.Entries))) + } + + for k, v := range a.Entries { + if _, ok := b.Entries[k]; !ok { + errs = append(errs, xerrors.Errorf("entry %q not found in second set", k)) + continue + } + if err := v.Equal(b.Entries[k]); err != nil { + errs = append(errs, xerrors.Errorf("entry %q: %w", k, err)) + } + } + return errors.Join(errs...) +} + +type File struct { + Size int64 `json:"size"` + Digests Digests `json:"digests"` +} + +func (f File) Equal(b File) error { + if f.Size != b.Size { + return xerrors.Errorf("size mismatch: %d != %d", f.Size, b.Size) + } + if f.Digests.SHA256 != b.Digests.SHA256 { + return xerrors.Errorf("sha256 mismatch: %s != %s", f.Digests.SHA256, b.Digests.SHA256) + } + return nil +} + +func FileManifest(file io.Reader) (File, error) { + hash := sha256.New() + + n, err := io.Copy(hash, file) + if err != nil { + return File{}, xerrors.Errorf("hash file: %w", err) + } + + return File{ + Size: n, + Digests: Digests{ + SHA256: base64.StdEncoding.EncodeToString(hash.Sum(nil)), + }, + }, nil +} + +type Digests struct { + SHA256 string `json:"sha256"` +} + +// GenerateSignatureManifest generates a signature manifest for a VSIX file. +// It does not sign the manifest. The manifest is the base64 encoded file path +// followed by the sha256 hash of the file, and it's size. +func GenerateSignatureManifest(vsixFile []byte) (SignatureManifest, error) { + pkgManifest, err := FileManifest(bytes.NewReader(vsixFile)) + if err != nil { + return SignatureManifest{}, xerrors.Errorf("package manifest: %w", err) + } + + manifest := SignatureManifest{ + Package: pkgManifest, + Entries: make(map[string]File), + } + + err = easyzip.ExtractZip(vsixFile, func(name string, reader io.Reader) error { + fm, err := FileManifest(reader) + if err != nil { + return xerrors.Errorf("file %q: %w", name, err) + } + manifest.Entries[base64.StdEncoding.EncodeToString([]byte(name))] = fm + return nil + }) + + if err != nil { + return SignatureManifest{}, err + } + + return manifest, nil +} diff --git a/extensionsign/sigzip.go b/extensionsign/sigzip.go new file mode 100644 index 0000000..31c82bf --- /dev/null +++ b/extensionsign/sigzip.go @@ -0,0 +1,54 @@ +package extensionsign + +import ( + "archive/zip" + "bytes" + "encoding/json" + + "golang.org/x/xerrors" + + "github.com/coder/code-marketplace/storage/easyzip" +) + +func ExtractSignatureManifest(zip []byte) (SignatureManifest, error) { + r, err := easyzip.GetZipFileReader(zip, ".signature.manifest") + if err != nil { + return SignatureManifest{}, xerrors.Errorf("get manifest: %w", err) + } + + defer r.Close() + var manifest SignatureManifest + err = json.NewDecoder(r).Decode(&manifest) + if err != nil { + return SignatureManifest{}, xerrors.Errorf("decode manifest: %w", err) + } + return manifest, nil +} + +func IncludeEmptySignature() ([]byte, error) { + var buf bytes.Buffer + w := zip.NewWriter(&buf) + + manFile, err := w.Create(".signature.manifest") + if err != nil { + return nil, xerrors.Errorf("create manifest: %w", err) + } + + _, err = manFile.Write([]byte{}) + if err != nil { + return nil, xerrors.Errorf("write manifest: %w", err) + } + + // Empty file + _, err = w.Create(".signature.p7s") + if err != nil { + return nil, xerrors.Errorf("create empty p7s signature: %w", err) + } + + err = w.Close() + if err != nil { + return nil, xerrors.Errorf("close zip: %w", err) + } + + return buf.Bytes(), nil +} diff --git a/fixtures/generate.bash b/fixtures/generate.bash index a27b3a3..9b9b7cc 100755 --- a/fixtures/generate.bash +++ b/fixtures/generate.bash @@ -105,7 +105,7 @@ EOF cat< "$dest/extension/extension.js" const vscode = require("vscode"); function activate(context) { - vscode.window.showInformationMessage("mock extension $publisher.$name-$version loaded"); + vscode.window.showInformationMessage("mock extension $publisher.$name@$version loaded"); } exports.activate = activate; EOF @@ -121,7 +121,7 @@ mock changelog EOF cp "$dir/icon.png" "$dest/extension/images/icon.png" pushd "$dest" >/dev/null - rm "$publisher.$name-$version.vsix" + rm -f "$publisher.$name-$version.vsix" zip -r "$publisher.$name-$version.vsix" * -q popd >/dev/null done < "$dir/versions" diff --git a/flake.lock b/flake.lock index d6c2c5a..9f3817b 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,15 @@ { "nodes": { "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -17,12 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1661353537, - "narHash": "sha256-1E2IGPajOsrkR49mM5h55OtYnU0dGyre6gl60NXKITE=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "0e304ff0d9db453a4b230e9386418fd974d5804a", - "type": "github" + "lastModified": 1744932701, + "narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=", + "path": "/nix/store/isfbldda5j8j6x3nbv1zim0c0dpf90v8-source", + "rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef", + "type": "path" }, "original": { "id": "nixpkgs", @@ -34,6 +36,21 @@ "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index fdc309b..be8e3d2 100644 --- a/flake.nix +++ b/flake.nix @@ -10,7 +10,7 @@ in { devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ - go_1_19 + go_1_24 golangci-lint gotestsum kubernetes-helm diff --git a/go.mod b/go.mod index 20421d1..9d8cf50 100644 --- a/go.mod +++ b/go.mod @@ -1,39 +1,46 @@ module github.com/coder/code-marketplace -go 1.19 +go 1.23.0 require ( - cdr.dev/slog v1.4.1 - github.com/go-chi/chi/v5 v5.0.7 + cdr.dev/slog v1.6.1 + github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/cors v1.2.1 - github.com/go-chi/httprate v0.7.0 - github.com/google/uuid v1.3.0 - github.com/lithammer/fuzzysearch v1.1.5 - github.com/spf13/cobra v1.5.0 - github.com/stretchr/testify v1.6.1 - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f + github.com/go-chi/httprate v0.15.0 + github.com/google/uuid v1.6.0 + github.com/lithammer/fuzzysearch v1.1.8 + github.com/spf13/cobra v1.9.1 + github.com/stretchr/testify v1.10.0 + golang.org/x/mod v0.24.0 + golang.org/x/sync v0.14.0 + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 ) require ( - github.com/alecthomas/chroma v0.9.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect + cloud.google.com/go/logging v1.8.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v0.7.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.4.0 // indirect - github.com/fatih/color v1.12.0 // indirect - github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect - github.com/google/go-cmp v0.5.8 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/mattn/go-colorable v0.1.8 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - go.opencensus.io v0.23.0 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect - golang.org/x/text v0.3.7 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opentelemetry.io/otel v1.16.0 // indirect + go.opentelemetry.io/otel/trace v1.16.0 // indirect + golang.org/x/crypto v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect + google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5dab701..e2352ee 100644 --- a/go.sum +++ b/go.sum @@ -1,554 +1,138 @@ -cdr.dev/slog v1.4.1 h1:Q8+X63m8/WB4geelMTDO8t4CTwVh1f7+5Cxi7kS/SZg= -cdr.dev/slog v1.4.1/go.mod h1:O76C6gZJxa5HK1SXMrjd48V2kJxYZKFRTcFfn/V9OhA= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0 h1:bAMqZidYkmIsUqe6PtkEPT7Q+vfizScn+jfNA6jwK9c= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/chroma v0.9.1 h1:cBmvQqRImzR5aWqdMxYZByND4S7BCS/g0svZb28h0Dc= -github.com/alecthomas/chroma v0.9.1/go.mod h1:eMuEnpA18XbG/WhOWtCzJHS7WqEtDAI+HxdwoW0nVSk= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= -github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= -github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= -github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= -github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +cdr.dev/slog v1.6.1 h1:IQjWZD0x6//sfv5n+qEhbu3wBkmtBQY5DILXNvMaIv4= +cdr.dev/slog v1.6.1/go.mod h1:eHEYQLaZvxnIAXC+XdTSNLb/kgA/X2RVSF72v5wsxEI= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/logging v1.8.1 h1:26skQWPeYhvIasWKm48+Eq7oUqdcdbwsCVwz5Ys0FvU= +cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= +cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= -github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= -github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= -github.com/go-chi/httprate v0.7.0 h1:8W0dF7Xa2Duz2p8ncGaehIphrxQGNlOtoGY0+NRRfjQ= -github.com/go-chi/httprate v0.7.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lithammer/fuzzysearch v1.1.5 h1:Ag7aKU08wp0R9QCfF4GoGST9HbmAIeLP7xwMrOBEp1c= -github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g= +github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= +go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= +go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 9d9d182..1b8ac0d 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 1.3.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.1.0" +appVersion: "v2.3.1" diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 14a69ec..27c4895 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -25,10 +25,17 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "code-marketplace.serviceAccountName" . }} + {{- if or (.Values.volumes) (not .Values.persistence.artifactory.enabled) }} volumes: + {{- if not .Values.persistence.artifactory.enabled }} - name: extensions persistentVolumeClaim: claimName: {{ include "code-marketplace.fullname" . }} + {{- end }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: @@ -37,18 +44,43 @@ spec: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{- if .Values.persistence.artifactory.enabled }} + - name: "ARTIFACTORY_TOKEN" + valueFrom: + secretKeyRef: + name: artifactory + key: token + {{- end }} + {{- with .Values.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} ports: - name: http - containerPort: 80 + containerPort: {{ .Values.service.port }} protocol: TCP args: - --address - - 0.0.0.0:80 + - "0.0.0.0:{{ .Values.service.port }}" + {{- if .Values.persistence.artifactory.enabled }} + - --artifactory + - {{ .Values.persistence.artifactory.uri }} + - --repo + - {{ .Values.persistence.artifactory.repo }} + {{- else }} - --extensions-dir - /extensions + {{- end }} + {{- if or (.Values.volumeMounts) (not .Values.persistence.artifactory.enabled) }} volumeMounts: + {{- if not .Values.persistence.artifactory.enabled }} - name: extensions mountPath: /extensions + {{- end }} + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- end }} livenessProbe: httpGet: path: /healthz diff --git a/helm/templates/pvc.yaml b/helm/templates/pvc.yaml index 1dcf57e..17dfa9b 100644 --- a/helm/templates/pvc.yaml +++ b/helm/templates/pvc.yaml @@ -1,3 +1,4 @@ +{{- if not .Values.persistence.artifactory.enabled }} apiVersion: v1 kind: PersistentVolumeClaim metadata: @@ -8,4 +9,5 @@ spec: resources: requests: storage: {{ .Values.persistence.size | quote }} - storageClassName: standard + storageClassName: {{ .Values.persistence.storageClass | quote }} +{{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index cba01dd..7b0280e 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -10,6 +10,8 @@ image: # Overrides the image tag whose default is the chart appVersion. tag: "" +extraEnv: [] + imagePullSecrets: [] nameOverride: "" fullnameOverride: "" @@ -77,6 +79,19 @@ autoscaling: targetCPUUtilizationPercentage: 80 # targetMemoryUtilizationPercentage: 80 +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + nodeSelector: {} tolerations: [] @@ -84,4 +99,12 @@ tolerations: [] affinity: {} persistence: + storageClass: standard + artifactory: + # Use Artifactory for extensions instead of a persistent volume. Make sure + # to create an `artifactory` secret with a `token` key. + enabled: false + uri: https://artifactory.server/artifactory + repo: extensions + # Size is ignored when using Artifactory. size: 100Gi diff --git a/storage/artifactory.go b/storage/artifactory.go index 479af4d..9b15e20 100644 --- a/storage/artifactory.go +++ b/storage/artifactory.go @@ -15,11 +15,11 @@ import ( "sync" "time" - "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/code-marketplace/storage/easyzip" "github.com/coder/code-marketplace/util" ) @@ -42,6 +42,8 @@ type ArtifactoryList struct { Files []ArtifactoryFile `json:"files"` } +var _ Storage = (*Artifactory)(nil) + // Artifactory implements Storage. It stores extensions remotely through // Artifactory by both copying the VSIX and extracting said VSIX to a tree // structure in the form of publisher/extension/version to easily serve @@ -90,7 +92,7 @@ func NewArtifactoryStorage(ctx context.Context, options *ArtifactoryOptions) (*A start := time.Now() count := 0 var eg errgroup.Group - err := s.WalkExtensions(ctx, func(manifest *VSIXManifest, versions []string) error { + err := s.WalkExtensions(ctx, func(manifest *VSIXManifest, versions []Version) error { for _, ver := range versions { count++ ver := ver @@ -100,8 +102,10 @@ func NewArtifactoryStorage(ctx context.Context, options *ArtifactoryOptions) (*A if err != nil && !errors.Is(err, context.Canceled) { return err } else if err != nil { - id := ExtensionID(identity.Publisher, identity.ID, ver) - s.logger.Error(ctx, "Unable to read extension manifest", slog.Error(err), slog.F("id", id)) + id := ExtensionID(identity.Publisher, identity.ID, ver.Version) + s.logger.Error(ctx, "Unable to read extension manifest", slog.Error(err), + slog.F("id", id), + slog.F("targetPlatform", ver.TargetPlatform)) } return nil }) @@ -212,10 +216,13 @@ func (s *Artifactory) upload(ctx context.Context, endpoint string, r io.Reader) return code, nil } -func (s *Artifactory) AddExtension(ctx context.Context, manifest *VSIXManifest, vsix []byte) (string, error) { +func (s *Artifactory) AddExtension(ctx context.Context, manifest *VSIXManifest, vsix []byte, extra ...File) (string, error) { // Extract the zip to the correct path. identity := manifest.Metadata.Identity - dir := path.Join(identity.Publisher, identity.ID, identity.Version) + dir := path.Join(identity.Publisher, identity.ID, Version{ + Version: identity.Version, + TargetPlatform: identity.TargetPlatform, + }.String()) // Uploading every file in an extension such as ms-python.python can take // quite a while (16 minutes!!). As a compromise only extract a file if it @@ -240,7 +247,7 @@ func (s *Artifactory) AddExtension(ctx context.Context, manifest *VSIXManifest, } } - err := ExtractZip(vsix, func(name string, r io.Reader) error { + err := easyzip.ExtractZip(vsix, func(name string, r io.Reader) error { if util.Contains(assets, name) || (browser != "" && strings.HasPrefix(name, browser)) { _, err := s.upload(ctx, path.Join(dir, name), r) return err @@ -252,12 +259,19 @@ func (s *Artifactory) AddExtension(ctx context.Context, manifest *VSIXManifest, } // Copy the VSIX itself as well. - vsixName := fmt.Sprintf("%s.vsix", ExtensionIDFromManifest(manifest)) + vsixName := fmt.Sprintf("%s.vsix", ExtensionVSIXNameFromManifest(manifest)) _, err = s.upload(ctx, path.Join(dir, vsixName), bytes.NewReader(vsix)) if err != nil { return "", err } + for _, file := range extra { + _, err := s.upload(ctx, path.Join(dir, file.RelativePath), bytes.NewReader(file.Content)) + if err != nil { + return "", err + } + } + return s.uri + dir, nil } @@ -282,24 +296,24 @@ func (s *Artifactory) FileServer() http.Handler { }) } -func (s *Artifactory) Manifest(ctx context.Context, publisher, name, version string) (*VSIXManifest, error) { +func (s *Artifactory) Manifest(ctx context.Context, publisher, name string, version Version) (*VSIXManifest, error) { // These queries are so slow it seems worth the extra memory to cache the // manifests for future use. // TODO: Remove manifests that are no longer found in the list to prevent // indefinitely caching manifests belonging to extensions that have since been // removed or dump the cache periodically. - id := ExtensionID(publisher, name, version) - rawMutex, _ := s.manifestMutexes.LoadOrStore(id, &sync.Mutex{}) + vsixName := ExtensionVSIXName(publisher, name, version) + rawMutex, _ := s.manifestMutexes.LoadOrStore(vsixName, &sync.Mutex{}) mutex := rawMutex.(*sync.Mutex) mutex.Lock() defer mutex.Unlock() - rawManifest, ok := s.manifests.Load(id) + rawManifest, ok := s.manifests.Load(vsixName) if ok { return rawManifest.(*VSIXManifest), nil } - reader, _, err := s.read(ctx, path.Join(publisher, name, version, "extension.vsixmanifest")) + reader, _, err := s.read(ctx, path.Join(publisher, name, version.String(), "extension.vsixmanifest")) if err != nil { return nil, err } @@ -316,26 +330,19 @@ func (s *Artifactory) Manifest(ctx context.Context, publisher, name, version str manifest.Assets.Asset = append(manifest.Assets.Asset, VSIXAsset{ Type: VSIXAssetType, - Path: fmt.Sprintf("%s.vsix", ExtensionIDFromManifest(manifest)), + Path: fmt.Sprintf("%s.vsix", ExtensionVSIXNameFromManifest(manifest)), Addressable: "true", }) - rawManifest, _ = s.manifests.LoadOrStore(id, manifest) + rawManifest, _ = s.manifests.LoadOrStore(vsixName, manifest) return rawManifest.(*VSIXManifest), nil } -func (s *Artifactory) RemoveExtension(ctx context.Context, publisher, name, version string) error { - _, err := s.delete(ctx, path.Join(publisher, name, version)) +func (s *Artifactory) RemoveExtension(ctx context.Context, publisher, name string, version Version) error { + _, err := s.delete(ctx, path.Join(publisher, name, version.String())) return err } -type extension struct { - manifest *VSIXManifest - name string - publisher string - versions []string -} - func (s *Artifactory) listWithCache(ctx context.Context) *[]ArtifactoryFile { s.listMutex.Lock() defer s.listMutex.Unlock() @@ -350,7 +357,7 @@ func (s *Artifactory) listWithCache(ctx context.Context) *[]ArtifactoryFile { return s.listCache } -func (s *Artifactory) WalkExtensions(ctx context.Context, fn func(manifest *VSIXManifest, versions []string) error) error { +func (s *Artifactory) WalkExtensions(ctx context.Context, fn func(manifest *VSIXManifest, versions []Version) error) error { // Listing one directory at a time is very slow so get them all at once. If // we already fetched it recently just use that since getting them all at once // is also pretty slow (on the parsing end). @@ -368,12 +375,12 @@ func (s *Artifactory) WalkExtensions(ctx context.Context, fn func(manifest *VSIX id := fmt.Sprintf("%s.%s", parts[1], parts[2]) e, ok := extensions[id] if ok { - e.versions = append(e.versions, parts[3]) + e.versions = append(e.versions, VersionFromString(parts[3])) } else { extensions[id] = &extension{ name: parts[2], publisher: parts[1], - versions: []string{parts[3]}, + versions: []Version{VersionFromString(parts[3])}, } } } @@ -387,14 +394,16 @@ func (s *Artifactory) WalkExtensions(ctx context.Context, fn func(manifest *VSIX defer cancel() for _, ext := range extensions { ext := ext - sort.Sort(sort.Reverse(semver.ByVersion(ext.versions))) + sort.Sort(ByVersion(ext.versions)) eg.Go(func() error { manifest, err := s.Manifest(ctx, ext.publisher, ext.name, ext.versions[0]) if err != nil && errors.Is(err, context.Canceled) { return err } else if err != nil { - id := ExtensionID(ext.publisher, ext.name, ext.versions[0]) - s.logger.Error(ctx, "Unable to read extension manifest", slog.Error(err), slog.F("id", id)) + id := ExtensionID(ext.publisher, ext.name, ext.versions[0].Version) + s.logger.Error(ctx, "Unable to read extension manifest; extension will be ignored", slog.Error(err), + slog.F("id", id), + slog.F("targetPlatform", ext.versions[0].TargetPlatform)) } else { ext.manifest = manifest } @@ -406,6 +415,9 @@ func (s *Artifactory) WalkExtensions(ctx context.Context, fn func(manifest *VSIX return err } for _, ext := range extensions { + if ext.manifest == nil { + continue + } if err = fn(ext.manifest, ext.versions); err != nil { return err } @@ -413,19 +425,20 @@ func (s *Artifactory) WalkExtensions(ctx context.Context, fn func(manifest *VSIX return nil } -func (s *Artifactory) Versions(ctx context.Context, publisher, name string) ([]string, error) { +func (s *Artifactory) Versions(ctx context.Context, publisher, name string) ([]Version, error) { files, _, err := s.list(ctx, path.Join(publisher, name), 1) if err != nil { return nil, err } - versions := []string{} + versions := []Version{} for _, file := range files { // There should only be directories but check just in case. if file.Folder { // The files come with leading slashes so remove them. - versions = append(versions, strings.TrimLeft(file.URI, "/")) + versionDir := strings.TrimLeft(file.URI, "/") + versions = append(versions, VersionFromString(versionDir)) } } - sort.Sort(sort.Reverse(semver.ByVersion(versions))) + sort.Sort(ByVersion(versions)) return versions, nil } diff --git a/storage/zip.go b/storage/easyzip/zip.go similarity index 99% rename from storage/zip.go rename to storage/easyzip/zip.go index da2526f..9a76e89 100644 --- a/storage/zip.go +++ b/storage/easyzip/zip.go @@ -1,4 +1,4 @@ -package storage +package easyzip import ( "archive/zip" diff --git a/storage/zip_test.go b/storage/easyzip/zip_test.go similarity index 99% rename from storage/zip_test.go rename to storage/easyzip/zip_test.go index 3ae2ceb..febe877 100644 --- a/storage/zip_test.go +++ b/storage/easyzip/zip_test.go @@ -1,4 +1,4 @@ -package storage +package easyzip import ( "archive/zip" diff --git a/storage/local.go b/storage/local.go index 7611ef7..1aeedd3 100644 --- a/storage/local.go +++ b/storage/local.go @@ -8,36 +8,100 @@ import ( "os" "path/filepath" "sort" + "sync" + "time" - "golang.org/x/mod/semver" + "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/code-marketplace/storage/easyzip" ) +var _ Storage = (*Local)(nil) + // Local implements Storage. It stores extensions locally on disk by both // copying the VSIX and extracting said VSIX to a tree structure in the form of // publisher/extension/version to easily serve individual assets via HTTP. type Local struct { - extdir string - logger slog.Logger + listCache []extension + listDuration time.Duration + listExpiration time.Time + listMutex sync.Mutex + extdir string + logger slog.Logger +} + +type LocalOptions struct { + // How long to cache the list of extensions with their manifests. Zero means + // no cache. + ListCacheDuration time.Duration + ExtDir string } -func NewLocalStorage(extdir string, logger slog.Logger) (*Local, error) { - extdir, err := filepath.Abs(extdir) +func NewLocalStorage(options *LocalOptions, logger slog.Logger) (*Local, error) { + extdir, err := filepath.Abs(options.ExtDir) if err != nil { return nil, err } return &Local{ - extdir: extdir, - logger: logger, + // TODO: Eject the cache when adding/removing extensions and/or add a + // command to eject the cache? + extdir: extdir, + listDuration: options.ListCacheDuration, + logger: logger, }, nil } -func (s *Local) AddExtension(ctx context.Context, manifest *VSIXManifest, vsix []byte) (string, error) { +func (s *Local) list(ctx context.Context) []extension { + var list []extension + publishers, err := s.getDirNames(ctx, s.extdir) + if err != nil { + s.logger.Error(ctx, "Error reading publisher", slog.Error(err)) + } + for _, publisher := range publishers { + ctx := slog.With(ctx, slog.F("publisher", publisher)) + dir := filepath.Join(s.extdir, publisher) + + extensions, err := s.getDirNames(ctx, dir) + if err != nil { + s.logger.Error(ctx, "Error reading extensions", slog.Error(err)) + } + for _, name := range extensions { + ctx := slog.With(ctx, slog.F("extension", name)) + versions, err := s.Versions(ctx, publisher, name) + if err != nil { + s.logger.Error(ctx, "Error reading versions", slog.Error(err)) + } + if len(versions) == 0 { + continue + } + + // The manifest from the latest version is used for filtering. + manifest, err := s.Manifest(ctx, publisher, name, versions[0]) + if err != nil { + s.logger.Error(ctx, "Unable to read extension manifest", slog.Error(err)) + continue + } + + list = append(list, extension{ + manifest, + name, + publisher, + versions, + }) + } + } + return list +} + +func (s *Local) AddExtension(ctx context.Context, manifest *VSIXManifest, vsix []byte, extra ...File) (string, error) { // Extract the zip to the correct path. identity := manifest.Metadata.Identity - dir := filepath.Join(s.extdir, identity.Publisher, identity.ID, identity.Version) - err := ExtractZip(vsix, func(name string, r io.Reader) error { + dir := filepath.Join(s.extdir, identity.Publisher, identity.ID, Version{ + Version: identity.Version, + TargetPlatform: identity.TargetPlatform, + }.String()) + err := easyzip.ExtractZip(vsix, func(name string, r io.Reader) error { path := filepath.Join(dir, name) err := os.MkdirAll(filepath.Dir(path), 0o755) if err != nil { @@ -56,12 +120,24 @@ func (s *Local) AddExtension(ctx context.Context, manifest *VSIXManifest, vsix [ } // Copy the VSIX itself as well. - vsixPath := filepath.Join(dir, fmt.Sprintf("%s.vsix", ExtensionIDFromManifest(manifest))) + vsixPath := filepath.Join(dir, fmt.Sprintf("%s.vsix", ExtensionVSIXNameFromManifest(manifest))) err = os.WriteFile(vsixPath, vsix, 0o644) if err != nil { return "", err } + for _, file := range extra { + path := filepath.Join(dir, file.RelativePath) + err := os.MkdirAll(filepath.Dir(path), 0o755) + if err != nil { + return "", err + } + err = os.WriteFile(path, file.Content, 0o644) + if err != nil { + return dir, xerrors.Errorf("write extra file %q: %w", path, err) + } + } + return dir, nil } @@ -69,8 +145,8 @@ func (s *Local) FileServer() http.Handler { return http.FileServer(http.Dir(s.extdir)) } -func (s *Local) Manifest(ctx context.Context, publisher, name, version string) (*VSIXManifest, error) { - reader, err := os.Open(filepath.Join(s.extdir, publisher, name, version, "extension.vsixmanifest")) +func (s *Local) Manifest(ctx context.Context, publisher, name string, version Version) (*VSIXManifest, error) { + reader, err := os.Open(filepath.Join(s.extdir, publisher, name, version.String(), "extension.vsixmanifest")) if err != nil { return nil, err } @@ -87,15 +163,15 @@ func (s *Local) Manifest(ctx context.Context, publisher, name, version string) ( manifest.Assets.Asset = append(manifest.Assets.Asset, VSIXAsset{ Type: VSIXAssetType, - Path: fmt.Sprintf("%s.vsix", ExtensionIDFromManifest(manifest)), + Path: fmt.Sprintf("%s.vsix", ExtensionVSIXNameFromManifest(manifest)), Addressable: "true", }) return manifest, nil } -func (s *Local) RemoveExtension(ctx context.Context, publisher, name, version string) error { - dir := filepath.Join(s.extdir, publisher, name, version) +func (s *Local) RemoveExtension(ctx context.Context, publisher, name string, version Version) error { + dir := filepath.Join(s.extdir, publisher, name, version.String()) // RemoveAll() will not error if the directory does not exist so check first // as this function should error when removing versions that do not exist. _, err := os.Stat(dir) @@ -105,47 +181,35 @@ func (s *Local) RemoveExtension(ctx context.Context, publisher, name, version st return os.RemoveAll(dir) } -func (s *Local) Versions(ctx context.Context, publisher, name string) ([]string, error) { +func (s *Local) Versions(ctx context.Context, publisher, name string) ([]Version, error) { dir := filepath.Join(s.extdir, publisher, name) - versions, err := s.getDirNames(ctx, dir) + versionDirs, err := s.getDirNames(ctx, dir) + var versions []Version + for _, versionDir := range versionDirs { + versions = append(versions, VersionFromString(versionDir)) + } // Return anything we did get even if there was an error. - sort.Sort(sort.Reverse(semver.ByVersion(versions))) + sort.Sort(ByVersion(versions)) return versions, err } -func (s *Local) WalkExtensions(ctx context.Context, fn func(manifest *VSIXManifest, versions []string) error) error { - publishers, err := s.getDirNames(ctx, s.extdir) - if err != nil { - s.logger.Error(ctx, "Error reading publisher", slog.Error(err)) +func (s *Local) listWithCache(ctx context.Context) []extension { + s.listMutex.Lock() + defer s.listMutex.Unlock() + if s.listCache == nil || time.Now().After(s.listExpiration) { + s.listExpiration = time.Now().Add(s.listDuration) + s.listCache = s.list(ctx) } - for _, publisher := range publishers { - ctx := slog.With(ctx, slog.F("publisher", publisher)) - dir := filepath.Join(s.extdir, publisher) - - extensions, err := s.getDirNames(ctx, dir) - if err != nil { - s.logger.Error(ctx, "Error reading extensions", slog.Error(err)) - } - for _, extension := range extensions { - ctx := slog.With(ctx, slog.F("extension", extension)) - versions, err := s.Versions(ctx, publisher, extension) - if err != nil { - s.logger.Error(ctx, "Error reading versions", slog.Error(err)) - } - if len(versions) == 0 { - continue - } - - // The manifest from the latest version is used for filtering. - manifest, err := s.Manifest(ctx, publisher, extension, versions[0]) - if err != nil { - s.logger.Error(ctx, "Unable to read extension manifest", slog.Error(err)) - continue - } + return s.listCache +} - if err = fn(manifest, versions); err != nil { - return err - } +func (s *Local) WalkExtensions(ctx context.Context, fn func(manifest *VSIXManifest, versions []Version) error) error { + // Walking through directories on disk and parsing manifest files takes several + // minutes with many extensions installed, so if we already did that within + // a specified duration, just load extensions from the cache instead. + for _, extension := range s.listWithCache(ctx) { + if err := fn(extension.manifest, extension.versions); err != nil { + return err } } return nil diff --git a/storage/local_test.go b/storage/local_test.go index d79fcfa..ae5a8e6 100644 --- a/storage/local_test.go +++ b/storage/local_test.go @@ -15,7 +15,7 @@ import ( func localFactory(t *testing.T) testStorage { extdir := t.TempDir() logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) - s, err := storage.NewLocalStorage(extdir, logger) + s, err := storage.NewLocalStorage(&storage.LocalOptions{ExtDir: extdir}, logger) require.NoError(t, err) return testStorage{ storage: s, diff --git a/storage/signature.go b/storage/signature.go new file mode 100644 index 0000000..6d15a9f --- /dev/null +++ b/storage/signature.go @@ -0,0 +1,109 @@ +package storage + +import ( + "context" + "net/http" + "strconv" + "strings" + + "cdr.dev/slog" + "github.com/coder/code-marketplace/api/httpapi" + "github.com/coder/code-marketplace/api/httpmw" + + "github.com/coder/code-marketplace/extensionsign" +) + +var _ Storage = (*Signature)(nil) + +const ( + SigzipFileExtension = ".signature.p7s" + sigManifestName = ".signature.manifest" +) + +func SignatureZipFilename(manifest *VSIXManifest) string { + return ExtensionVSIXNameFromManifest(manifest) + SigzipFileExtension +} + +// Signature is a storage wrapper that can sign extensions on demand. +type Signature struct { + Logger slog.Logger + IncludeEmptySignatures bool + Storage +} + +func NewSignatureStorage(logger slog.Logger, includeEmptySignatures bool, s Storage) *Signature { + if includeEmptySignatures { + logger.Info(context.Background(), "Signature storage enabled, if using VS Code on Windows or macOS, this will not work.") + } + return &Signature{ + Logger: logger, + IncludeEmptySignatures: includeEmptySignatures, + Storage: s, + } +} + +func (s *Signature) SigningEnabled() bool { + return s.IncludeEmptySignatures +} + +func (s *Signature) Manifest(ctx context.Context, publisher, name string, version Version) (*VSIXManifest, error) { + manifest, err := s.Storage.Manifest(ctx, publisher, name, version) + if err != nil { + return nil, err + } + + if s.SigningEnabled() { + for _, asset := range manifest.Assets.Asset { + if asset.Path == SignatureZipFilename(manifest) { + // Already signed + return manifest, nil + } + } + manifest.Assets.Asset = append(manifest.Assets.Asset, VSIXAsset{ + Type: VSIXSignatureType, + Path: SignatureZipFilename(manifest), + Addressable: "true", + }) + return manifest, nil + } + return manifest, nil +} + +// FileServer will intercept requests for signed extensions payload. +// It does this by looking for 'SigzipFileExtension' or p7s.sig. +// +// The signed payload is completely empty. Nothing it actually signed. +// +// Some notes: +// +// - VSCodium requires a signature to exist, but it does appear to actually read +// the signature. Meaning the signature could be empty, incorrect, or a +// picture of cat and it would work. There is no signature verification. +// +// - VSCode requires a signature payload to exist, but the content is optional +// for linux users. +// For windows users, the signature must be valid, and this implementation +// will not work. +func (s *Signature) FileServer() http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if s.SigningEnabled() && strings.HasSuffix(r.URL.Path, SigzipFileExtension) { + // hijack this request, return an empty signature payload + signed, err := extensionsign.IncludeEmptySignature() + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.ErrorResponse{ + Message: "Unable to generate empty signature for extension", + Detail: err.Error(), + RequestID: httpmw.RequestID(r), + }) + return + } + + rw.Header().Set("Content-Length", strconv.FormatInt(int64(len(signed)), 10)) + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write(signed) + return + } + + s.Storage.FileServer().ServeHTTP(rw, r) + }) +} diff --git a/storage/signature_test.go b/storage/signature_test.go new file mode 100644 index 0000000..2aead64 --- /dev/null +++ b/storage/signature_test.go @@ -0,0 +1,36 @@ +package storage_test + +import ( + "testing" + + "cdr.dev/slog" + "github.com/coder/code-marketplace/storage" +) + +func expectSignature(manifest *storage.VSIXManifest) { + manifest.Assets.Asset = append(manifest.Assets.Asset, storage.VSIXAsset{ + Type: storage.VSIXSignatureType, + Path: storage.SignatureZipFilename(manifest), + Addressable: "true", + }) +} + +//nolint:revive // test control flag +func signed(signer bool, factory func(t *testing.T) testStorage) func(t *testing.T) testStorage { + return func(t *testing.T) testStorage { + st := factory(t) + key := false + var exp func(*storage.VSIXManifest) + if signer { + key = true + exp = expectSignature + } + + return testStorage{ + storage: storage.NewSignatureStorage(slog.Make(), key, st.storage), + write: st.write, + exists: st.exists, + expectedManifest: exp, + } + } +} diff --git a/storage/storage.go b/storage/storage.go index dcbeb21..1ab6ffd 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -12,9 +12,12 @@ import ( "strings" "time" + "golang.org/x/mod/semver" + "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/code-marketplace/storage/easyzip" ) // VSIXManifest implement XMLManifest.PackageManifest. @@ -44,14 +47,40 @@ type VSIXMetadata struct { Categories string } -// VSIXManifest implement XMLManifest.PackageManifest.Metadata.Identity. +// Platform implements TargetPlatform. +// https://github.com/microsoft/vscode/blob/main/src/vs/platform/extensions/common/extensions.ts#L291-L311 +type Platform string + +const ( + PlatformWin32X64 Platform = "win32-x64" + PlatformWin32Ia32 Platform = "win32-ia32" + PlatformWin32Arm64 Platform = "win32-arm64" + + PlatformLinuxX64 Platform = "linux-x64" + PlatformLinuxArm64 Platform = "linux-arm64" + PlatformLinuxArmhf Platform = "linux-armhf" + + PlatformAlpineX64 Platform = "alpine-x64" + PlatformAlpineArm64 Platform = "alpine-arm64" + + PlatformDarwinX64 Platform = "darwin-x64" + PlatformDarwinArm64 Platform = "darwin-arm64" + + PlatformWeb Platform = "web" + + PlatformUniversal Platform = "universal" + PlatformUnknown Platform = "unknown" + PlatformUndefined Platform = "undefined" +) + +// VSIXManifest implements XMLManifest.PackageManifest.Metadata.Identity. // https://github.com/microsoft/vscode-vsce/blob/main/src/xml.ts#L14 type VSIXIdentity struct { // ID correlates to ExtensionName, *not* ExtensionID. - ID string `xml:"Id,attr"` - Version string `xml:",attr"` - Publisher string `xml:",attr"` - TargetPlatform string `xml:",attr"` + ID string `xml:"Id,attr"` + Version string `xml:",attr"` + Publisher string `xml:",attr"` + TargetPlatform Platform `xml:",attr"` } // VSIXProperties implements XMLManifest.PackageManifest.Metadata.Properties. @@ -85,6 +114,7 @@ type AssetType string const ( ManifestAssetType AssetType = "Microsoft.VisualStudio.Code.Manifest" // This is the package.json. VSIXAssetType AssetType = "Microsoft.VisualStudio.Services.VSIXPackage" + VSIXSignatureType AssetType = "Microsoft.VisualStudio.Services.VsixSignature" ) // VSIXAsset implements XMLManifest.PackageManifest.Assets.Asset. @@ -96,36 +126,118 @@ type VSIXAsset struct { } type Options struct { - Artifactory string - ExtDir string - Repo string - Logger slog.Logger + IncludeEmptySignatures bool + Artifactory string + ExtDir string + Repo string + Logger slog.Logger + ListCacheDuration time.Duration +} + +type extension struct { + manifest *VSIXManifest + name string + publisher string + versions []Version +} + +// Version is a subset of database.ExtVersion. +type Version struct { + TargetPlatform Platform `json:"targetPlatform,omitempty"` + Version string `json:"version"` +} + +func (v Version) isUniversal() bool { + switch v.TargetPlatform { + case PlatformUniversal, PlatformUnknown, PlatformUndefined, "": + return true + default: + return false + } +} + +// Strings encodes the version and platform into a string that can be reversed +// by VersionFromString. For example 1.0.0@linux-x64. For universal versions +// the @platform will be omitted. +// +// For directory names it might have been ideal to a nested path such as +// `version/platform` but we use this instead for backwards compatibility since +// we were unpacking directly into the `version` directory. Otherwise, we would +// have to migrate existing extensions or have a mechanism for detecting in +// which format the extension was being stored. +func (v Version) String() string { + if v.isUniversal() { + return v.Version + } else { + return fmt.Sprintf("%s@%s", v.Version, v.TargetPlatform) + } +} + +// VersionFromString creates a version from a version directory. More or less it +// reverses Version.String(). Since @ is not allowed in semantic versions this +// should be future-proof. +func VersionFromString(dir string) Version { + parts := strings.SplitN(dir, "@", 2) + var platform Platform + if len(parts) > 1 { + platform = Platform(parts[1]) + } + return Version{ + Version: parts[0], + TargetPlatform: platform, + } +} + +// ByVersion implements sort.Interface for sorting Version slices. +type ByVersion []Version + +func (vs ByVersion) Len() int { return len(vs) } +func (vs ByVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } +func (vs ByVersion) Less(i, j int) bool { + // Go's semver library requires a v prefix. + cmp := semver.Compare("v"+vs[i].Version, "v"+vs[j].Version) + if cmp != 0 { + return cmp >= 0 + } + if vs[i].Version == vs[j].Version { + return vs[i].TargetPlatform < vs[j].TargetPlatform + } + return vs[i].Version >= vs[j].Version } type Storage interface { // AddExtension adds the provided VSIX into storage and returns the location - // for verification purposes. - AddExtension(ctx context.Context, manifest *VSIXManifest, vsix []byte) (string, error) + // for verification purposes. Extra files can be included, but not required. + // All extra files will be placed relative to the manifest outside the vsix. + AddExtension(ctx context.Context, manifest *VSIXManifest, vsix []byte, extra ...File) (string, error) // FileServer provides a handler for fetching extension repository files from // a client. FileServer() http.Handler // Manifest returns the manifest bytes for the provided extension. The // extension asset itself (the VSIX) will be included on the manifest even if // it does not exist on the manifest on disk. - Manifest(ctx context.Context, publisher, name, version string) (*VSIXManifest, error) + Manifest(ctx context.Context, publisher, name string, version Version) (*VSIXManifest, error) // RemoveExtension removes the provided version of the extension. It errors - // if the provided version does not exist or if removing it fails. If version - // is blank all versions of that extension will be removed. - RemoveExtension(ctx context.Context, publisher, name, version string) error + // if the version does not exist or if removing it fails. If both the version + // and platform are blank all versions of that extension will be removed. If + // only the platform is blank the universal version will be removed. If only + // the version is blank it will error; it is not currently possible to delete + // all versions for a specific platform. + RemoveExtension(ctx context.Context, publisher, name string, version Version) error // Versions returns the available versions of the provided extension in sorted // order. If the extension does not exits it returns an error. - Versions(ctx context.Context, publisher, name string) ([]string, error) + Versions(ctx context.Context, publisher, name string) ([]Version, error) // WalkExtensions applies a function over every extension. The extension // points to the latest version and the versions slice includes all the // versions in sorted order including the latest version (which will be in // [0]). If the function returns an error the error is immediately returned // which aborts the walk. - WalkExtensions(ctx context.Context, fn func(manifest *VSIXManifest, versions []string) error) error + WalkExtensions(ctx context.Context, fn func(manifest *VSIXManifest, versions []Version) error) error +} + +type File struct { + RelativePath string + Content []byte } const ArtifactoryTokenEnvKey = "ARTIFACTORY_TOKEN" @@ -138,28 +250,44 @@ func NewStorage(ctx context.Context, options *Options) (Storage, error) { return nil, xerrors.Errorf("cannot use both Artifactory and extension directory") } else if options.Artifactory != "" && options.Repo == "" { return nil, xerrors.Errorf("must provide repository") - } else if options.Artifactory != "" { + } + + var store Storage + var err error + switch { + case options.Artifactory != "": token := os.Getenv(ArtifactoryTokenEnvKey) if token == "" { return nil, xerrors.Errorf("the %s environment variable must be set", ArtifactoryTokenEnvKey) } - return NewArtifactoryStorage(ctx, &ArtifactoryOptions{ - ListCacheDuration: time.Minute, + store, err = NewArtifactoryStorage(ctx, &ArtifactoryOptions{ + ListCacheDuration: options.ListCacheDuration, Logger: options.Logger, Repo: options.Repo, Token: token, URI: options.Artifactory, }) - } else if options.ExtDir != "" { - return NewLocalStorage(options.ExtDir, options.Logger) + case options.ExtDir != "": + store, err = NewLocalStorage(&LocalOptions{ + ListCacheDuration: options.ListCacheDuration, + ExtDir: options.ExtDir, + }, options.Logger) + default: + return nil, xerrors.Errorf("must provide an Artifactory repository or local directory") + } + if err != nil { + return nil, err } - return nil, xerrors.Errorf("must provide an Artifactory repository or local directory") + + signingStorage := NewSignatureStorage(options.Logger, options.IncludeEmptySignatures, store) + + return signingStorage, nil } // ReadVSIXManifest reads and parses an extension manifest from a vsix file. If // the manifest is invalid it will be returned along with the validation error. func ReadVSIXManifest(vsix []byte) (*VSIXManifest, error) { - vmr, err := GetZipFileReader(vsix, "extension.vsixmanifest") + vmr, err := easyzip.GetZipFileReader(vsix, "extension.vsixmanifest") if err != nil { return nil, err } @@ -177,7 +305,21 @@ func parseVSIXManifest(reader io.Reader) (*VSIXManifest, error) { if err != nil { return nil, err } - return vm, validateManifest(vm) + err = validateManifest(vm) + if err != nil { + return vm, err + } + // The manifest stores these as capitalized space-delimited strings but we + // want to present them as lowercased comma-separated strings to VS Code. + // For example, "Public Preview" becomes "public, preview". Make sure to + // handle the case where they are already comma-separated, just in case. + flags := strings.Fields(vm.Metadata.GalleryFlags) + converted := make([]string, len(flags)) + for i, flag := range flags { + converted[i] = strings.ToLower(strings.TrimRight(flag, ",")) + } + vm.Metadata.GalleryFlags = strings.Join(converted, ", ") + return vm, nil } // validateManifest checks a manifest for issues. @@ -203,7 +345,7 @@ type VSIXPackageJSON struct { // ReadVSIXPackageJSON reads and parses an extension's package.json from a vsix // file. func ReadVSIXPackageJSON(vsix []byte, packageJsonPath string) (*VSIXPackageJSON, error) { - vpjr, err := GetZipFileReader(vsix, packageJsonPath) + vpjr, err := easyzip.GetZipFileReader(vsix, packageJsonPath) if err != nil { return nil, err } @@ -240,7 +382,8 @@ func ReadVSIX(ctx context.Context, source string) ([]byte, error) { }) } -// ExtensionIDFromManifest returns the full ID of an extension. +// ExtensionIDFromManifest returns the full ID of an extension without the the +// platform, for example publisher.name@0.0.1. func ExtensionIDFromManifest(manifest *VSIXManifest) string { return ExtensionID( manifest.Metadata.Identity.Publisher, @@ -248,18 +391,41 @@ func ExtensionIDFromManifest(manifest *VSIXManifest) string { manifest.Metadata.Identity.Version) } -// ExtensionID returns the full ID of an extension. +// ExtensionID returns the full ID of an extension without the platform, for +// example publisher.name@0.0.1. func ExtensionID(publisher, name, version string) string { + return fmt.Sprintf("%s.%s@%s", publisher, name, version) +} + +// ExtensionVSIXNameFromManifest returns the full ID of an extension including +// the platform if not universal, for example publisher.name-0.0.1 or +// publisher.name-0.0.1@linux-x64. +func ExtensionVSIXNameFromManifest(manifest *VSIXManifest) string { + return ExtensionVSIXName( + manifest.Metadata.Identity.Publisher, + manifest.Metadata.Identity.ID, + Version{ + Version: manifest.Metadata.Identity.Version, + TargetPlatform: manifest.Metadata.Identity.TargetPlatform, + }) +} + +// ExtensionVSIXName returns the full ID of an extension including the +// platform if not universal, for example publisher.name-0.0.1 or +// publisher.name-0.0.1@linux-x64. +func ExtensionVSIXName(publisher, name string, version Version) string { return fmt.Sprintf("%s.%s-%s", publisher, name, version) } -// ParseExtensionID parses an extension ID into its separate parts: publisher, -// name, and version (version may be blank). +// ParseExtensionID parses an full or partial extension ID into its separate +// parts: publisher, name, and version (version may be blank). It does not +// support specifying the platform and requires that the delimiter for the +// version be @. func ParseExtensionID(id string) (string, string, string, error) { - re := regexp.MustCompile(`^([^.]+)\.([^-]+)-?(.*)$`) + re := regexp.MustCompile(`^([^.]+)\.([^@]+)@?(.*)$`) match := re.FindAllStringSubmatch(id, -1) if match == nil { - return "", "", "", xerrors.Errorf("\"%s\" does not match . or .-", id) + return "", "", "", xerrors.Errorf("\"%s\" does not match . or .@", id) } return match[0][1], match[0][2], match[0][3], nil } diff --git a/storage/storage_test.go b/storage/storage_test.go index 812c60e..d3c4739 100644 --- a/storage/storage_test.go +++ b/storage/storage_test.go @@ -16,7 +16,6 @@ import ( "testing" "github.com/stretchr/testify/require" - "golang.org/x/mod/semver" "github.com/coder/code-marketplace/storage" "github.com/coder/code-marketplace/testutil" @@ -26,6 +25,8 @@ type testStorage struct { storage storage.Storage write func(content []byte, elem ...string) exists func(elem ...string) bool + + expectedManifest func(man *storage.VSIXManifest) } type storageFactory = func(t *testing.T) testStorage @@ -105,11 +106,13 @@ func TestNewStorage(t *testing.T) { require.Error(t, err) require.Regexp(t, test.error, err.Error()) } else if test.local { - _, ok := s.(*storage.Local) + under := s.(*storage.Signature) + _, ok := under.Storage.(*storage.Local) require.True(t, ok) require.NoError(t, err) } else { - _, ok := s.(*storage.Artifactory) + under := s.(*storage.Signature) + _, ok := under.Storage.(*storage.Artifactory) require.True(t, ok) require.NoError(t, err) } @@ -131,6 +134,14 @@ func TestStorage(t *testing.T) { name: "Artifactory", factory: artifactoryFactory, }, + { + name: "SignedLocal", + factory: signed(true, localFactory), + }, + { + name: "SignedArtifactory", + factory: signed(true, artifactoryFactory), + }, } for _, sf := range factories { t.Run(sf.name, func(t *testing.T) { @@ -214,46 +225,95 @@ func testManifest(t *testing.T, factory storageFactory) { tests := []struct { // error is the expected error, if any. error error + // expected is the manifest we should get back, if there is no error. + expected *storage.VSIXManifest // extension contains the expected manifest. extension testutil.Extension // name is the name of the test. name string - // version is the version to expect in the manifest. Defaults to the - // extension's latest version. - version string + // version is the version to use in the manifest request. + version storage.Version }{ { - name: "OK", + name: "PlatformDefault", + extension: testutil.Extensions[0], + version: storage.Version{Version: testutil.Extensions[0].LatestVersion}, + expected: testutil.ConvertExtensionToManifest(testutil.Extensions[0], + storage.Version{Version: testutil.Extensions[0].LatestVersion}), + }, + { + name: "PlatformUniversal", + extension: testutil.Extensions[0], + version: storage.Version{Version: testutil.Extensions[0].LatestVersion, + TargetPlatform: storage.PlatformUniversal}, + expected: testutil.ConvertExtensionToManifest(testutil.Extensions[0], + storage.Version{Version: testutil.Extensions[0].LatestVersion}), + }, + { + name: "PlatformUnknown", + extension: testutil.Extensions[0], + version: storage.Version{Version: testutil.Extensions[0].LatestVersion, + TargetPlatform: storage.PlatformUnknown}, + expected: testutil.ConvertExtensionToManifest(testutil.Extensions[0], + storage.Version{Version: testutil.Extensions[0].LatestVersion}), + }, + { + name: "PlatformUndefined", + extension: testutil.Extensions[0], + version: storage.Version{Version: testutil.Extensions[0].LatestVersion, + TargetPlatform: storage.PlatformUndefined}, + expected: testutil.ConvertExtensionToManifest(testutil.Extensions[0], + storage.Version{Version: testutil.Extensions[0].LatestVersion}), + }, + { + name: "PlatformLinuxX64", + extension: testutil.Extensions[0], + version: storage.Version{Version: testutil.Extensions[0].LatestVersion, + TargetPlatform: storage.PlatformLinuxX64}, + expected: testutil.ConvertExtensionToManifest(testutil.Extensions[0], + storage.Version{Version: testutil.Extensions[0].LatestVersion, + TargetPlatform: storage.PlatformLinuxX64}), + }, + { + name: "MissingPlatform", + error: fs.ErrNotExist, extension: testutil.Extensions[0], + version: storage.Version{TargetPlatform: storage.PlatformWeb}, }, { name: "MissingVersion", error: fs.ErrNotExist, extension: testutil.Extensions[0], - version: "some-nonexistent-version", + version: storage.Version{Version: "some-nonexistent@version"}, }, { name: "MissingExtension", error: fs.ErrNotExist, extension: testutil.Extensions[1], + version: storage.Version{Version: testutil.Extensions[1].LatestVersion}, }, { name: "MissingPublisher", error: fs.ErrNotExist, extension: testutil.Extensions[2], + version: storage.Version{Version: testutil.Extensions[2].LatestVersion}, }, { name: "ParseError", error: io.EOF, extension: testutil.Extensions[3], + version: storage.Version{Version: testutil.Extensions[3].LatestVersion}, }, } f := factory(t) ext := testutil.Extensions[0] - manifestBytes := testutil.ConvertExtensionToManifestBytes(t, ext, ext.LatestVersion) + manifestBytes := testutil.ConvertExtensionToManifestBytes(t, ext, storage.Version{Version: ext.LatestVersion}) f.write(manifestBytes, ext.Publisher, ext.Name, ext.LatestVersion, "extension.vsixmanifest") + manifestBytes = testutil.ConvertExtensionToManifestBytes(t, ext, storage.Version{Version: ext.LatestVersion, TargetPlatform: storage.PlatformLinuxX64}) + f.write(manifestBytes, ext.Publisher, ext.Name, ext.LatestVersion+"@linux-x64", "extension.vsixmanifest") + ext = testutil.Extensions[3] f.write([]byte("invalid"), ext.Publisher, ext.Name, ext.LatestVersion, "extension.vsixmanifest") @@ -261,25 +321,23 @@ func testManifest(t *testing.T, factory storageFactory) { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() - version := test.version - if version == "" { - version = test.extension.LatestVersion - } - manifest, err := f.storage.Manifest(context.Background(), test.extension.Publisher, test.extension.Name, version) + manifest, err := f.storage.Manifest(context.Background(), test.extension.Publisher, test.extension.Name, test.version) if test.error != nil { require.Error(t, err) require.True(t, errors.Is(err, test.error)) } else { - expected := testutil.ConvertExtensionToManifest(testutil.Extensions[0], version) + require.NoError(t, err) // The storage interface should add the extension asset when it reads the // manifest since it is not on the actual manifest on disk. - expected.Assets.Asset = append(expected.Assets.Asset, storage.VSIXAsset{ + test.expected.Assets.Asset = append(test.expected.Assets.Asset, storage.VSIXAsset{ Type: storage.VSIXAssetType, - Path: fmt.Sprintf("%s.%s-%s.vsix", test.extension.Publisher, test.extension.Name, version), + Path: fmt.Sprintf("%s.%s-%s.vsix", test.extension.Publisher, test.extension.Name, test.version), Addressable: "true", }) - require.NoError(t, err) - require.Equal(t, expected, manifest) + if f.expectedManifest != nil { + f.expectedManifest(test.expected) + } + require.Equal(t, test.expected, manifest) } }) } @@ -287,7 +345,7 @@ func testManifest(t *testing.T, factory storageFactory) { type extension struct { manifest *storage.VSIXManifest - versions []string + versions []storage.Version } func testWalkExtensions(t *testing.T, factory storageFactory) { @@ -301,7 +359,7 @@ func testWalkExtensions(t *testing.T, factory storageFactory) { // name is then ame of the test name string // run is an optional walk callback. - run func() error + run func(_ []storage.Version) error }{ { name: "OK", @@ -314,7 +372,7 @@ func testWalkExtensions(t *testing.T, factory storageFactory) { name: "PropagateError", error: "propagate", extensions: []testutil.Extension{testutil.Extensions[0]}, - run: func() error { + run: func(_ []storage.Version) error { return errors.New("propagate") }, }, @@ -327,10 +385,10 @@ func testWalkExtensions(t *testing.T, factory storageFactory) { f := factory(t) expected := []extension{} for _, ext := range test.extensions { - versions := make([]string, len(ext.Versions)) + versions := make([]storage.Version, len(ext.Versions)) copy(versions, ext.Versions) - sort.Sort(sort.Reverse(semver.ByVersion(versions))) - manifest := testutil.ConvertExtensionToManifest(ext, ext.LatestVersion) + sort.Sort(storage.ByVersion(versions)) + manifest := testutil.ConvertExtensionToManifest(ext, storage.Version{Version: ext.LatestVersion}) // The storage interface should add the extension asset when it reads the // manifest since it is not on the actual manifest on disk. manifest.Assets.Asset = append(manifest.Assets.Asset, storage.VSIXAsset{ @@ -343,17 +401,17 @@ func testWalkExtensions(t *testing.T, factory storageFactory) { versions: versions, }) for _, version := range ext.Versions { - f.write(testutil.ConvertExtensionToManifestBytes(t, ext, version), ext.Publisher, ext.Name, version, "extension.vsixmanifest") + f.write(testutil.ConvertExtensionToManifestBytes(t, ext, version), ext.Publisher, ext.Name, version.String(), "extension.vsixmanifest") } } got := []extension{} - err := f.storage.WalkExtensions(context.Background(), func(manifest *storage.VSIXManifest, versions []string) error { + err := f.storage.WalkExtensions(context.Background(), func(manifest *storage.VSIXManifest, versions []storage.Version) error { got = append(got, extension{ manifest: manifest, versions: versions, }) if test.run != nil { - return test.run() + return test.run(versions) } return nil }) @@ -363,6 +421,7 @@ func testWalkExtensions(t *testing.T, factory storageFactory) { } else { require.NoError(t, err) } + // Ignores the extension order, but the version order will matter. require.ElementsMatch(t, expected, got) }) } @@ -379,7 +438,7 @@ func TestReadVSIX(t *testing.T) { error string // expected is compared with the return VSIX. It is not checked if an // error is expected. - expected testutil.Extension + expected []byte // handler is the handler for the HTTP server returning the VSIX. By // default it returns the `expected` extension. handler http.HandlerFunc @@ -388,7 +447,11 @@ func TestReadVSIX(t *testing.T) { }{ { name: "OK", - expected: testutil.Extensions[0], + expected: testutil.CreateVSIXFromExtension(t, testutil.Extensions[0], storage.Version{Version: testutil.Extensions[0].LatestVersion}), + }, + { + name: "OKPlatform", + expected: testutil.CreateVSIXFromExtension(t, testutil.Extensions[0], storage.Version{Version: testutil.Extensions[0].LatestVersion, TargetPlatform: storage.PlatformLinuxX64}), }, { name: "InternalError", @@ -406,10 +469,10 @@ func TestReadVSIX(t *testing.T) { }, { name: "Redirect", - expected: testutil.Extensions[0], + expected: testutil.CreateVSIXFromExtension(t, testutil.Extensions[0], storage.Version{Version: testutil.Extensions[0].LatestVersion}), handler: func(rw http.ResponseWriter, r *http.Request) { if r.URL.Path == "/redirected" { - vsix := testutil.CreateVSIXFromExtension(t, testutil.Extensions[0]) + vsix := testutil.CreateVSIXFromExtension(t, testutil.Extensions[0], storage.Version{Version: testutil.Extensions[0].LatestVersion}) _, err := rw.Write(vsix) require.NoError(t, err) } else { @@ -434,8 +497,7 @@ func TestReadVSIX(t *testing.T) { handler := test.handler if handler == nil { handler = func(rw http.ResponseWriter, r *http.Request) { - vsix := testutil.CreateVSIXFromExtension(t, test.expected) - _, err := rw.Write(vsix) + _, err := rw.Write(test.expected) require.NoError(t, err) } } @@ -448,7 +510,7 @@ func TestReadVSIX(t *testing.T) { require.Error(t, err) require.Regexp(t, test.error, err.Error()) } else { - require.Equal(t, testutil.CreateVSIXFromExtension(t, test.expected), got) + require.Equal(t, test.expected, got) } }) } @@ -462,7 +524,7 @@ func TestReadVSIX(t *testing.T) { error error // expected is compared with the return VSIX. It is not checked if an // error is expected. - expected testutil.Extension + expected []byte // name is the name of the test. name string // skip indicates whether to skip the test since some failure modes are @@ -470,21 +532,28 @@ func TestReadVSIX(t *testing.T) { skip bool // source sets up the extension on disk and returns the path to that // extension. - source func(t *testing.T, extdir string) (string, error) + source func(t *testing.T, expected []byte, extdir string) (string, error) }{ { name: "OK", - expected: testutil.Extensions[0], - source: func(t *testing.T, extdir string) (string, error) { - vsix := testutil.CreateVSIXFromExtension(t, testutil.Extensions[0]) + expected: testutil.CreateVSIXFromExtension(t, testutil.Extensions[0], storage.Version{Version: testutil.Extensions[0].LatestVersion}), + source: func(t *testing.T, expected []byte, extdir string) (string, error) { vsixPath := filepath.Join(extdir, "extension.vsix") - return vsixPath, os.WriteFile(vsixPath, vsix, 0o644) + return vsixPath, os.WriteFile(vsixPath, expected, 0o644) + }, + }, + { + name: "OKPlatform", + expected: testutil.CreateVSIXFromExtension(t, testutil.Extensions[0], storage.Version{Version: testutil.Extensions[0].LatestVersion, TargetPlatform: storage.PlatformLinuxX64}), + source: func(t *testing.T, expected []byte, extdir string) (string, error) { + vsixPath := filepath.Join(extdir, "extension.vsix") + return vsixPath, os.WriteFile(vsixPath, expected, 0o644) }, }, { name: "NotFound", error: os.ErrNotExist, - source: func(t *testing.T, extdir string) (string, error) { + source: func(t *testing.T, _ []byte, extdir string) (string, error) { return filepath.Join(extdir, "foo.vsix"), nil }, }, @@ -494,7 +563,7 @@ func TestReadVSIX(t *testing.T) { // It does not appear possible to create a file that is not readable on // Windows? skip: runtime.GOOS == "windows", - source: func(t *testing.T, extdir string) (string, error) { + source: func(t *testing.T, _ []byte, extdir string) (string, error) { vsixPath := filepath.Join(extdir, "extension.vsix") return vsixPath, os.WriteFile(vsixPath, []byte{}, 0o222) }, @@ -510,7 +579,7 @@ func TestReadVSIX(t *testing.T) { } extdir := t.TempDir() - source, err := test.source(t, extdir) + source, err := test.source(t, test.expected, extdir) require.NoError(t, err) got, err := storage.ReadVSIX(context.Background(), source) @@ -518,7 +587,7 @@ func TestReadVSIX(t *testing.T) { require.Error(t, err) require.True(t, errors.Is(err, test.error)) } else { - require.Equal(t, testutil.CreateVSIXFromExtension(t, test.expected), got) + require.Equal(t, test.expected, got) } }) } @@ -531,6 +600,9 @@ func TestReadVSIXManifest(t *testing.T) { tests := []struct { // error is the expected error, if any. error string + // expected is the expected manifest. If not provided, check against + // `manifest` instead. + expected *storage.VSIXManifest // manifest is the manifest from which to create the VSIX. Use `vsix` to // specify raw bytes instead. manifest *storage.VSIXManifest @@ -553,6 +625,78 @@ func TestReadVSIXManifest(t *testing.T) { }, }, }, + { + name: "SpaceSeparatedFlags", + manifest: &storage.VSIXManifest{ + Metadata: storage.VSIXMetadata{ + Identity: storage.VSIXIdentity{ + Publisher: "foo", + ID: "bar", + Version: "baz", + }, + GalleryFlags: "Public Preview", + }, + }, + expected: &storage.VSIXManifest{ + Metadata: storage.VSIXMetadata{ + Identity: storage.VSIXIdentity{ + Publisher: "foo", + ID: "bar", + Version: "baz", + }, + GalleryFlags: "public, preview", + }, + }, + }, + { + name: "CommaSpaceSeparatedFlags", + manifest: &storage.VSIXManifest{ + Metadata: storage.VSIXMetadata{ + Identity: storage.VSIXIdentity{ + Publisher: "foo", + ID: "bar", + Version: "baz", + }, + GalleryFlags: "public, preview", + }, + }, + }, + { + name: "CommaSpaceSpaceSeparatedFlags", + manifest: &storage.VSIXManifest{ + Metadata: storage.VSIXMetadata{ + Identity: storage.VSIXIdentity{ + Publisher: "foo", + ID: "bar", + Version: "baz", + }, + GalleryFlags: "public, preview", + }, + }, + expected: &storage.VSIXManifest{ + Metadata: storage.VSIXMetadata{ + Identity: storage.VSIXIdentity{ + Publisher: "foo", + ID: "bar", + Version: "baz", + }, + GalleryFlags: "public, preview", + }, + }, + }, + { + name: "CommaSeparatedFlags", + manifest: &storage.VSIXManifest{ + Metadata: storage.VSIXMetadata{ + Identity: storage.VSIXIdentity{ + Publisher: "foo", + ID: "bar", + Version: "baz", + }, + GalleryFlags: "public,preview", + }, + }, + }, { name: "MissingManifest", error: "not found", @@ -616,8 +760,12 @@ func TestReadVSIXManifest(t *testing.T) { require.Error(t, err) require.Regexp(t, test.error, err.Error()) } else { + expected := test.expected + if expected == nil { + expected = test.manifest + } require.NoError(t, err) - require.Equal(t, test.manifest, manifest) + require.Equal(t, expected, manifest) } }) } @@ -702,22 +850,33 @@ func testAddExtension(t *testing.T, factory storageFactory) { extension testutil.Extension // name is the name of the test. name string + // version is the version of the extension to add. Use `vsix` to specify + // raw bytes instead. + version storage.Version // vsix contains the raw bytes of the extension to add. If omitted it will - // be created from `extension`. For non-error cases always use `extension` - // instead so we can check the result. + // be created from `extension` and `version`. For non-error cases always + // use `extension` instead so we can check the result. vsix []byte }{ { name: "OK", extension: testutil.Extensions[0], + version: storage.Version{Version: testutil.Extensions[0].LatestVersion}, + }, + { + name: "OKPlatform", + extension: testutil.Extensions[0], + version: storage.Version{Version: testutil.Extensions[0].LatestVersion, TargetPlatform: storage.PlatformLinuxX64}, }, { name: "EmptyDependencies", extension: testutil.Extensions[1], + version: storage.Version{Version: testutil.Extensions[1].LatestVersion}, }, { name: "NoDependencies", extension: testutil.Extensions[2], + version: storage.Version{Version: testutil.Extensions[2].LatestVersion}, }, { name: "InvalidZip", @@ -727,6 +886,7 @@ func testAddExtension(t *testing.T, factory storageFactory) { { name: "CopyOverDirectory", extension: testutil.Extensions[3], + version: storage.Version{Version: testutil.Extensions[3].LatestVersion}, error: "is a directory|found a folder", }, } @@ -744,7 +904,7 @@ func testAddExtension(t *testing.T, factory storageFactory) { expected := &storage.VSIXManifest{} vsix := test.vsix if vsix == nil { - expected = testutil.ConvertExtensionToManifest(test.extension, test.extension.LatestVersion) + expected = testutil.ConvertExtensionToManifest(test.extension, test.version) vsix = testutil.CreateVSIXFromManifest(t, expected) } location, err := f.storage.AddExtension(context.Background(), expected, vsix) @@ -757,8 +917,11 @@ func testAddExtension(t *testing.T, factory storageFactory) { require.Contains(t, location, test.extension.Publisher) require.Contains(t, location, test.extension.Name) require.Contains(t, location, test.extension.LatestVersion) + if test.version.TargetPlatform != "" { + require.Contains(t, location, test.version.TargetPlatform) + } // There should be a manifest now. - require.True(t, f.exists(test.extension.Publisher, test.extension.Name, test.extension.LatestVersion, "extension.vsixmanifest")) + require.True(t, f.exists(test.extension.Publisher, test.extension.Name, test.version.String(), "extension.vsixmanifest")) } }) } @@ -776,37 +939,51 @@ func testRemoveExtension(t *testing.T, factory storageFactory) { // name is the name of the test. name string // version is the version to remove. - version string + version storage.Version }{ { name: "OK", extension: testutil.Extensions[0], - version: testutil.Extensions[0].LatestVersion, + version: storage.Version{Version: testutil.Extensions[0].LatestVersion}, }, { name: "NoVersionMatch", error: os.ErrNotExist, extension: testutil.Extensions[0], - version: "does-not-exist", + version: storage.Version{Version: "does-not-exist"}, }, { name: "NoPublisherMatch", error: os.ErrNotExist, // [3]'s publisher does not exist. extension: testutil.Extensions[3], - version: testutil.Extensions[3].LatestVersion, + version: storage.Version{Version: testutil.Extensions[3].LatestVersion}, }, { name: "NoNameMatch", error: os.ErrNotExist, // [1] shares a publisher with [0] but the extension does not exist. extension: testutil.Extensions[1], - version: testutil.Extensions[1].LatestVersion, + version: storage.Version{Version: testutil.Extensions[1].LatestVersion}, + }, + { + name: "NoPlatformMatch", + error: os.ErrNotExist, + extension: testutil.Extensions[1], + version: storage.Version{Version: testutil.Extensions[1].LatestVersion, TargetPlatform: storage.PlatformWeb}, + }, + { + // We could support this as a way to delete all versions belonging to a + // specific platform, but for now it is an error. + name: "NoVersion", + error: os.ErrNotExist, + extension: testutil.Extensions[1], + version: storage.Version{TargetPlatform: storage.PlatformWeb}, }, { name: "All", extension: testutil.Extensions[0], - version: "", + version: storage.Version{}, }, } @@ -818,7 +995,7 @@ func testRemoveExtension(t *testing.T, factory storageFactory) { f := factory(t) for _, ext := range []testutil.Extension{testutil.Extensions[0], testutil.Extensions[2]} { for _, version := range ext.Versions { - f.write(testutil.ConvertExtensionToManifestBytes(t, ext, version), ext.Publisher, ext.Name, version, "extension.vsixmanifest") + f.write(testutil.ConvertExtensionToManifestBytes(t, ext, version), ext.Publisher, ext.Name, version.String(), "extension.vsixmanifest") } } @@ -830,9 +1007,9 @@ func testRemoveExtension(t *testing.T, factory storageFactory) { require.NoError(t, err) // If a version was specified the parent extension directory should // still exist otherwise the whole thing should have been removed. - if test.version != "" { + if test.version.Version != "" { require.True(t, f.exists(test.extension.Publisher, test.extension.Name)) - require.False(t, f.exists(test.extension.Publisher, test.extension.Name, test.version)) + require.False(t, f.exists(test.extension.Publisher, test.extension.Name, test.version.String())) } else { require.False(t, f.exists(test.extension.Publisher, test.extension.Name)) } @@ -873,7 +1050,7 @@ func testVersions(t *testing.T, factory storageFactory) { f := factory(t) ext := testutil.Extensions[0] for _, version := range ext.Versions { - f.write([]byte("stub"), ext.Publisher, ext.Name, version, "extension.vsixmanifest") + f.write([]byte("stub"), ext.Publisher, ext.Name, version.String(), "extension.vsixmanifest") } for _, test := range tests { @@ -887,10 +1064,7 @@ func testVersions(t *testing.T, factory storageFactory) { require.True(t, errors.Is(err, test.error)) } else { require.NoError(t, err) - versions := make([]string, len(test.extension.Versions)) - copy(versions, test.extension.Versions) - sort.Sort(sort.Reverse(semver.ByVersion(versions))) - require.Equal(t, versions, got) + require.True(t, sort.IsSorted(storage.ByVersion(got))) } }) } @@ -909,7 +1083,7 @@ func TestExtensionID(t *testing.T) { }{ { name: "OK", - expected: "foo.bar-test", + expected: "foo.bar@test", manifest: &storage.VSIXManifest{ Metadata: storage.VSIXMetadata{ Identity: storage.VSIXIdentity{ @@ -931,6 +1105,56 @@ func TestExtensionID(t *testing.T) { } } +func TestExtensionVSIXNameWithPlatform(t *testing.T) { + t.Parallel() + + tests := []struct { + // expected is the expected VSIX name. + expected string + // manifest is the manifest from which to build the ID. + manifest *storage.VSIXManifest + // name is the name of the test. + name string + }{ + { + name: "NoPlatform", + expected: "foo.bar-1.0.0", + manifest: &storage.VSIXManifest{ + Metadata: storage.VSIXMetadata{ + Identity: storage.VSIXIdentity{ + Publisher: "foo", + ID: "bar", + Version: "1.0.0", + }, + }, + }, + }, + { + name: "PlatformWeb", + expected: "foo.bar-1.0.0@web", + manifest: &storage.VSIXManifest{ + Metadata: storage.VSIXMetadata{ + Identity: storage.VSIXIdentity{ + Publisher: "foo", + ID: "bar", + Version: "1.0.0", + TargetPlatform: storage.PlatformWeb, + }, + }, + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, test.expected, + storage.ExtensionVSIXNameFromManifest(test.manifest)) + }) + } +} + func TestParseExtensionID(t *testing.T) { t.Parallel() @@ -948,12 +1172,12 @@ func TestParseExtensionID(t *testing.T) { { name: "OK", expected: []string{"foo", "bar", "test"}, - id: "foo.bar-test", + id: "foo.bar@test", }, { name: "VersionWithDots", expected: []string{"foo", "bar", "test.test"}, - id: "foo.bar-test.test", + id: "foo.bar@test.test", }, { name: "EmptyID", @@ -963,12 +1187,12 @@ func TestParseExtensionID(t *testing.T) { { name: "MissingPublisher", error: true, - id: ".qux-bar", + id: ".qux@bar", }, { name: "MissingExtension", error: true, - id: "foo.-baz", + id: "foo.@baz", }, { name: "MissingExtensionAndVersion", @@ -983,7 +1207,7 @@ func TestParseExtensionID(t *testing.T) { { name: "InvalidID", error: true, - id: "publisher-version", + id: "publisher@version", }, } @@ -1001,3 +1225,53 @@ func TestParseExtensionID(t *testing.T) { }) } } + +func TestSortByVersion(t *testing.T) { + t.Parallel() + + versions := make([]storage.Version, len(testutil.Extensions[0].Versions)) + copy(versions, testutil.Extensions[0].Versions) + tests := []struct { + name string + versions []storage.Version + expected []storage.Version + }{ + { + name: "Compare", + versions: versions, + expected: []storage.Version{ + {Version: "3.0.0"}, + {Version: "3.0.0", TargetPlatform: storage.PlatformAlpineX64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformDarwinX64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformLinuxArm64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformLinuxX64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformWin32X64}, + {Version: "2.2.2"}, + {Version: "2.0.0"}, + {Version: "1.5.2"}, + {Version: "1.0.0"}, + {Version: "1.0.0", TargetPlatform: storage.PlatformWin32X64}, + }, + }, + { + name: "CompareMSPythonStyle", + versions: []storage.Version{ + {Version: "2023.9.1102792234"}, + {Version: "2023.10.1002811100"}, + }, + expected: []storage.Version{ + {Version: "2023.10.1002811100"}, + {Version: "2023.9.1102792234"}, + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + sort.Sort(storage.ByVersion(test.versions)) + require.Equal(t, test.expected, test.versions) + }) + } +} diff --git a/testutil/extensions.go b/testutil/extensions.go index 6b730bd..a32967e 100644 --- a/testutil/extensions.go +++ b/testutil/extensions.go @@ -20,12 +20,19 @@ type Extension struct { Properties []storage.VSIXProperty Description string Categories string - Versions []string + Versions []storage.Version LatestVersion string Dependencies []string Pack []string } +func (e Extension) Copy() Extension { + var n Extension + data, _ := json.Marshal(e) + _ = json.Unmarshal(data, &n) + return n +} + var Extensions = []Extension{ { Publisher: "foo", @@ -47,7 +54,19 @@ var Extensions = []Extension{ Value: "d.e", }, }, - Versions: []string{"1.0.0", "2.0.0", "3.0.0", "1.5.2", "2.2.2"}, + Versions: []storage.Version{ + {Version: "1.0.0"}, + {Version: "1.0.0", TargetPlatform: storage.PlatformWin32X64}, + {Version: "2.0.0"}, + {Version: "3.0.0"}, + {Version: "3.0.0", TargetPlatform: storage.PlatformLinuxX64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformLinuxArm64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformWin32X64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformAlpineX64}, + {Version: "3.0.0", TargetPlatform: storage.PlatformDarwinX64}, + {Version: "1.5.2"}, + {Version: "2.2.2"}, + }, LatestVersion: "3.0.0", Dependencies: []string{"d.e"}, Pack: []string{"a.b", "b.c"}, @@ -68,7 +87,7 @@ var Extensions = []Extension{ Value: "", }, }, - Versions: []string{"version1"}, + Versions: []storage.Version{{Version: "version1"}}, LatestVersion: "version1", }, { @@ -77,7 +96,7 @@ var Extensions = []Extension{ Description: "squigly foo and more foo bar baz", Tags: "tag1,tag2", Categories: "category1,category2", - Versions: []string{"version1", "version2"}, + Versions: []storage.Version{{Version: "version1"}, {Version: "version2"}}, LatestVersion: "version2", }, { @@ -86,7 +105,7 @@ var Extensions = []Extension{ Description: "frobbles the frobnozzle", Tags: "tag3,tag4,tag5", Categories: "category1", - Versions: []string{"version1", "version2"}, + Versions: []storage.Version{{Version: "version1"}, {Version: "version2"}}, LatestVersion: "version2", }, { @@ -95,18 +114,20 @@ var Extensions = []Extension{ Description: "qqqqqqqqqqqqqqqqqqq", Tags: "qq,qqq,qqqq", Categories: "q", - Versions: []string{"qqq", "q"}, + Versions: []storage.Version{{Version: "qqq"}, {Version: "q"}}, LatestVersion: "qqq", }, } -func ConvertExtensionToManifest(ext Extension, version string) *storage.VSIXManifest { +func ConvertExtensionToManifest(ext Extension, version storage.Version) *storage.VSIXManifest { + ext = ext.Copy() return &storage.VSIXManifest{ Metadata: storage.VSIXMetadata{ Identity: storage.VSIXIdentity{ - ID: ext.Name, - Version: version, - Publisher: ext.Publisher, + ID: ext.Name, + Version: version.Version, + Publisher: ext.Publisher, + TargetPlatform: version.TargetPlatform, }, Properties: storage.VSIXProperties{ Property: ext.Properties, @@ -121,7 +142,7 @@ func ConvertExtensionToManifest(ext Extension, version string) *storage.VSIXMani } } -func ConvertExtensionToManifestBytes(t *testing.T, ext Extension, version string) []byte { +func ConvertExtensionToManifestBytes(t *testing.T, ext Extension, version storage.Version) []byte { manifestBytes, err := xml.Marshal(ConvertExtensionToManifest(ext, version)) require.NoError(t, err) return manifestBytes @@ -171,6 +192,6 @@ func CreateVSIXFromPackageJSON(t *testing.T, packageJSON *storage.VSIXPackageJSO // CreateVSIXFromExtension returns the bytes for a VSIX file containing the // manifest for the provided test extension and an icon. -func CreateVSIXFromExtension(t *testing.T, ext Extension) []byte { - return CreateVSIXFromManifest(t, ConvertExtensionToManifest(ext, ext.LatestVersion)) +func CreateVSIXFromExtension(t *testing.T, ext Extension, version storage.Version) []byte { + return CreateVSIXFromManifest(t, ConvertExtensionToManifest(ext, version)) } diff --git a/testutil/extensions_test.go b/testutil/extensions_test.go index 9653d51..7dae0c5 100644 --- a/testutil/extensions_test.go +++ b/testutil/extensions_test.go @@ -5,13 +5,22 @@ import ( "github.com/stretchr/testify/require" + "github.com/coder/code-marketplace/storage" "github.com/coder/code-marketplace/testutil" ) func TestConvert(t *testing.T) { ext := testutil.Extensions[0] - manifest := testutil.ConvertExtensionToManifest(ext, "a") + + manifest := testutil.ConvertExtensionToManifest(ext, storage.Version{Version: "a"}) + require.Equal(t, manifest.Metadata.Identity.ID, ext.Name) + require.Equal(t, manifest.Metadata.Identity.Publisher, ext.Publisher) + require.Equal(t, manifest.Metadata.Identity.Version, "a") + require.Equal(t, manifest.Metadata.Identity.TargetPlatform, storage.Platform("")) + + manifest = testutil.ConvertExtensionToManifest(ext, storage.Version{Version: "a", TargetPlatform: storage.PlatformDarwinX64}) require.Equal(t, manifest.Metadata.Identity.ID, ext.Name) require.Equal(t, manifest.Metadata.Identity.Publisher, ext.Publisher) require.Equal(t, manifest.Metadata.Identity.Version, "a") + require.Equal(t, manifest.Metadata.Identity.TargetPlatform, storage.PlatformDarwinX64) } diff --git a/testutil/mockdb.go b/testutil/mockdb.go index 2a1983b..781bdd1 100644 --- a/testutil/mockdb.go +++ b/testutil/mockdb.go @@ -31,7 +31,7 @@ func (db *MockDB) GetExtensionAssetPath(ctx context.Context, asset *database.Ass if asset.Type == storage.VSIXAssetType { assetPath = "extension.vsix" } - return strings.Join([]string{baseURL.Path, "files", asset.Publisher, asset.Extension, asset.Version, assetPath}, "/"), nil + return strings.Join([]string{baseURL.Path, "files", asset.Publisher, asset.Extension, asset.Version.String(), assetPath}, "/"), nil } func (db *MockDB) GetExtensions(ctx context.Context, filter database.Filter, flags database.Flag, baseURL url.URL) ([]*database.Extension, int, error) { diff --git a/testutil/mockstorage.go b/testutil/mockstorage.go index 41cc99a..c1f6249 100644 --- a/testutil/mockstorage.go +++ b/testutil/mockstorage.go @@ -5,10 +5,13 @@ import ( "errors" "net/http" "os" + "sort" "github.com/coder/code-marketplace/storage" ) +var _ storage.Storage = (*MockStorage)(nil) + // MockStorage implements storage.Storage for tests. type MockStorage struct{} @@ -16,7 +19,7 @@ func NewMockStorage() *MockStorage { return &MockStorage{} } -func (s *MockStorage) AddExtension(ctx context.Context, manifest *storage.VSIXManifest, vsix []byte) (string, error) { +func (s *MockStorage) AddExtension(ctx context.Context, manifest *storage.VSIXManifest, vsix []byte, extra ...storage.File) (string, error) { return "", errors.New("not implemented") } @@ -30,11 +33,13 @@ func (s *MockStorage) FileServer() http.Handler { }) } -func (s *MockStorage) Manifest(ctx context.Context, publisher, name, version string) (*storage.VSIXManifest, error) { +func (s *MockStorage) Manifest(ctx context.Context, publisher, name string, version storage.Version) (*storage.VSIXManifest, error) { for _, ext := range Extensions { if ext.Publisher == publisher && ext.Name == name { for _, ver := range ext.Versions { - if ver == version { + // Use the string encoding to match since that is how the real storage + // implementations will do it too. + if ver.String() == version.String() { return ConvertExtensionToManifest(ext, ver), nil } } @@ -44,19 +49,22 @@ func (s *MockStorage) Manifest(ctx context.Context, publisher, name, version str return nil, os.ErrNotExist } -func (s *MockStorage) RemoveExtension(ctx context.Context, publisher, name, version string) error { +func (s *MockStorage) RemoveExtension(ctx context.Context, publisher, name string, version storage.Version) error { return errors.New("not implemented") } -func (s *MockStorage) WalkExtensions(ctx context.Context, fn func(manifest *storage.VSIXManifest, versions []string) error) error { +func (s *MockStorage) WalkExtensions(ctx context.Context, fn func(manifest *storage.VSIXManifest, versions []storage.Version) error) error { for _, ext := range Extensions { - if err := fn(ConvertExtensionToManifest(ext, ext.Versions[0]), ext.Versions); err != nil { + versions := make([]storage.Version, len(ext.Versions)) + copy(versions, ext.Versions) + sort.Sort(storage.ByVersion(versions)) + if err := fn(ConvertExtensionToManifest(ext, versions[0]), versions); err != nil { return nil } } return nil } -func (s *MockStorage) Versions(ctx context.Context, publisher, name string) ([]string, error) { +func (s *MockStorage) Versions(ctx context.Context, publisher, name string) ([]storage.Version, error) { return nil, errors.New("not implemented") } diff --git a/util/util.go b/util/util.go index 5f54363..9572480 100644 --- a/util/util.go +++ b/util/util.go @@ -14,11 +14,17 @@ func Plural(count int, singular, plural string) string { return strconv.Itoa(count) + " " + plural } -func Contains(a []string, b string) bool { - for _, astr := range a { - if astr == b { +func ContainsCompare[T any](haystack []T, needle T, equal func(a, b T) bool) bool { + for _, hay := range haystack { + if equal(needle, hay) { return true } } return false } + +func Contains[T comparable](haystack []T, needle T) bool { + return ContainsCompare(haystack, needle, func(a, b T) bool { + return a == b + }) +}