├── .gitignore ├── Makefile ├── NOTICE ├── sdk ├── helpers_test.go ├── product_test.go ├── major_versions_test.go ├── dlg_header_test.go ├── eula_test.go ├── eula.go ├── account_test.go ├── validators.go ├── major_versions.go ├── dlg_list.go ├── login_test.go ├── account.go ├── dlg_list_test.go ├── dlg_header.go ├── product.go ├── download.go ├── versions.go ├── versions_test.go ├── dlg_details.go ├── dlg_details_test.go ├── login.go ├── subproduct_test.go ├── download_test.go └── subproducts.go ├── .github └── workflows │ └── tests.yml ├── go.mod ├── README.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── LICENSE └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | __debug_bin* 3 | .envrc 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | TEST_ARGS ?= -v -count=1 -timeout 120s 4 | 5 | test: 6 | go test $(TEST_ARGS) ./... 7 | 8 | test.debug: 9 | go test -tags debug $(TEST_ARGS) ./... 10 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2022 VMware, Inc. 2 | 3 | This product is licensed to you under the Apache License, V2.0 (the "License"). You may not use this product except in compliance with the License. 4 | 5 | This product may include a number of subcomponents with separate copyright notices and license terms. Your use of these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file. -------------------------------------------------------------------------------- /sdk/helpers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "net/http" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | var err error 13 | 14 | // Print contents of object 15 | // b, _ := json.Marshal(data) 16 | // fmt.Println(string(b)) 17 | 18 | var basicClient = Client{HttpClient: &http.Client{}} 19 | 20 | func mustEnv(t *testing.T, k string) string { 21 | t.Helper() 22 | 23 | if v, ok := os.LookupEnv(k); ok { 24 | return v 25 | } 26 | 27 | t.Fatalf("expected environment variable %q", k) 28 | return "" 29 | } 30 | -------------------------------------------------------------------------------- /sdk/product_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | ) 10 | 11 | func TestGetProducts(t *testing.T) { 12 | var products []MajorProducts 13 | products, err = basicClient.GetProductsSlice() 14 | assert.Nil(t, err) 15 | assert.Greater(t, len(products), 80, "Expected response to contain at least 80 items") 16 | } 17 | 18 | func TestGetProductMap(t *testing.T) { 19 | var products map[string]ProductDetails 20 | products, err = basicClient.GetProductsMap() 21 | assert.Nil(t, err) 22 | assert.Contains(t, products, "vmware_tools") 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: tests 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | push: 9 | branches: 10 | - main 11 | 12 | env: 13 | VMWCC_USER: ${{ secrets.VMWCC_USER }} 14 | VMWCC_PASS: ${{ secrets.VMWCC_PASS }} 15 | 16 | jobs: 17 | tests: 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest, macos-latest, macos-14, windows-latest] 21 | 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - name: Set up Go 25 | uses: actions/setup-go@v3 26 | with: 27 | go-version: 1.21.4 28 | 29 | - name: Checkout Code 30 | uses: actions/checkout@v4 31 | - name: Run Golang Tests 32 | run: go test -v ./... 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vmware-labs/vmware-customer-connect-sdk 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/andybalholm/cascadia v1.3.2 7 | github.com/orirawlings/persistent-cookiejar v0.3.2 8 | github.com/stretchr/testify v1.7.0 9 | golang.org/x/net v0.19.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.0 // indirect 14 | github.com/google/go-cmp v0.5.5 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | go4.org v0.0.0-20230225012048-214862532bf5 // indirect 17 | golang.org/x/sys v0.15.0 // indirect 18 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 19 | gopkg.in/retry.v1 v1.0.3 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /sdk/major_versions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGetMajorVersionsSuccess(t *testing.T) { 13 | var majorVersions []string 14 | majorVersions, err = basicClient.GetMajorVersionsSlice("vmware_tools") 15 | assert.Nil(t, err) 16 | assert.Greater(t, len(majorVersions), 1, "Expected response to contain at least 1 item") 17 | assert.Contains(t, majorVersions, "11_x") 18 | } 19 | 20 | func TestGetMajorVersionsInvalidSlug(t *testing.T) { 21 | var majorVersions []string 22 | majorVersions, err = basicClient.GetMajorVersionsSlice("mware_tools") 23 | assert.ErrorIs(t, err, ErrorInvalidSlug) 24 | assert.Empty(t, majorVersions, "Expected response to be empty") 25 | } 26 | -------------------------------------------------------------------------------- /sdk/dlg_header_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGetHeaderSuccess(t *testing.T) { 13 | var dlgHeader DlgHeader 14 | dlgHeader, err = basicClient.GetDlgHeader("VMTOOLS1130", "1073") 15 | assert.Nil(t, err) 16 | assert.Equal(t, dlgHeader.Product.Productmap, "vmware_tools", "Expected product name vmware_tools") 17 | } 18 | 19 | func TestGetHeaderInvalidProductId(t *testing.T) { 20 | var dlgHeader DlgHeader 21 | dlgHeader, err = basicClient.GetDlgHeader("VMTOOLS1130", "666666") 22 | assert.ErrorIs(t, err, ErrorDlgHeader) 23 | assert.Empty(t, dlgHeader, "Expected response to be empty") 24 | } 25 | 26 | func TestGetHeaderInvalidDownloadGroup(t *testing.T) { 27 | var dlgHeader DlgHeader 28 | dlgHeader, err = basicClient.GetDlgHeader("VMTOOLS666", "1073") 29 | assert.ErrorIs(t, err, ErrorDlgHeader) 30 | assert.Empty(t, dlgHeader, "Expected response to be empty") 31 | } 32 | -------------------------------------------------------------------------------- /sdk/eula_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestFetchEulaLink(t *testing.T) { 14 | err = ensureLogin(t) 15 | require.Nil(t, err) 16 | 17 | var eulaUrl string 18 | eulaUrl, err = authenticatedClient.FetchEulaUrl("VMTOOLS1235", "1259") 19 | assert.Nil(t, err) 20 | assert.NotEmpty(t, eulaUrl, "Expected eulaUrl not be empty") 21 | } 22 | 23 | func TestFetchEulaLinkInvalidCode(t *testing.T) { 24 | err = ensureLogin(t) 25 | assert.Nil(t, err) 26 | 27 | var eulaUrl string 28 | eulaUrl, err = authenticatedClient.FetchEulaUrl("VMTOOLS1235", "9999") 29 | assert.NotNil(t, err) 30 | assert.ErrorIs(t, err, ErrorDlgDetailsInputs) 31 | assert.Empty(t, eulaUrl, "Expected eulaUrl be empty") 32 | } 33 | 34 | func TestAcceptEula(t *testing.T) { 35 | err = ensureLogin(t) 36 | require.Nil(t, err) 37 | 38 | err = authenticatedClient.AcceptEula("VMTOOLS1235", "1259") 39 | assert.Nil(t, err) 40 | } 41 | -------------------------------------------------------------------------------- /sdk/eula.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | ) 11 | 12 | const ( 13 | eulaURL = baseURL + "/channel/api/v1.0/dlg/eula/accept" 14 | ) 15 | 16 | var ErrorEulaInputs = errors.New("eula: downloadGroup or productId invalid") 17 | 18 | func (c *Client) FetchEulaUrl(downloadGroup, productId string) (url string, err error) { 19 | if err = c.CheckLoggedIn(); err != nil { 20 | return 21 | } 22 | 23 | var dlgDetails DlgDetails 24 | dlgDetails, err = c.GetDlgDetails(downloadGroup, productId) 25 | if err != nil { 26 | return 27 | } 28 | 29 | url = dlgDetails.EulaResponse.EulaURL 30 | 31 | return 32 | } 33 | 34 | func (c *Client) AcceptEula(downloadGroup, productId string) (err error) { 35 | if err = c.CheckLoggedIn(); err != nil { 36 | return 37 | } 38 | 39 | search_string := fmt.Sprintf("?downloadGroup=%s&productId=%s", downloadGroup, productId) 40 | var res *http.Response 41 | res, err = c.HttpClient.Get(eulaURL + search_string) 42 | if err != nil { 43 | return 44 | } 45 | defer res.Body.Close() 46 | 47 | if res.StatusCode == 400 { 48 | err = ErrorEulaInputs 49 | return 50 | } else if res.StatusCode != 200 { 51 | err = ErrorNon200Response 52 | } 53 | 54 | return 55 | } 56 | -------------------------------------------------------------------------------- /sdk/account_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestAccountInfo(t *testing.T) { 14 | err = ensureLogin(t) 15 | require.Nil(t, err) 16 | 17 | var accountInfo AccountInfo 18 | accountInfo, err = authenticatedClient.AccountInfo() 19 | t.Log(accountInfo) 20 | assert.Nil(t, err) 21 | } 22 | 23 | func TestAccountInfoNotLoggedIn(t *testing.T) { 24 | var accountInfo AccountInfo 25 | accountInfo, err = basicClient.AccountInfo() 26 | assert.ErrorIs(t, err, ErrorNotAuthorized) 27 | assert.Empty(t, accountInfo, "Expected response to be empty") 28 | } 29 | 30 | func TestCurrentUser(t *testing.T) { 31 | err = ensureLogin(t) 32 | require.Nil(t, err) 33 | var currentUser CurrentUser 34 | currentUser, err = authenticatedClient.CurrentUser() 35 | t.Log(currentUser) 36 | assert.Nil(t, err) 37 | } 38 | 39 | func TestCurrentUserNotLoggedIn(t *testing.T) { 40 | _, err = basicClient.CurrentUser() 41 | assert.ErrorIs(t, err, ErrorNotAuthorized) 42 | } 43 | 44 | // func TestInvalidProxy(t *testing.T) { 45 | // os.Setenv("HTTPS_PROXY", "http://NOT_A_PROXY") 46 | // defer os.Unsetenv("HTTPS_PROXY") 47 | // err := sdk.CheckConnectivity() 48 | // if err == nil { 49 | // t.Errorf("Did not generate error when invalid proxy set") 50 | // } 51 | // } 52 | -------------------------------------------------------------------------------- /sdk/validators.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "errors" 8 | "net/http" 9 | ) 10 | 11 | var ErrorInvalidSlug = errors.New("api: slug is not valid") 12 | var ErrorInvalidCategory = errors.New("api: category is not valid") 13 | var ErrorInvalidVersion = errors.New("api: version is not valid") 14 | var ErrorServerError = errors.New("api: server down. 500 error received") 15 | 16 | func (c *Client) validateSlugCategoryVersion(slug, category, majorVersion string) (err error) { 17 | c.EnsureProductDetailMap() 18 | if err != nil { 19 | return 20 | } 21 | 22 | if _, ok := ProductDetailMap[slug]; !ok { 23 | err = ErrorInvalidSlug 24 | return 25 | } 26 | 27 | if ProductDetailMap[slug].LatestMajorVersion != majorVersion { 28 | err = ErrorInvalidVersion 29 | return 30 | } 31 | return 32 | // TODO add check for non-latest version 33 | // Potential for circular dependency as this validator used by version get command 34 | } 35 | 36 | func (c *Client) validateResponseSlugCategoryVersion(slug, category, majorVersion string, res http.Response) (err error) { 37 | if res.StatusCode == 400 { 38 | err = c.validateSlugCategoryVersion(slug, category, majorVersion) 39 | return 40 | } 41 | return 42 | } 43 | 44 | func (c *Client) validateResponseGeneric(resCode int) (err error) { 45 | if resCode == 401 { 46 | err = ErrorNotAuthorized 47 | return 48 | } else if resCode == 500 { 49 | err = ErrorServerError 50 | return 51 | } else if resCode != 200 { 52 | err = ErrorNon200Response 53 | return 54 | } 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /sdk/major_versions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | const ( 13 | majorVersionsURL = baseURL + "/channel/public/api/v1.0/products/getProductHeader" 14 | ) 15 | 16 | type ProductVersions struct { 17 | MajorVersions []MajorVersions `json:"versions"` 18 | Resources []interface{} `json:"resources"` 19 | } 20 | type MajorVersions struct { 21 | ID string `json:"id"` 22 | } 23 | 24 | var ErrorInvalidMajorVersion = errors.New("major version not found") 25 | 26 | // curl "https://customerconnect.vmware.com/channel/public/api/v1.0/products/getProductHeader?category=datacenter_cloud_infrastructure&product=vmware_vsphere_storage_appliance&version=5_5" |jq 27 | func (c *Client) GetMajorVersionsSlice(slug string) (data []string, err error) { 28 | c.EnsureProductDetailMap() 29 | if err != nil { 30 | return 31 | } 32 | 33 | if _, ok := ProductDetailMap[slug]; !ok { 34 | err = ErrorInvalidSlug 35 | return 36 | } 37 | 38 | search_string := fmt.Sprintf("?category=%s&product=%s&version=%s", 39 | ProductDetailMap[slug].Category, slug, ProductDetailMap[slug].LatestMajorVersion) 40 | 41 | res, err := c.HttpClient.Get(majorVersionsURL + search_string) 42 | if err != nil { 43 | return 44 | } 45 | defer res.Body.Close() 46 | 47 | var productVersions ProductVersions 48 | err = json.NewDecoder(res.Body).Decode(&productVersions) 49 | 50 | if err == nil { 51 | for _, version := range productVersions.MajorVersions { 52 | data = append(data, version.ID) 53 | } 54 | } 55 | 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /sdk/dlg_list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "net/http" 10 | ) 11 | 12 | // dlg list 13 | type DlgEditions struct { 14 | DlgEditionsLists []DlgEditionsLists `json:"dlgEditionsLists"` 15 | } 16 | type DlgList struct { 17 | Name string `json:"name"` 18 | Code string `json:"code"` 19 | ProductID string `json:"productId"` 20 | ReleaseDate string `json:"releaseDate"` 21 | ReleasePackageID string `json:"releasePackageId"` 22 | } 23 | type DlgEditionsLists struct { 24 | Name string `json:"name"` 25 | DlgList []DlgList `json:"dlgList"` 26 | OrderID int `json:"orderId"` 27 | } 28 | 29 | const ( 30 | dlgListURL = baseURL + "/channel/public/api/v1.0/products/getRelatedDLGList" 31 | ) 32 | 33 | // curl "https://my.vmware.com/channel/public/api/v1.0/products/getRelatedDLGList?category= &product=vmware_vsan&version=7_0&dlgType=PRODUCT_BINARY" |jq 34 | func (c *Client) GetDlgEditionsList(slug, majorVersion, dlgType string) (data []DlgEditionsLists, err error) { 35 | var category string 36 | category, err = c.GetCategory(slug) 37 | if err != nil {return} 38 | 39 | search_string := fmt.Sprintf("?category=%s&product=%s&version=%s&dlgType=%s", category, slug, majorVersion, dlgType) 40 | var res *http.Response 41 | res, err = c.HttpClient.Get(dlgListURL + search_string) 42 | if err != nil {return} 43 | defer res.Body.Close() 44 | 45 | err = c.validateResponseSlugCategoryVersion(slug, category, majorVersion, *res) 46 | if err != nil {return} 47 | 48 | var dlgEditions DlgEditions 49 | err = json.NewDecoder(res.Body).Decode(&dlgEditions) 50 | data = dlgEditions.DlgEditionsLists 51 | 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /sdk/login_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "testing" 11 | 12 | "github.com/orirawlings/persistent-cookiejar" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | var authenticatedClient *Client 17 | 18 | func ensureLogin(t *testing.T) (err error) { 19 | if authenticatedClient == nil { 20 | var jar *cookiejar.Jar 21 | // Persist cookies on file system to speed up testing 22 | jar, err = cookiejar.New(&cookiejar.Options{ 23 | Filename: filepath.Join(homeDir(), ".vmware.cookies"), 24 | PersistSessionCookies: true, 25 | }) 26 | if err != nil {return} 27 | user, pass := mustEnv(t, "VMWCC_USER"), mustEnv(t, "VMWCC_PASS") 28 | authenticatedClient, err = Login(user, pass, jar) 29 | if err == nil { 30 | err = jar.Save() 31 | } 32 | } 33 | return 34 | } 35 | 36 | // homeDir returns the OS-specific home path as specified in the environment. 37 | func homeDir() string { 38 | if runtime.GOOS == "windows" { 39 | return filepath.Join(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH")) 40 | } 41 | return os.Getenv("HOME") 42 | } 43 | 44 | func TestSuccessfulLogin(t *testing.T) { 45 | jar, _ := cookiejar.New(&cookiejar.Options{NoPersist: true}) 46 | user, pass := mustEnv(t, "VMWCC_USER"), mustEnv(t, "VMWCC_PASS") 47 | _, err = Login(user, pass, jar) 48 | assert.Nil(t, err) 49 | } 50 | 51 | func TestFailedLogin(t *testing.T) { 52 | jar, _ := cookiejar.New(&cookiejar.Options{NoPersist: true}) 53 | _, err = Login("user", "pass", jar) 54 | assert.ErrorIs(t, err, ErrorAuthenticationFailure) 55 | } 56 | 57 | func TestSuccessfulConnection(t *testing.T) { 58 | err = CheckConnectivity() 59 | if err != nil { 60 | t.Errorf("Expected error not to occur, got %q", err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vmware-customer-connect-sdk 2 | This SDK builds a layer of abstraction above customerconnect.vmware.com to hide the complexity for the client. This allows for downloads to be requested using the minimum of information. 3 | 4 | **WARNING:** This SDK is unofficial and experimental, with no guarantee of API stability. 5 | 6 | ## Overview 7 | 8 | This code is not meant to be compiled directly, but rather be consumed by another compiled program. 9 | 10 | ### Prerequisites 11 | 12 | See `go.mod` for details of the version of Golang used. 13 | 14 | Install modules with `go mod download` from the root of the repo. 15 | 16 | You must export environmental variables with your credentials to VMware Customer Connect. A generic account with no specific entitlement is required. Make sure that password are enclosed in single quotes to prevent issues with special charactors in passwords. 17 | 18 | ``` 19 | export VMWCC_USER='' 20 | export VMWCC_PASS='' 21 | ``` 22 | 23 | ## Testing 24 | 25 | Run test with `go test ./...`. 26 | 27 | ## Updating dependencies 28 | Run `GOPROXY=direct go get -u ./...` to pull in the latest dependencies. 29 | 30 | ## Contributing 31 | 32 | Please see our [Code of Conduct](CODE-OF-CONDUCT.md) and [Contributors guide](CONTRIBUTING.md). 33 | 34 | The vmware-customer-connect-sdk project team welcomes contributions from the community. Before you start working with vmware-customer-connect-sdk, please read and sign our Contributor License Agreement [CLA](https://cla.vmware.com/cla/1/preview). If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will prompt you to do so when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ]([https://cla.vmware.com/faq](https://cla.vmware.com/faq)). 35 | 36 | ## License 37 | Apache License 38 | 39 | 40 | Credit [@hoegaarden] (https://github.com/hoegaarden) for some of the original code. 41 | -------------------------------------------------------------------------------- /sdk/account.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | type AccountInfo struct { 14 | UserType string `json:"userType"` 15 | AccountList []AccntList `json:"accntList"` 16 | } 17 | 18 | type AccntList struct { 19 | EaNumber string `json:"eaNumber"` 20 | EaName string `json:"eaName"` 21 | IsDefault string `json:"isDefault"` 22 | } 23 | 24 | type CurrentUser struct { 25 | FirstName string `json:"firstname"` 26 | LastName string `json:"lastname"` 27 | } 28 | 29 | const ( 30 | accountInfoURL = baseURL + "/channel/api/v1.0/ems/accountinfo" 31 | currentUserURL = baseURL + "/vmwauth/loggedinuser" 32 | ) 33 | 34 | var ErrorNotAuthorized = errors.New("account: you are not authenticated") 35 | var ErrorNon200Response = errors.New("account: server did not respond with 200 ok") 36 | 37 | func (c *Client) AccountInfo() (data AccountInfo, err error) { 38 | payload := `{"rowLimit": 1000}` 39 | var res *http.Response 40 | res, err = c.HttpClient.Post(accountInfoURL, "application/json", strings.NewReader(payload)) 41 | if err != nil { 42 | return 43 | } 44 | defer res.Body.Close() 45 | 46 | if err = c.validateResponseGeneric(res.StatusCode); err != nil { 47 | return 48 | } 49 | 50 | err = json.NewDecoder(res.Body).Decode(&data) 51 | 52 | return 53 | } 54 | 55 | func (c *Client) CheckLoggedIn() (err error) { 56 | _, err = c.AccountInfo() 57 | return 58 | } 59 | 60 | func (c *Client) CurrentUser() (data CurrentUser, err error) { 61 | if err = c.CheckLoggedIn(); err != nil { 62 | return 63 | } 64 | 65 | var res *http.Response 66 | res, err = c.HttpClient.Get(currentUserURL) 67 | if err != nil { 68 | return 69 | } 70 | defer res.Body.Close() 71 | 72 | if err = c.validateResponseGeneric(res.StatusCode); err != nil { 73 | return 74 | } 75 | 76 | err = json.NewDecoder(res.Body).Decode(&data) 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /sdk/dlg_list_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGetDlgListDownloads(t *testing.T) { 13 | var dlgEditions []DlgEditionsLists 14 | dlgEditions, err = basicClient.GetDlgEditionsList("vmware_vsphere", "7_0", "PRODUCT_BINARY") 15 | assert.Nil(t, err) 16 | assert.Greater(t, len(dlgEditions), 1, "Expected response to contain more that one item") 17 | } 18 | 19 | func TestGetDlgListDrivers(t *testing.T) { 20 | var dlgEditions []DlgEditionsLists 21 | dlgEditions, err = basicClient.GetDlgEditionsList("vmware_vsphere", "7_0", "DRIVERS_TOOLS") 22 | assert.Nil(t, err) 23 | assert.Greater(t, len(dlgEditions), 1, "Expected response to contain more that one item") 24 | } 25 | 26 | func TestGetDlgListCustomISO(t *testing.T) { 27 | var dlgEditions []DlgEditionsLists 28 | dlgEditions, err = basicClient.GetDlgEditionsList("vmware_vsphere", "7_0", "CUSTOM_ISO") 29 | assert.Nil(t, err) 30 | assert.Greater(t, len(dlgEditions), 0, "Expected response to contain more that one item") 31 | } 32 | 33 | func TestGetDlgListAddons(t *testing.T) { 34 | var dlgEditions []DlgEditionsLists 35 | dlgEditions, err = basicClient.GetDlgEditionsList("vmware_vsphere", "7_0", "ADDONS") 36 | assert.Nil(t, err) 37 | assert.Greater(t, len(dlgEditions), 0, "Expected response to contain more that one item") 38 | } 39 | 40 | func TestGetDlgListInvalidSlug(t *testing.T) { 41 | var dlgEditions []DlgEditionsLists 42 | dlgEditions, err = basicClient.GetDlgEditionsList("mware_tools", "11_x", "PRODUCT_BINARY") 43 | assert.NotNil(t, err) 44 | assert.ErrorIs(t, err, ErrorInvalidSlug) 45 | assert.Empty(t, dlgEditions, "Expected response to be empty") 46 | } 47 | 48 | func TestGetDlgListInvalidVersion(t *testing.T) { 49 | var dlgEditions []DlgEditionsLists 50 | dlgEditions, err = basicClient.GetDlgEditionsList("vmware_tools", "99_x", "PRODUCT_BINARY") 51 | assert.NotNil(t, err) 52 | assert.ErrorIs(t, err, ErrorInvalidVersion) 53 | assert.Empty(t, dlgEditions, "Expected response to be empty") 54 | } 55 | -------------------------------------------------------------------------------- /sdk/dlg_header.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | ) 12 | 13 | type DlgHeader struct { 14 | Versions []Versions `json:"versions"` 15 | Product Product `json:"product"` 16 | Dlg Dlg `json:"dlg"` 17 | Resources []interface{} `json:"resources"` 18 | } 19 | type Versions struct { 20 | ID string `json:"id"` 21 | Name string `json:"name"` 22 | IsSelected bool `json:"isSelected"` 23 | } 24 | type Product struct { 25 | ID string `json:"id"` 26 | ReleasePackageID string `json:"releasePackageId"` 27 | Categorymap string `json:"categorymap"` 28 | Productmap string `json:"productmap"` 29 | Versionmap string `json:"versionmap"` 30 | Name string `json:"name"` 31 | Version string `json:"version"` 32 | } 33 | type Dlg struct { 34 | Name string `json:"name"` 35 | ReleaseDate string `json:"releaseDate"` 36 | Type string `json:"type"` 37 | Code string `json:"code"` 38 | Documentation string `json:"documentation"` 39 | InternalType string `json:"internalType"` 40 | IsFreeProduct bool `json:"isFreeProduct"` 41 | IsThirdParty bool `json:"isThirdParty"` 42 | IsMassMarket bool `json:"isMassMarket"` 43 | TagID int `json:"tagId"` 44 | Notes string `json:"notes"` 45 | Description string `json:"description"` 46 | CompatibleWith string `json:"compatibleWith"` 47 | } 48 | 49 | const ( 50 | dlgHeaderURL = baseURL + "/channel/public/api/v1.0/products/getDLGHeader" 51 | ) 52 | 53 | var ErrorDlgHeader = errors.New("dlgHeader: downloadGroup or productId invalid") 54 | 55 | // curl "https://my.vmware.com/channel/public/api/v1.0/products/getDLGHeader?downloadGroup=VMTOOLS1130&productId=1073" |jq 56 | func (c *Client) GetDlgHeader(downloadGroup, productId string) (data DlgHeader, err error) { 57 | search_string := fmt.Sprintf("?downloadGroup=%s&productId=%s", downloadGroup, productId) 58 | var res *http.Response 59 | res, err = c.HttpClient.Get(dlgHeaderURL + search_string) 60 | if err != nil {return} 61 | defer res.Body.Close() 62 | 63 | if res.StatusCode == 400 { 64 | err = ErrorDlgHeader 65 | return 66 | } 67 | 68 | err = json.NewDecoder(res.Body).Decode(&data) 69 | 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to vmware-customer-connect-sdk 2 | 3 | We welcome contributions from the community and first want to thank you for taking the time to contribute! 4 | 5 | Please familiarize yourself with the [Code of Conduct](https://github.com/vmware/.github/blob/main/CODE_OF_CONDUCT.md) before contributing. 6 | 7 | Before you start working with vmware-customer-connect-sdk, please read and sign our Contributor License Agreement [CLA](https://cla.vmware.com/cla/1/preview). If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will prompt you to do so when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ]([https://cla.vmware.com/faq](https://cla.vmware.com/faq)). 8 | 9 | ## Ways to contribute 10 | 11 | We welcome many different types of contributions and not all of them need a Pull request. Contributions may include: 12 | 13 | * New features and proposals 14 | * Documentation 15 | * Bug fixes 16 | * Issue Triage 17 | * Answering questions and giving feedback 18 | * Helping to onboard new contributors 19 | * Other related activities 20 | 21 | 22 | ## Contribution Flow 23 | 24 | This is a rough outline of what a contributor's workflow looks like: 25 | 26 | * Make a fork of the repository within your GitHub account 27 | * Create a topic branch in your fork from where you want to base your work 28 | * Make commits of logical units 29 | * Make sure your commit messages are with the proper format, quality and descriptiveness (see below) 30 | * Push your changes to the topic branch in your fork 31 | * Create a pull request containing that commit 32 | 33 | We follow the GitHub workflow and you can find more details on the [GitHub flow documentation](https://docs.github.com/en/get-started/quickstart/github-flow). 34 | 35 | 36 | ### Pull Request Checklist 37 | 38 | Before submitting your pull request, we advise you to use the following: 39 | 40 | 1. Check if your code changes will pass both code linting checks and unit tests. 41 | 2. Ensure your commit messages are descriptive. We follow the conventions on [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/). Be sure to include any related GitHub issue references in the commit message. See [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) for referencing issues and commits. 42 | 3. Check the commits and commits messages and ensure they are free from typos. 43 | 44 | ## Reporting Bugs and Creating Issues 45 | 46 | For specifics on what to include in your report, please follow the guidelines in the issue and pull request templates when available. 47 | 48 | ## Ask for Help 49 | 50 | For all issues please raise a Github issue. 51 | -------------------------------------------------------------------------------- /sdk/product.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "encoding/json" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | productsURL = baseURL + "/channel/public/api/v1.0/products/getProductsAtoZ?isPrivate=true" 14 | ) 15 | 16 | var ProductDetailMap map[string]ProductDetails 17 | 18 | type ProductDetails struct { 19 | Category string 20 | DisplayName string 21 | LatestMajorVersion string 22 | } 23 | type ProductResponse struct { 24 | ProductCategoryList []ProductCategoryList `json:"productCategoryList"` 25 | } 26 | type MajorProductEntities struct { 27 | Linkname string `json:"linkname"` 28 | OrderID int `json:"orderId"` 29 | Target string `json:"target"` 30 | } 31 | type MajorProducts struct { 32 | MajorProductEntities []MajorProductEntities `json:"actions"` 33 | Name string `json:"name"` 34 | } 35 | type ProductCategoryList struct { 36 | ID string `json:"id"` 37 | Name string `json:"name"` 38 | MajorProducts []MajorProducts `json:"productList"` 39 | } 40 | 41 | func (c *Client) GetProductsSlice() (data []MajorProducts, err error) { 42 | var res *http.Response 43 | res, err = c.HttpClient.Get(productsURL) 44 | if err != nil { 45 | return 46 | } 47 | defer res.Body.Close() 48 | 49 | var decodedProducts ProductResponse 50 | err = json.NewDecoder(res.Body).Decode(&decodedProducts) 51 | 52 | if err == nil { 53 | data = decodedProducts.ProductCategoryList[0].MajorProducts 54 | } 55 | 56 | return 57 | } 58 | 59 | // returned map is used to look up products by their slig 60 | func (c *Client) GetProductsMap() (productMap map[string]ProductDetails, err error) { 61 | productMap = make(map[string]ProductDetails) 62 | 63 | var products []MajorProducts 64 | products, err = c.GetProductsSlice() 65 | if err != nil { 66 | return 67 | } 68 | 69 | for _, product := range products { 70 | for _, subProduct := range product.MajorProductEntities { 71 | if !strings.Contains(subProduct.Target, "http") { 72 | splitTarget := strings.Split(subProduct.Target, "/") 73 | productDetails := ProductDetails{ 74 | Category: splitTarget[3], 75 | DisplayName: product.Name, 76 | LatestMajorVersion: strings.Split(splitTarget[5], "#")[0], 77 | } 78 | productMap[splitTarget[4]] = productDetails 79 | } 80 | } 81 | } 82 | return 83 | } 84 | 85 | func (c *Client) EnsureProductDetailMap() (err error) { 86 | if len(ProductDetailMap) < 1 { 87 | ProductDetailMap, err = c.GetProductsMap() 88 | } 89 | return 90 | } 91 | 92 | func (c *Client) GetCategory(slug string) (data string, err error) { 93 | c.EnsureProductDetailMap() 94 | if err != nil { 95 | return 96 | } 97 | 98 | if category, ok := ProductDetailMap[slug]; ok { 99 | data = category.Category 100 | } else { 101 | err = ErrorInvalidSlug 102 | } 103 | 104 | return 105 | } 106 | -------------------------------------------------------------------------------- /sdk/download.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "errors" 10 | "net/http" 11 | "strings" 12 | ) 13 | 14 | type DownloadPayload struct { 15 | Locale string `json:"locale"` // en_US 16 | DownloadGroup string `json:"downloadGroup"` // Versions versionMap[version].Code 17 | ProductId string `json:"productId"` // DlgList ProductID 18 | Md5checksum string `json:"md5checksum"` // dlgDetails Md5Checksum 19 | TagId int `json:"tagId"` // dlgHeader Dlg.TagID 20 | UUId string `json:"uUId"` // dlgDetails UUID 21 | DlgType string `json:"dlgType"` // dlgHeader Dlg.Type replace(/&/g, '&') 22 | ProductFamily string `json:"productFamily"` // dlgHeader Product.Name 23 | ReleaseDate string `json:"releaseDate"` // dlgDetails ReleaseDate 24 | DlgVersion string `json:"dlgVersion"` // dlgDetails Version 25 | IsBetaFlow bool `json:"isBetaFlow"` // false 26 | } 27 | 28 | type AuthorizedDownload struct { 29 | DownloadURL string `json:"downloadURL"` 30 | FileName string `json:"fileName"` 31 | } 32 | 33 | const ( 34 | downloadURL = baseURL + "/channel/api/v1.0/dlg/download" 35 | ) 36 | 37 | var ErrorInvalidDownloadPayload = errors.New("download: invalid download payload") 38 | 39 | func (c *Client) GenerateDownloadPayload(slug, subProduct, version, fileName, dlgType string, acceptEula bool) (data []DownloadPayload, err error) { 40 | if err = c.CheckLoggedIn(); err != nil { 41 | return 42 | } 43 | 44 | if err = c.EnsureProductDetailMap(); err != nil { 45 | return 46 | } 47 | 48 | if _, ok := ProductDetailMap[slug]; !ok { 49 | err = ErrorInvalidSlug 50 | return 51 | } 52 | 53 | var productID string 54 | var apiVersions APIVersions 55 | productID, apiVersions, err = c.GetDlgProduct(slug, subProduct, version, dlgType) 56 | if err != nil { 57 | return 58 | } 59 | 60 | var dlgHeader DlgHeader 61 | dlgHeader, err = c.GetDlgHeader(apiVersions.Code, productID) 62 | if err != nil { 63 | return 64 | } 65 | if dlgHeader.Dlg.Type == "OEM Addons" { 66 | dlgHeader.Dlg.Type = "Drivers & Tools" 67 | } else { 68 | dlgHeader.Dlg.Type = strings.Replace(dlgHeader.Dlg.Type, "amp;", "",1) 69 | } 70 | 71 | var downloadDetails FoundDownload 72 | downloadDetails, err = c.FindDlgDetails(apiVersions.Code, productID, fileName) 73 | if err != nil { 74 | return 75 | } 76 | 77 | if !downloadDetails.EligibleToDownload { 78 | err = ErrorNotEntitled 79 | return 80 | } 81 | 82 | if !downloadDetails.EulaAccepted { 83 | if !acceptEula { 84 | err = ErrorEulaUnaccepted 85 | return 86 | } else { 87 | err = c.AcceptEula(apiVersions.Code, productID) 88 | if err != nil { 89 | return 90 | } 91 | } 92 | } 93 | 94 | for _, downloadFile := range downloadDetails.DownloadDetails { 95 | downloadPayload := DownloadPayload{ 96 | Locale: "en_US", 97 | DownloadGroup: apiVersions.Code, 98 | ProductId: productID, 99 | Md5checksum: downloadFile.Md5Checksum, 100 | TagId: dlgHeader.Dlg.TagID, 101 | UUId: downloadFile.UUID, 102 | DlgType: dlgHeader.Dlg.Type, 103 | ProductFamily: dlgHeader.Product.Name, 104 | ReleaseDate: downloadFile.ReleaseDate, 105 | DlgVersion: downloadFile.Version, 106 | IsBetaFlow: false, 107 | } 108 | 109 | data = append(data, downloadPayload) 110 | 111 | } 112 | 113 | return 114 | } 115 | 116 | func (c *Client) FetchDownloadLink(downloadPayload DownloadPayload) (data AuthorizedDownload, err error) { 117 | if err = c.CheckLoggedIn(); err != nil { 118 | return 119 | } 120 | 121 | postJson, _ := json.Marshal(downloadPayload) 122 | payload := bytes.NewBuffer(postJson) 123 | 124 | var req *http.Request 125 | req, err = http.NewRequest("POST", downloadURL, payload) 126 | if err != nil { 127 | return 128 | } 129 | req.Header.Add("Content-Type", "application/json") 130 | req.Header.Add("X-XSRF-TOKEN", c.XsrfToken) 131 | var res *http.Response 132 | res, err = c.HttpClient.Do(req) 133 | if err != nil { 134 | return 135 | } 136 | defer res.Body.Close() 137 | 138 | if res.StatusCode == 200 { 139 | err = json.NewDecoder(res.Body).Decode(&data) 140 | } else if res.StatusCode == 400 { 141 | err = ErrorInvalidDownloadPayload 142 | } else { 143 | err = ErrorNon200Response 144 | } 145 | 146 | return 147 | } 148 | -------------------------------------------------------------------------------- /sdk/versions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "errors" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | type APIVersions struct { 13 | Code string 14 | MajorVersion string 15 | MinorVersion string 16 | } 17 | 18 | var ErrorNoMatchingVersions = errors.New("versions: invalid glob. no versions found") 19 | var ErrorNoVersinGlob = errors.New("versions: invalid glob. glob my be provided") 20 | var ErrorMultipleVersionGlob = errors.New("versions: invalid glob. a single version glob must be used") 21 | 22 | func (c *Client) GetVersionMap(slug, subProductName, dlgType string) (data map[string]APIVersions, err error) { 23 | data = make(map[string]APIVersions) 24 | 25 | var subProductDetails SubProductDetails 26 | subProductDetails, err = c.GetSubProduct(slug, subProductName, dlgType) 27 | if err != nil { 28 | return 29 | } 30 | 31 | // Loop through each major version and collect all versions 32 | for majorVersion, dlgList := range subProductDetails.DlgListByVersion { 33 | var dlgHeader DlgHeader 34 | dlgHeader, err = c.GetDlgHeader(dlgList.Code, dlgList.ProductID) 35 | if err != nil { 36 | return 37 | } 38 | 39 | for _, version := range dlgHeader.Versions { 40 | if ( subProductName != "nsx" && subProductName != "nsx_le" && subProductName != "nsx-t" && subProductName != "nsx-t_le" ) || 41 | (subProductName == "nsx_le" && strings.HasSuffix(version.ID, "-LE")) || 42 | (subProductName == "nsx" && !strings.HasSuffix(version.ID, "-LE")) || 43 | (subProductName == "nsx-t_le" && strings.HasSuffix(version.ID, "-LE")) || 44 | (subProductName == "nsx-t" && !strings.HasSuffix(version.ID, "-LE")) { 45 | data[version.Name] = APIVersions{ 46 | Code: version.ID, 47 | MajorVersion: majorVersion, 48 | } 49 | } 50 | } 51 | } 52 | 53 | return 54 | } 55 | 56 | func (c *Client) FindVersion(slug, subProduct, version, dlgType string) (data APIVersions, err error) { 57 | var versionMap map[string]APIVersions 58 | versionMap, err = c.GetVersionMap(slug, subProduct, dlgType) 59 | if err != nil { 60 | return 61 | } 62 | 63 | var searchVersion string 64 | if strings.Contains(version, "*") { 65 | searchVersion, err = c.FindVersionFromGlob(version, versionMap) 66 | if err != nil { 67 | return 68 | } 69 | } else { 70 | searchVersion = version 71 | } 72 | 73 | if _, ok := versionMap[searchVersion]; !ok { 74 | err = ErrorInvalidVersion 75 | return 76 | } 77 | 78 | data = versionMap[searchVersion] 79 | data.MinorVersion = searchVersion 80 | return 81 | } 82 | 83 | func (c *Client) FindVersionFromGlob(versionGlob string, versionMap map[string]APIVersions) (version string, err error) { 84 | // Ensure only one glob is defined 85 | globCount := strings.Count(versionGlob, "*") 86 | if globCount == 0 { 87 | err = ErrorNoVersinGlob 88 | return 89 | } else if globCount > 1 { 90 | err = ErrorMultipleVersionGlob 91 | return 92 | } 93 | 94 | // Extract prefix by removing * 95 | versionPrefix := strings.Split(versionGlob, "*")[0] 96 | 97 | sortedKeys := sortVersionMapKeys(versionMap) 98 | 99 | // Check if only * is provided as strings. Split returns empty if separator is found. 100 | if versionPrefix == "" { 101 | // return the first entry, which is the highest number. 102 | version = sortedKeys[0] 103 | return 104 | } else { 105 | // return the first entry matching the prefix 106 | for _, key := range sortedKeys { 107 | if strings.HasPrefix(key, versionPrefix) { 108 | version = key 109 | return 110 | } 111 | } 112 | } 113 | 114 | err = ErrorNoMatchingVersions 115 | return 116 | } 117 | 118 | func (c *Client) GetVersionSlice(slug, subProductName, dlgType string) (data []string, err error) { 119 | var versionMap map[string]APIVersions 120 | versionMap, err = c.GetVersionMap(slug, subProductName, dlgType) 121 | if err != nil { 122 | return 123 | } 124 | 125 | data = sortVersionMapKeys(versionMap) 126 | 127 | return 128 | } 129 | 130 | func sortVersionMapKeys(versionMap map[string]APIVersions) (keys []string) { 131 | // Extract all keys which are the version strings and reverse sort them 132 | // This means the versions will go from high to low 133 | keys = make([]string, len(versionMap)) 134 | i := 0 135 | for key := range versionMap { 136 | keys[i] = key 137 | i++ 138 | } 139 | sort.Sort(sort.Reverse(sort.StringSlice(keys))) 140 | return 141 | } 142 | -------------------------------------------------------------------------------- /sdk/versions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGetVersionSuccess(t *testing.T) { 13 | var versions map[string]APIVersions 14 | versions, err = basicClient.GetVersionMap("vmware_horizon_clients", "cart+win", "PRODUCT_BINARY") 15 | assert.Nil(t, err) 16 | assert.Greater(t, len(versions), 1, "Expected response to contain at least 1 item") 17 | assert.Contains(t, versions, "2106") 18 | assert.Contains(t, versions, "2006") 19 | } 20 | 21 | func TestGetVersionSuccessHorizon(t *testing.T) { 22 | var versions map[string]APIVersions 23 | versions, err = basicClient.GetVersionMap("vmware_horizon", "dem+standard", "PRODUCT_BINARY") 24 | assert.Nil(t, err) 25 | assert.Greater(t, len(versions), 1, "Expected response to contain at least 1 item") 26 | assert.Contains(t, versions, "2106") 27 | assert.Contains(t, versions, "2006") 28 | } 29 | 30 | func TestGetVersionSuccessNsxLe(t *testing.T) { 31 | var versions map[string]APIVersions 32 | versions, err = basicClient.GetVersionMap("vmware_nsx", "nsx_le", "PRODUCT_BINARY") 33 | assert.Nil(t, err) 34 | assert.Greater(t, len(versions), 1, "Expected response to contain at least 1 item") 35 | assert.Contains(t, versions, "4.0.1.1 LE") 36 | assert.NotContains(t, versions, "4.0.1.1") 37 | } 38 | 39 | func TestGetVersionSuccessNsx(t *testing.T) { 40 | var versions map[string]APIVersions 41 | versions, err = basicClient.GetVersionMap("vmware_nsx", "nsx", "PRODUCT_BINARY") 42 | assert.Nil(t, err) 43 | assert.Greater(t, len(versions), 1, "Expected response to contain at least 1 item") 44 | assert.Contains(t, versions, "4.0.1.1") 45 | assert.NotContains(t, versions, "4.0.1.1 LE") 46 | } 47 | 48 | func TestGetVersionMapInvalidSubProduct(t *testing.T) { 49 | var versions map[string]APIVersions 50 | versions, err = basicClient.GetVersionMap("vmware_tools", "dummy", "PRODUCT_BINARY") 51 | assert.ErrorIs(t, err, ErrorInvalidSubProduct) 52 | assert.Empty(t, versions, "Expected response to be empty") 53 | } 54 | 55 | func TestGetVersionInvalidSlug(t *testing.T) { 56 | var versions map[string]APIVersions 57 | versions, err = basicClient.GetVersionMap("mware_tools", "vmtools", "PRODUCT_BINARY") 58 | assert.ErrorIs(t, err, ErrorInvalidSlug) 59 | assert.Empty(t, versions, "Expected response to be empty") 60 | } 61 | 62 | func TestFindVersion(t *testing.T) { 63 | var foundVersion APIVersions 64 | foundVersion, err = basicClient.FindVersion("vmware_tools", "vmtools", "11.1.1", "PRODUCT_BINARY") 65 | assert.Nil(t, err) 66 | assert.NotEmpty(t, foundVersion.Code, "Expected response not to be empty") 67 | assert.Equal(t, foundVersion.MinorVersion, "11.1.1") 68 | } 69 | 70 | func TestFindVersionInvalidSlug(t *testing.T) { 71 | var foundVersion APIVersions 72 | foundVersion, err = basicClient.FindVersion("mware_tools", "vmtools", "11.1.1", "PRODUCT_BINARY") 73 | assert.ErrorIs(t, err, ErrorInvalidSlug) 74 | assert.Empty(t, foundVersion.Code, "Expected response to be empty") 75 | } 76 | 77 | func TestFindVersionInvalidVersion(t *testing.T) { 78 | var foundVersion APIVersions 79 | foundVersion, err = basicClient.FindVersion("vmware_tools", "vmtools", "666", "PRODUCT_BINARY") 80 | assert.ErrorIs(t, err, ErrorInvalidVersion) 81 | assert.Empty(t, foundVersion.Code, "Expected response to be empty") 82 | } 83 | 84 | func TestFindVersionInvalidSubProduct(t *testing.T) { 85 | var foundVersion APIVersions 86 | foundVersion, err = basicClient.FindVersion("vmware_tools", "tools", "11.1.1", "PRODUCT_BINARY") 87 | assert.ErrorIs(t, err, ErrorInvalidSubProduct) 88 | assert.Empty(t, foundVersion.Code, "Expected response to be empty") 89 | 90 | } 91 | 92 | func TestFindVersionMinorGlob(t *testing.T) { 93 | var foundVersion APIVersions 94 | foundVersion, err = basicClient.FindVersion("vmware_tools", "vmtools", "10.2.*", "PRODUCT_BINARY") 95 | assert.Nil(t, err) 96 | assert.Equal(t, foundVersion.Code, "VMTOOLS1021") 97 | assert.Contains(t, foundVersion.MinorVersion, "10.2") 98 | } 99 | 100 | func TestFindVersionOnlyGlob(t *testing.T) { 101 | var foundVersion APIVersions 102 | foundVersion, err = basicClient.FindVersion("vmware_tools", "vmtools", "*", "PRODUCT_BINARY") 103 | assert.Nil(t, err) 104 | assert.NotEmpty(t, foundVersion.Code) 105 | assert.Contains(t, foundVersion.MinorVersion, ".") 106 | } 107 | 108 | func TestGetVersionArraySuccess(t *testing.T) { 109 | var versions []string 110 | versions, err = basicClient.GetVersionSlice("vmware_tools", "vmtools", "PRODUCT_BINARY") 111 | assert.Nil(t, err) 112 | assert.Greater(t, len(versions), 10, "Expected response to contain at least 10 items") 113 | } 114 | -------------------------------------------------------------------------------- /sdk/dlg_details.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "path/filepath" 12 | ) 13 | 14 | type DlgDetails struct { 15 | DownloadDetails []DownloadDetails `json:"downloadFiles"` 16 | EligibilityResponse EligibilityResponse `json:"eligibilityResponse"` 17 | EulaResponse EulaResponse `json:"eulaResponse"` 18 | } 19 | 20 | type DownloadDetails struct { 21 | FileName string `json:"fileName"` 22 | Sha1Checksum string `json:"sha1checksum"` 23 | Sha256Checksum string `json:"sha256checksum"` 24 | Md5Checksum string `json:"md5checksum"` 25 | Build string `json:"build"` 26 | ReleaseDate string `json:"releaseDate"` 27 | FileType string `json:"fileType"` 28 | Description string `json:"description"` 29 | FileSize string `json:"fileSize"` 30 | Title string `json:"title"` 31 | Version string `json:"version"` 32 | Status string `json:"status"` 33 | UUID string `json:"uuid"` 34 | Header bool `json:"header"` 35 | DisplayOrder int `json:"displayOrder"` 36 | Relink bool `json:"relink"` 37 | Rsync bool `json:"rsync"` 38 | } 39 | 40 | type EligibilityResponse struct { 41 | EligibleToDownload bool `json:"eligibleToDownload"` 42 | } 43 | type EulaResponse struct { 44 | EulaAccepted bool `json:"eulaAccepted"` 45 | EulaURL string `json:"eulaURL"` 46 | } 47 | 48 | type FoundDownload struct { 49 | DownloadDetails []DownloadDetails 50 | EulaAccepted bool 51 | EligibleToDownload bool 52 | } 53 | 54 | const ( 55 | dlgDetailsURLAuthenticated = baseURL + "/channel/api/v1.0/dlg/details" 56 | dlgDetailsURLPublic = baseURL + "/channel/public/api/v1.0/dlg/details" 57 | ) 58 | 59 | var ( 60 | ErrorDlgDetailsInputs = errors.New("dlgDetails: downloadGroup or productId invalid") 61 | ErrorNoMatchingFiles = errors.New("dlgDetails: no files match provided glob") 62 | ErrorMultipleMatchingFiles = errors.New("dlgDetails: more than 1 file matches glob") 63 | ErrorEulaUnaccepted = errors.New("dlgDetails: EULA needs to be accepted for this version") 64 | ErrorNotEntitled = errors.New("dlgDetails: user is not entitled to download this file") 65 | ) 66 | 67 | // curl "https://my.vmware.com/channel/public/api/v1.0/dlg/details?downloadGroup=VMTOOLS1130&productId=1073" |jq 68 | func (c *Client) GetDlgDetails(downloadGroup, productId string) (data DlgDetails, err error) { 69 | err = c.CheckLoggedIn() 70 | // Use public URL when user is not logged in 71 | // This will not return entitlement or EULA sections 72 | var dlgDetailsURL string 73 | if err != nil { 74 | dlgDetailsURL = dlgDetailsURLPublic 75 | } else { 76 | dlgDetailsURL = dlgDetailsURLAuthenticated 77 | } 78 | 79 | search_string := fmt.Sprintf("?downloadGroup=%s&productId=%s", downloadGroup, productId) 80 | var res *http.Response 81 | res, err = c.HttpClient.Get(dlgDetailsURL + search_string) 82 | if err != nil { 83 | return 84 | } 85 | defer res.Body.Close() 86 | 87 | if res.StatusCode == 400 { 88 | err = ErrorDlgDetailsInputs 89 | return 90 | } else if res.StatusCode == 401 { 91 | err = ErrorNotAuthenticated 92 | return 93 | } 94 | 95 | err = json.NewDecoder(res.Body).Decode(&data) 96 | 97 | return 98 | } 99 | 100 | func (c *Client) FindDlgDetails(downloadGroup, productId, fileName string) (data FoundDownload, err error) { 101 | if err = c.CheckLoggedIn(); err != nil { 102 | return 103 | } 104 | 105 | var dlgDetails DlgDetails 106 | dlgDetails, err = c.GetDlgDetails(downloadGroup, productId) 107 | if err != nil { 108 | return 109 | } 110 | 111 | data = FoundDownload{ 112 | EulaAccepted: dlgDetails.EulaResponse.EulaAccepted, 113 | EligibleToDownload: dlgDetails.EligibilityResponse.EligibleToDownload, 114 | } 115 | 116 | // Search for file which matches the pattern. If glob is used multiple will return. 117 | for _, download := range dlgDetails.DownloadDetails { 118 | filename := download.FileName 119 | if match, _ := filepath.Match(fileName, filename); match { 120 | 121 | data.DownloadDetails = append(data.DownloadDetails, download) 122 | } 123 | } 124 | 125 | if len(data.DownloadDetails) == 0 { 126 | err = ErrorNoMatchingFiles 127 | } 128 | return 129 | } 130 | 131 | func (c *Client) GetFileArray(slug, subProduct, version, dlgType string) (data []string, err error) { 132 | var productID string 133 | var apiVersions APIVersions 134 | productID, apiVersions, err = c.GetDlgProduct(slug, subProduct, version, dlgType) 135 | if err != nil { 136 | return 137 | } 138 | 139 | var dlgDetails DlgDetails 140 | dlgDetails, err = c.GetDlgDetails(apiVersions.Code, productID) 141 | if err != nil { 142 | return 143 | } 144 | 145 | for _, download := range dlgDetails.DownloadDetails { 146 | if download.FileName != "" { 147 | data = append(data, download.FileName) 148 | } 149 | } 150 | 151 | return 152 | } 153 | 154 | func (c *Client) GetDlgProduct(slug, subProduct, version, dlgType string) (productID string, apiVersions APIVersions, err error) { 155 | // Find the API version details 156 | apiVersions, err = c.FindVersion(slug, subProduct, version, dlgType) 157 | if err != nil { 158 | return 159 | } 160 | 161 | var subProductDetails DlgList 162 | subProductDetails, err = c.GetSubProductDetails(slug, subProduct, apiVersions.MajorVersion, dlgType) 163 | if err != nil { 164 | return 165 | } 166 | 167 | productID = subProductDetails.ProductID 168 | 169 | return 170 | } 171 | -------------------------------------------------------------------------------- /sdk/dlg_details_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestGetDetailsSuccess(t *testing.T) { 14 | var dlgDetails DlgDetails 15 | dlgDetails, err = basicClient.GetDlgDetails("VMTOOLS1130", "1073") 16 | assert.Nil(t, err) 17 | assert.NotEmpty(t, dlgDetails.DownloadDetails, "Expected response to no be empty") 18 | } 19 | 20 | func TestGetDetailsInvalidProductId(t *testing.T) { 21 | var dlgDetails DlgDetails 22 | dlgDetails, err = basicClient.GetDlgDetails("VMTOOLS1130", "6666666") 23 | assert.NotNil(t, err) 24 | assert.ErrorIs(t, err, ErrorDlgDetailsInputs) 25 | assert.Empty(t, dlgDetails, "Expected response to be empty") 26 | } 27 | 28 | func TestGetDetailsInvalidDownloadGroup(t *testing.T) { 29 | var dlgDetails DlgDetails 30 | dlgDetails, err = basicClient.GetDlgDetails("VMTOOLS666", "1073") 31 | assert.NotNil(t, err) 32 | assert.ErrorIs(t, err, ErrorDlgDetailsInputs) 33 | assert.Empty(t, dlgDetails, "Expected response to be empty") 34 | } 35 | 36 | func TestFindDlgDetailsSuccess(t *testing.T) { 37 | err = ensureLogin(t) 38 | require.Nil(t, err) 39 | 40 | var downloadDetails FoundDownload 41 | downloadDetails, err = authenticatedClient.FindDlgDetails("VMTOOLS1130", "1073", "VMware-Tools-darwin-*.tar.gz") 42 | assert.Nil(t, err) 43 | require.NotEmpty(t, downloadDetails.DownloadDetails) 44 | assert.NotEmpty(t, downloadDetails.DownloadDetails[0].FileName, "Expected response to not be empty") 45 | } 46 | 47 | func TestFindDlgDetailsGlobMultipleResults(t *testing.T) { 48 | err = ensureLogin(t) 49 | require.Nil(t, err) 50 | 51 | var downloadDetails FoundDownload 52 | downloadDetails, err = authenticatedClient.FindDlgDetails("VMTOOLS1130", "1073", "*") 53 | assert.Nil(t, err) 54 | assert.Greater(t, len(downloadDetails.DownloadDetails), 1, "Expected response to be empty") 55 | } 56 | 57 | func TestFindDlgDetailsMultipleGlob(t *testing.T) { 58 | err = ensureLogin(t) 59 | require.Nil(t, err) 60 | 61 | var downloadDetails FoundDownload 62 | downloadDetails, err = authenticatedClient.FindDlgDetails("VMTOOLS1130", "1073", "VMware-Tools-*-core-offline-depot-ESXi-all-*.zip") 63 | assert.Nil(t, err) 64 | require.NotEmpty(t, downloadDetails.DownloadDetails) 65 | assert.NotEmpty(t, downloadDetails.DownloadDetails[0].FileName, "Expected response to not be empty") 66 | } 67 | 68 | func TestFindDlgDetailsNoGlob(t *testing.T) { 69 | err = ensureLogin(t) 70 | require.Nil(t, err) 71 | 72 | var downloadDetails FoundDownload 73 | downloadDetails, err = authenticatedClient.FindDlgDetails("VMTOOLS1130", "1073", "VMware-Tools-11.3.0-core-offline-depot-ESXi-all-18090558.zip") 74 | assert.Nil(t, err) 75 | require.NotEmpty(t, downloadDetails.DownloadDetails) 76 | assert.NotEmpty(t, downloadDetails.DownloadDetails[0].FileName, "Expected response to not be empty") 77 | } 78 | 79 | func TestFindDlgDetailsNoMatch(t *testing.T) { 80 | err = ensureLogin(t) 81 | require.Nil(t, err) 82 | 83 | var downloadDetails FoundDownload 84 | downloadDetails, err = authenticatedClient.FindDlgDetails("VMTOOLS1130", "1073", "invalid*glob") 85 | assert.NotNil(t, err) 86 | assert.ErrorIs(t, err, ErrorNoMatchingFiles) 87 | assert.Empty(t, downloadDetails.DownloadDetails, "Expected response to be empty") 88 | } 89 | 90 | func TestGetFileArray(t *testing.T) { 91 | err = ensureLogin(t) 92 | require.Nil(t, err) 93 | 94 | var fileArray []string 95 | fileArray, err = authenticatedClient.GetFileArray("vmware_horizon", "dem+standard", "2106", "PRODUCT_BINARY") 96 | assert.Nil(t, err) 97 | assert.NotEmpty(t, fileArray, "Expected response to no be empty") 98 | } 99 | 100 | func TestGetGetDlgProduct(t *testing.T) { 101 | var productID string 102 | var apiVersions APIVersions 103 | productID, apiVersions, err = basicClient.GetDlgProduct("vmware_tools", "vmtools", "11.1.1", "PRODUCT_BINARY") 104 | assert.Nil(t, err) 105 | assert.NotEmpty(t, apiVersions.Code, "Expected response to no be empty") 106 | assert.NotEmpty(t, productID, "Expected response to no be empty") 107 | } 108 | 109 | func TestGetGetDlgProductNsx(t *testing.T) { 110 | var productID string 111 | var apiVersions APIVersions 112 | productID, apiVersions, err = basicClient.GetDlgProduct("vmware_nsx", "nsx", "4.0*", "PRODUCT_BINARY") 113 | assert.Nil(t, err) 114 | assert.NotContains(t, apiVersions.Code, "-LE") 115 | assert.NotEmpty(t, productID, "Expected response to no be empty") 116 | } 117 | 118 | func TestGetGetDlgProductNsxLe(t *testing.T) { 119 | var productID string 120 | var apiVersions APIVersions 121 | productID, apiVersions, err = basicClient.GetDlgProduct("vmware_nsx", "nsx_le", "4.0.1.1 LE", "PRODUCT_BINARY") 122 | assert.Nil(t, err) 123 | assert.Contains(t, apiVersions.Code, "-LE") 124 | assert.NotEmpty(t, productID, "Expected response to no be empty") 125 | } 126 | 127 | func TestGetGetDlgProductNsxT(t *testing.T) { 128 | var productID string 129 | var apiVersions APIVersions 130 | productID, apiVersions, err = basicClient.GetDlgProduct("vmware_nsx_t_data_center", "nsx-t", "3.2*", "PRODUCT_BINARY") 131 | assert.Nil(t, err) 132 | assert.NotContains(t, apiVersions.Code, "-LE") 133 | assert.NotEmpty(t, productID, "Expected response to no be empty") 134 | } 135 | 136 | func TestGetGetDlgProductNsxTLe(t *testing.T) { 137 | var productID string 138 | var apiVersions APIVersions 139 | productID, apiVersions, err = basicClient.GetDlgProduct("vmware_nsx_t_data_center", "nsx-t_le", "3.2.1.2 LE", "PRODUCT_BINARY") 140 | assert.Nil(t, err) 141 | assert.Contains(t, apiVersions.Code, "-LE") 142 | assert.NotEmpty(t, productID, "Expected response to no be empty") 143 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in vmware-customer-connect-sdk project and our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at oss-coc@vmware.com. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /sdk/login.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "net/url" 13 | "strings" 14 | 15 | "github.com/andybalholm/cascadia" 16 | "github.com/orirawlings/persistent-cookiejar" 17 | "golang.org/x/net/html" 18 | ) 19 | 20 | type Client struct { 21 | HttpClient *http.Client 22 | XsrfToken string 23 | } 24 | 25 | type TokenValidation struct { 26 | IsAccessTokenValid string `json:"isAccessTokenValid"` 27 | IsRefreshTokenPresent string `json:"isRefreshTokenPresent"` 28 | Cn string `json:"cn"` 29 | Email string `json:"email"` 30 | Firstname string `json:"firstname"` 31 | Lastname string `json:"lastname"` 32 | UserType string `json:"userType"` 33 | } 34 | 35 | const ( 36 | baseURL = "https://customerconnect.vmware.com" 37 | initURL = baseURL + "/web/vmware/login" 38 | ssoURL = baseURL + "/vmwauth/saml/SSO" 39 | authURL = "https://auth.vmware.com/oam/server/auth_cred_submit?Auth-AppID=WMVMWR" 40 | ) 41 | 42 | const samlInputQuery = `input[name="SAMLResponse"]` 43 | 44 | var samlInputSelector = cascadia.MustCompile(samlInputQuery) 45 | 46 | var ErrorNotAuthenticated = errors.New("generic: returned http 401 not authenticated") 47 | var ErrorAuthenticationFailure = errors.New("login: authentication failure") 48 | var ErrorXsrfFailure = errors.New("login: server did not return XSRF token") 49 | var ErrorConnectionFailure = errors.New("login: server did not return 200 ok") 50 | 51 | func Login(username, password string, jar *cookiejar.Jar) (client *Client, err error) { 52 | err = CheckConnectivity() 53 | if err != nil { 54 | return 55 | } 56 | 57 | httpClient := &http.Client{Jar: jar} 58 | 59 | // When cookies are passed in and check to see can make calls 60 | // Otherwise perform a login 61 | _, errXsrf := setXsrfToken(httpClient) 62 | loginNeeded := false 63 | if len(jar.AllCookies()) > 0 && errXsrf == nil { 64 | 65 | payload := `{"rowLimit": 10}` 66 | var res *http.Response 67 | res, err = httpClient.Post(accountInfoURL, "application/json", strings.NewReader(payload)) 68 | if err != nil { 69 | return 70 | } 71 | defer res.Body.Close() 72 | 73 | if res.StatusCode == 401 || res.StatusCode == 500 { 74 | loginNeeded = true 75 | } 76 | } else { 77 | loginNeeded = true 78 | } 79 | 80 | if loginNeeded { 81 | jar.RemoveAll() 82 | err = performLogin(httpClient, username, password, jar) 83 | if err != nil { 84 | return 85 | } 86 | } 87 | 88 | var xsrfToken string 89 | if xsrfToken, err = setXsrfToken(httpClient); err != nil { 90 | return 91 | } 92 | 93 | client = &Client{ 94 | HttpClient: httpClient, 95 | XsrfToken: xsrfToken, 96 | } 97 | 98 | return 99 | } 100 | 101 | // Extract xsrf token value to be used when getting download link 102 | func setXsrfToken(client *http.Client) (xsrfToken string, err error) { 103 | u, _ := url.Parse("https://customerconnect.vmware.com") 104 | cookies := client.Jar.Cookies(u) 105 | for _, cookie := range cookies { 106 | if cookie.Name == "XSRF-TOKEN" { 107 | xsrfToken = cookie.Value 108 | } 109 | } 110 | if xsrfToken == "" { 111 | err = ErrorXsrfFailure 112 | return 113 | } 114 | return 115 | } 116 | 117 | func performLogin(httpClient *http.Client, username, password string, jar *cookiejar.Jar) (err error) { 118 | 119 | var buf bytes.Buffer 120 | 121 | // Attempting login 5 times, to allow for empty response issue. 122 | for i := 1; i < 5; i++ { 123 | // Initialize cookies 124 | var initRes *http.Response 125 | initRes, err = httpClient.Get(initURL) 126 | if err != nil { 127 | break 128 | } 129 | initRes.Body.Close() 130 | 131 | // Error if connaction cannot be made to login endpoint 132 | if initRes.StatusCode != 200 { 133 | err = ErrorConnectionFailure 134 | break 135 | } 136 | 137 | // Post credentials to get SAML token back 138 | var authResp *http.Response 139 | authResp, err = httpClient.PostForm(authURL, url.Values{ 140 | "username": {username}, 141 | "password": {password}, 142 | }) 143 | if err != nil { 144 | break 145 | } 146 | defer authResp.Body.Close() 147 | 148 | tee := io.TeeReader(authResp.Body, &buf) 149 | 150 | var authBodyBytes []byte 151 | authBodyBytes, err = io.ReadAll(tee) 152 | str_body := string(authBodyBytes) 153 | 154 | if str_body == "" { 155 | jar.RemoveAll() 156 | continue 157 | } 158 | 159 | // Return auth failure if reposonse is redirect to the login page 160 | if authResp.Request.URL.Path == "/login" { 161 | err = ErrorAuthenticationFailure 162 | } 163 | break 164 | } 165 | 166 | if err != nil { 167 | return 168 | } 169 | 170 | samlToken, err := getSAMLToken(&buf) 171 | if err != nil { 172 | return 173 | } 174 | 175 | // Post SAML token to generate final session cookies 176 | ssoRes, err := httpClient.PostForm(ssoURL, url.Values{ 177 | "SAMLResponse": {samlToken}, 178 | }) 179 | if err != nil { 180 | return 181 | } 182 | defer ssoRes.Body.Close() 183 | 184 | return 185 | } 186 | 187 | // Extract SAML token from HTML body 188 | func getSAMLToken(body io.Reader) (string, error) { 189 | doc, err := html.Parse(body) 190 | if err != nil { 191 | return "", err 192 | } 193 | 194 | node := samlInputSelector.MatchFirst(doc) 195 | if node == nil { 196 | return "", fmt.Errorf("could not find node that matches %#v", samlInputQuery) 197 | } 198 | 199 | for _, attr := range node.Attr { 200 | if attr.Key == "value" { 201 | return attr.Val, nil 202 | } 203 | } 204 | 205 | return "", fmt.Errorf("could not find the node's value attribute") 206 | } 207 | 208 | func CheckConnectivity() (err error) { 209 | httpClient := &http.Client{} 210 | var res *http.Response 211 | res, err = httpClient.Get(ssoURL) 212 | 213 | if res.StatusCode != 200 { 214 | err = ErrorConnectionFailure 215 | } 216 | 217 | return 218 | } 219 | -------------------------------------------------------------------------------- /sdk/subproduct_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "regexp" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestGetSubProductsSlice(t *testing.T) { 14 | var subProducts []SubProductDetails 15 | subProducts, err = basicClient.GetSubProductsSlice("vmware_horizon", "PRODUCT_BINARY", "") 16 | assert.Nil(t, err) 17 | assert.GreaterOrEqual(t, len(subProducts), 20, "Expected response to contain at least 20 items") 18 | } 19 | 20 | func TestGetSubProductsSliceDrivers(t *testing.T) { 21 | var subProducts []SubProductDetails 22 | subProducts, err = basicClient.GetSubProductsSlice("vmware_vsphere", "DRIVERS_TOOLS", "") 23 | assert.Nil(t, err) 24 | assert.GreaterOrEqual(t, len(subProducts), 400, "Expected response to contain at least 200 items") 25 | 26 | // Ensure less results are returned after specifying majpor version 27 | subProducts, err = basicClient.GetSubProductsSlice("vmware_vsphere", "DRIVERS_TOOLS", "8_0") 28 | assert.Nil(t, err) 29 | assert.GreaterOrEqual(t, len(subProducts), 100, "Expected response to contain at least 100 items") 30 | assert.LessOrEqual(t, len(subProducts), 400, "Expected response to contain less than 400 items") 31 | } 32 | 33 | func TestGetSubProductsSliceInvalidSlug(t *testing.T) { 34 | var subProducts []SubProductDetails 35 | subProducts, err = basicClient.GetSubProductsSlice("vsphere", "PRODUCT_BINARY", "") 36 | assert.ErrorIs(t, err, ErrorInvalidSlug) 37 | assert.Empty(t, subProducts, "Expected response to be empty") 38 | } 39 | 40 | func TestGetSubProductNsxLE(t *testing.T) { 41 | var subProduct SubProductDetails 42 | subProduct, err = basicClient.GetSubProduct("vmware_nsx_t_data_center", "nsx-t_le", "PRODUCT_BINARY") 43 | assert.Nil(t, err) 44 | assert.NotEmpty(t, subProduct.ProductName) 45 | assert.Greater(t, len(subProduct.DlgListByVersion), 0) 46 | } 47 | 48 | func TestGetSubProductDriver(t *testing.T) { 49 | var subProduct SubProductDetails 50 | subProduct, err = basicClient.GetSubProduct("vmware_horizon", "dem+standard", "PRODUCT_BINARY") 51 | assert.Nil(t, err) 52 | assert.NotEmpty(t, subProduct.ProductName) 53 | } 54 | 55 | func TestGetSubProductsMap(t *testing.T) { 56 | var subProducts map[string]SubProductDetails 57 | subProducts, err = basicClient.GetSubProductsMap("vmware_vsphere", "PRODUCT_BINARY", "") 58 | assert.Nil(t, err) 59 | assert.Contains(t, subProducts, "vmtools") 60 | } 61 | 62 | func TestGetSubProductsMapHorizon(t *testing.T) { 63 | var subProducts map[string]SubProductDetails 64 | subProducts, err = basicClient.GetSubProductsMap("vmware_horizon_clients", "PRODUCT_BINARY", "") 65 | assert.Nil(t, err) 66 | assert.Contains(t, subProducts, "cart+win") 67 | assert.Contains(t, subProducts, "cart+andrd_x8632") 68 | assert.Contains(t, subProducts, "cart+lin64") 69 | } 70 | 71 | func TestGetSubProductsMapNsxLe(t *testing.T) { 72 | var subProducts map[string]SubProductDetails 73 | subProducts, err = basicClient.GetSubProductsMap("vmware_nsx", "PRODUCT_BINARY", "") 74 | assert.Nil(t, err) 75 | assert.Contains(t, subProducts, "nsx") 76 | assert.Contains(t, subProducts, "nsx_le") 77 | } 78 | 79 | func TestGetSubProductsMapNsxTLe(t *testing.T) { 80 | var subProducts map[string]SubProductDetails 81 | subProducts, err = basicClient.GetSubProductsMap("vmware_nsx_t_data_center", "PRODUCT_BINARY", "") 82 | assert.Nil(t, err) 83 | assert.Contains(t, subProducts, "nsx-t") 84 | assert.Contains(t, subProducts, "nsx-t_le") 85 | } 86 | 87 | func TestGetSubProductsMapInvalidSlug(t *testing.T) { 88 | var subProductMap map[string]SubProductDetails 89 | subProductMap, err = basicClient.GetSubProductsMap("vsphere", "PRODUCT_BINARY", "" ) 90 | assert.ErrorIs(t, err, ErrorInvalidSlug) 91 | assert.Empty(t, subProductMap, "Expected response to be empty") 92 | } 93 | 94 | func TestGetSubProductsDetails(t *testing.T) { 95 | var subProductDetails DlgList 96 | subProductDetails, err = basicClient.GetSubProductDetails("vmware_vsphere", "vmtools", "6_7", "PRODUCT_BINARY") 97 | assert.Nil(t, err) 98 | assert.NotEmpty(t, subProductDetails.Code, "Expected response to not be empty") 99 | } 100 | 101 | func TestGetSubProductsDetailsInvalidSubProduct(t *testing.T) { 102 | var subProductDetails DlgList 103 | subProductDetails, err = basicClient.GetSubProductDetails("vmware_vsphere", "tools", "6_7", "PRODUCT_BINARY") 104 | assert.NotNil(t, err) 105 | assert.ErrorIs(t, err, ErrorInvalidSubProduct) 106 | assert.Empty(t, subProductDetails.Code, "Expected response to be empty") 107 | } 108 | 109 | func TestGetSubProductsDetailsInvalidMajorVersion(t *testing.T) { 110 | var subProductDetails DlgList 111 | subProductDetails, err = basicClient.GetSubProductDetails("vmware_vsphere", "vmtools", "5_5", "PRODUCT_BINARY") 112 | assert.NotNil(t, err) 113 | assert.ErrorIs(t, err, ErrorInvalidSubProductMajorVersion) 114 | assert.Empty(t, subProductDetails.Code, "Expected response to be empty") 115 | } 116 | 117 | func TestModifyHorizonClientCode(t *testing.T) { 118 | productCode := "cart24fq4_lin_2309.1_tarball" 119 | productCode = modifyHorizonClientCode(productCode) 120 | assert.Equal(t, "cart+tarball", productCode) 121 | 122 | productCode = "one_2" 123 | productCode = modifyHorizonClientCode(productCode) 124 | assert.Equal(t, "one", productCode) 125 | } 126 | 127 | func TestGetProductName(t *testing.T) { 128 | reEndVersion := regexp.MustCompile(`[0-9]+.*`) 129 | productName := "VMware vSphere Hypervisor (ESXi) 8.0U2" 130 | productName = getProductName(productName, "vmware_vsphere", "PRODUCT_BINARY", reEndVersion) 131 | assert.Equal(t, "VMware vSphere Hypervisor (ESXi)", productName) 132 | 133 | // Ensure drivers are unmodified 134 | productName = "VMware ESXi 8.0 native ixgben ENS 1.18.2.0 NIC Driver for Intel Ethernet Controllers 82599, x520, x540, x550, and x552 family" 135 | productName = getProductName(productName, "vmware_vsphere", "DRIVERS_TOOLS", reEndVersion) 136 | assert.Equal(t, productName, "VMware ESXi 8.0 native ixgben ENS 1.18.2.0 NIC Driver for Intel Ethernet Controllers 82599, x520, x540, x550, and x552 family") 137 | 138 | // Ensure drivers are unmodified 139 | productName = "HPE Custom Image for ESXi 7.0 U3 Install CD" 140 | productName = getProductName(productName, "vmware_vsphere", "ADDONS", reEndVersion) 141 | assert.Equal(t, productName, "HPE Custom Image for ESXi 7.0 U3 Install CD") 142 | } 143 | 144 | func TestGetProductCode(t *testing.T) { 145 | reEndVersion := regexp.MustCompile(`[0-9]+.*`) 146 | productCode := "ESXI80U2" 147 | productCode = getProductCode(productCode, "vmware_vsphere", "PRODUCT_BINARY", reEndVersion) 148 | assert.Equal(t, "esxi", productCode) 149 | 150 | // Ensure drivers are unmodified 151 | productCode = "DT-ESXI80-INTEL-I40EN-2650-1OEM" 152 | productCode = getProductCode(productCode, "vmware_vsphere", "DRIVERS_TOOLS", reEndVersion) 153 | assert.Equal(t, "dt-esxi80-intel-i40en-2650-1oem", productCode) 154 | 155 | // Ensure custom isos are unmodified 156 | productCode = "OEM-ESXI70U3-HPE" 157 | productCode = getProductCode(productCode, "vmware_vsphere", "ADDONS", reEndVersion) 158 | assert.Equal(t, "oem-esxi70u3-hpe", productCode) 159 | } -------------------------------------------------------------------------------- /sdk/download_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestFetchDownloadLinkVersionGlob(t *testing.T) { 15 | err = ensureLogin(t) 16 | require.Nil(t, err) 17 | 18 | var downloadPayload []DownloadPayload 19 | downloadPayload, err = authenticatedClient.GenerateDownloadPayload("vmware_tools", "vmtools", "11.*", "VMware-Tools-darwin-*.tar.gz", "PRODUCT_BINARY", true) 20 | require.Nil(t, err) 21 | require.NotEmpty(t, downloadPayload) 22 | assert.NotEmpty(t, downloadPayload[0].ProductId, "Expected response not to be empty") 23 | 24 | t.Logf(fmt.Sprintf("download_payload: %+v\n", downloadPayload)) 25 | 26 | var authorizedDownload AuthorizedDownload 27 | authorizedDownload, _ = authenticatedClient.FetchDownloadLink(downloadPayload[0]) 28 | assert.Nil(t, err) 29 | assert.NotEmpty(t, authorizedDownload.DownloadURL, "Expected response not to be empty") 30 | 31 | t.Logf(fmt.Sprintf("download_details: %+v\n", authorizedDownload)) 32 | } 33 | 34 | func TestFetchDownloadLinkVersionDrivers(t *testing.T) { 35 | err = ensureLogin(t) 36 | require.Nil(t, err) 37 | 38 | var downloadPayload []DownloadPayload 39 | downloadPayload, err = authenticatedClient.GenerateDownloadPayload("vmware_vsphere", "vs-mgmt-sdk80u2", "8.0U2", "VMware-vSphere-SDK-8.0.2-22394481.zip", "DRIVERS_TOOLS", true) 40 | require.Nil(t, err) 41 | require.NotEmpty(t, downloadPayload) 42 | assert.NotEmpty(t, downloadPayload[0].ProductId, "Expected response not to be empty") 43 | 44 | t.Logf(fmt.Sprintf("download_payload: %+v\n", downloadPayload)) 45 | 46 | var authorizedDownload AuthorizedDownload 47 | authorizedDownload, _ = authenticatedClient.FetchDownloadLink(downloadPayload[0]) 48 | assert.Nil(t, err) 49 | assert.NotEmpty(t, authorizedDownload.DownloadURL, "Expected response not to be empty") 50 | 51 | t.Logf(fmt.Sprintf("download_details: %+v\n", authorizedDownload)) 52 | } 53 | 54 | func TestFetchDownloadLinkVersionCustomIso(t *testing.T) { 55 | err = ensureLogin(t) 56 | require.Nil(t, err) 57 | 58 | var downloadPayload []DownloadPayload 59 | downloadPayload, err = authenticatedClient.GenerateDownloadPayload("vmware_vsphere", "oem-esxi80u2-hitachi", "ESXi 8.0.2", "VMware-ESXi-8.0-update2-22380479-hitachi-1301.iso", "CUSTOM_ISO", true) 60 | require.Nil(t, err) 61 | require.NotEmpty(t, downloadPayload) 62 | assert.NotEmpty(t, downloadPayload[0].ProductId, "Expected response not to be empty") 63 | 64 | t.Logf(fmt.Sprintf("download_payload: %+v\n", downloadPayload)) 65 | 66 | var authorizedDownload AuthorizedDownload 67 | authorizedDownload, _ = authenticatedClient.FetchDownloadLink(downloadPayload[0]) 68 | assert.Nil(t, err) 69 | assert.NotEmpty(t, authorizedDownload.DownloadURL, "Expected response not to be empty") 70 | 71 | t.Logf(fmt.Sprintf("download_details: %+v\n", authorizedDownload)) 72 | } 73 | 74 | func TestFetchDownloadLinkVersionAddons(t *testing.T) { 75 | err = ensureLogin(t) 76 | require.Nil(t, err) 77 | 78 | var downloadPayload []DownloadPayload 79 | downloadPayload, err = authenticatedClient.GenerateDownloadPayload("vmware_vsphere", "addon_esxi80u2_hitachi", "ESXi 8.0U2", "VMware-ESXi-8.0u2-addon-22380479-hitachi-1301.zip", "ADDONS", true) 80 | require.Nil(t, err) 81 | require.NotEmpty(t, downloadPayload) 82 | assert.NotEmpty(t, downloadPayload[0].ProductId, "Expected response not to be empty") 83 | 84 | t.Logf(fmt.Sprintf("download_payload: %+v\n", downloadPayload)) 85 | 86 | var authorizedDownload AuthorizedDownload 87 | authorizedDownload, _ = authenticatedClient.FetchDownloadLink(downloadPayload[0]) 88 | assert.Nil(t, err) 89 | assert.NotEmpty(t, authorizedDownload.DownloadURL, "Expected response not to be empty") 90 | 91 | t.Logf(fmt.Sprintf("download_details: %+v\n", authorizedDownload)) 92 | } 93 | 94 | func TestFetchDownloadLinkVersion(t *testing.T) { 95 | err = ensureLogin(t) 96 | require.Nil(t, err) 97 | 98 | var downloadPayload []DownloadPayload 99 | downloadPayload, err = authenticatedClient.GenerateDownloadPayload("vmware_tools", "vmtools", "11.1.1", "VMware-Tools-darwin-*.tar.gz", "PRODUCT_BINARY", true) 100 | assert.Nil(t, err) 101 | require.NotEmpty(t, downloadPayload) 102 | assert.NotEmpty(t, downloadPayload[0].ProductId, "Expected response not to be empty") 103 | 104 | t.Logf(fmt.Sprintf("download_payload: %+v\n", downloadPayload)) 105 | 106 | var authorizedDownload AuthorizedDownload 107 | authorizedDownload, _ = authenticatedClient.FetchDownloadLink(downloadPayload[0]) 108 | assert.Nil(t, err) 109 | assert.NotEmpty(t, authorizedDownload.DownloadURL, "Expected response not to be empty") 110 | 111 | t.Logf(fmt.Sprintf("download_details: %+v\n", authorizedDownload)) 112 | } 113 | 114 | func TestFetchDownloadLinkInvalidVersion(t *testing.T) { 115 | err = ensureLogin(t) 116 | require.Nil(t, err) 117 | 118 | var downloadPayload []DownloadPayload 119 | downloadPayload, err = authenticatedClient.GenerateDownloadPayload("vmware_tools", "vmtools", "666", "VMware-Tools-darwin-*.tar.gz", "PRODUCT_BINARY", true) 120 | assert.ErrorIs(t, err, ErrorInvalidVersion) 121 | assert.Empty(t, downloadPayload, "Expected response to be empty") 122 | } 123 | 124 | func TestFetchDownloadLinkNeedEula(t *testing.T) { 125 | err = ensureLogin(t) 126 | require.Nil(t, err) 127 | 128 | var downloadPayload []DownloadPayload 129 | downloadPayload, err = authenticatedClient.GenerateDownloadPayload("vmware_tools", "vmtools", "11.1.0", "VMware-Tools-darwin-*.tar.gz", "PRODUCT_BINARY", false) 130 | assert.ErrorIs(t, err, ErrorEulaUnaccepted) 131 | assert.Empty(t, downloadPayload, "Expected response to be empty") 132 | } 133 | 134 | func TestFetchDownloadLinkNotEntitled(t *testing.T) { 135 | err = ensureLogin(t) 136 | require.Nil(t, err) 137 | 138 | var downloadPayload []DownloadPayload 139 | downloadPayload, err = authenticatedClient.GenerateDownloadPayload("vmware_nsx_t_data_center", "nsx-t", "3.2.3.1", "nsx-unified-appliance-secondary-*.qcow2", "PRODUCT_BINARY", true) 140 | assert.ErrorIs(t, err, ErrorNotEntitled) 141 | assert.Empty(t, downloadPayload, "Expected response to be empty") 142 | } 143 | 144 | func TestGenerateDownloadInvalidVersionGlob(t *testing.T) { 145 | err = ensureLogin(t) 146 | require.Nil(t, err) 147 | 148 | var downloadPayload []DownloadPayload 149 | downloadPayload, err = authenticatedClient.GenerateDownloadPayload("vmware_tools", "vmtools", "666.*", "VMware-Tools-darwin-*.tar.gz", "PRODUCT_BINARY", true) 150 | assert.ErrorIs(t, err, ErrorNoMatchingVersions) 151 | assert.Empty(t, downloadPayload, "Expected response to be empty") 152 | } 153 | 154 | func TestGenerateDownloadDoubleVersion(t *testing.T) { 155 | err = ensureLogin(t) 156 | require.Nil(t, err) 157 | 158 | var downloadPayload []DownloadPayload 159 | downloadPayload, err = authenticatedClient.GenerateDownloadPayload("vmware_tools", "vmtools", "*.*", "VMware-Tools-darwin-*.tar.gz", "PRODUCT_BINARY", true) 160 | assert.ErrorIs(t, err, ErrorMultipleVersionGlob) 161 | assert.Empty(t, downloadPayload, "Expected response to be empty") 162 | } 163 | -------------------------------------------------------------------------------- /sdk/subproducts.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 VMware, Inc. 2 | // SPDX-License-Identifier: Apache 2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "errors" 8 | "regexp" 9 | "slices" 10 | "sort" 11 | "strings" 12 | ) 13 | 14 | type SubProductDetails struct { 15 | ProductName string 16 | ProductCode string 17 | DlgListByVersion map[string]DlgList 18 | } 19 | 20 | type SubProductSliceElement struct { 21 | Name string 22 | Description string 23 | } 24 | 25 | var ErrorInvalidSubProduct = errors.New("subproduct: invalid subproduct requested") 26 | var ErrorInvalidSubProductMajorVersion = errors.New("subproduct: invalid major version requested") 27 | 28 | func (c *Client) GetSubProductsMap(slug, dlgType, requestedMajorVersion string) (subProductMap map[string]SubProductDetails, err error) { 29 | c.EnsureProductDetailMap() 30 | if err != nil { 31 | return 32 | } 33 | if _, ok := ProductDetailMap[slug]; !ok { 34 | err = ErrorInvalidSlug 35 | return 36 | } 37 | var majorVersions []string 38 | majorVersions, err = c.GetMajorVersionsSlice(slug) 39 | if err != nil { 40 | return 41 | } 42 | 43 | subProductMap = make(map[string]SubProductDetails) 44 | 45 | // Only process requested major version, otherwise process all for slug 46 | if requestedMajorVersion != "" { 47 | if !slices.Contains(majorVersions, requestedMajorVersion) { 48 | err = ErrorInvalidSubProductMajorVersion 49 | return 50 | } 51 | err = c.processMajorVersion(slug, requestedMajorVersion, dlgType, subProductMap) 52 | if err != nil { 53 | return 54 | } 55 | } else { 56 | // Iterate major product versions and extract all unique products 57 | // All version information is stripped 58 | for _, majorVersion := range majorVersions { 59 | c.processMajorVersion(slug, majorVersion, dlgType, subProductMap) 60 | // Invalid version errors need to be ignored, as they come from deprecated products 61 | if err == ErrorInvalidVersion { 62 | err = nil 63 | } else if err != nil { 64 | return 65 | } 66 | } 67 | } 68 | return 69 | } 70 | 71 | func (c *Client) processMajorVersion (slug, majorVersion, dlgType string, subProductMap map[string]SubProductDetails) (err error) { 72 | var dlgEditionsList []DlgEditionsLists 73 | dlgEditionsList, err = c.GetDlgEditionsList(slug, majorVersion, dlgType) 74 | if err != nil { 75 | return 76 | } 77 | 78 | for _, dlgEdition := range dlgEditionsList { 79 | for _, dlgList := range dlgEdition.DlgList { 80 | // Regex captures numbers and all text after 81 | reEndVersion := regexp.MustCompile(`[0-9]+.*`) 82 | 83 | productName := getProductName(dlgList.Name, slug, dlgType, reEndVersion) 84 | productCode := getProductCode(strings.ToLower(dlgList.Code), slug, dlgType, reEndVersion) 85 | 86 | // Initalize the struct for a product code for the first time 87 | if _, ok := subProductMap[productCode]; !ok { 88 | subProductMap[productCode] = SubProductDetails{ 89 | ProductName: productName, 90 | ProductCode: productCode, 91 | DlgListByVersion: make(map[string]DlgList), 92 | } 93 | } 94 | 95 | subProductMap[productCode].DlgListByVersion[majorVersion] = dlgList 96 | 97 | if productCode == "nsx" || productCode == "nsx-t" { 98 | duplicateNsxToNsxLe(subProductMap, productCode, productName, majorVersion, dlgList) 99 | } 100 | } 101 | } 102 | return 103 | } 104 | 105 | func getProductCode(productCode, slug, dlgType string, reEndVersion *regexp.Regexp) (string) { 106 | productCode = strings.ToLower(productCode) 107 | if dlgType != "PRODUCT_BINARY" { 108 | return productCode 109 | } 110 | reMidVersion := regexp.MustCompile(`(-|_)([0-9.]+)(-|_)`) 111 | if strings.HasPrefix(productCode, "cart") { 112 | return modifyHorizonClientCode(productCode) 113 | } 114 | // Horizon clients don't follow a common pattern for API naming. This block aligns the pattern 115 | // Check if product code has text after the version section 116 | if ok := reMidVersion.MatchString(productCode); ok{ 117 | // replace version with + to allow for string to be split when searching 118 | productCode = reMidVersion.ReplaceAllString(productCode, "+") 119 | // remove versions prepended versions 120 | reFpStrip := regexp.MustCompile(`(\+fp[0-9])|(\+hf[0-9])`) 121 | productCode = reFpStrip.ReplaceAllString(productCode, "") 122 | } else { 123 | // when product ends with a version, remove all text after the first number 124 | productCode = reEndVersion.ReplaceAllString(productCode, "") 125 | productCode = strings.TrimSuffix(strings.TrimSuffix(productCode, "_"), "-") 126 | } 127 | return productCode 128 | } 129 | 130 | func getProductName(productName, slug, dlgType string, reEndVersion *regexp.Regexp) (string) { 131 | if dlgType != "PRODUCT_BINARY" { 132 | return productName 133 | } 134 | // Special case for Horizon due to inconsistent naming 135 | if slug == "vmware_horizon" { 136 | reNumbers := regexp.MustCompile(`[0-9.,]+`) 137 | reSpace := regexp.MustCompile(`\s+`) 138 | productName := strings.TrimSpace(reNumbers.ReplaceAllString(productName, "")) 139 | return reSpace.ReplaceAllString(productName, " ") 140 | } else { 141 | return strings.TrimSpace(reEndVersion.ReplaceAllString(productName, "")) 142 | } 143 | } 144 | 145 | func modifyHorizonClientCode(productCode string) (string) { 146 | productCode = strings.Replace(productCode, "-", "_", 1) 147 | 148 | // Remove version numbers at the start of the string only 149 | reHorizon := regexp.MustCompile(`([0-9.].*?)_`) 150 | found := reHorizon.FindString(productCode) 151 | if found != "" { 152 | productCode = strings.Replace(productCode, found, "+", 1) 153 | } 154 | // Handle tarball not following pattern. 155 | if strings.HasSuffix(productCode, "tarball") { 156 | // productCode = strings.Replace(productCode, "lin_+", "", 1) 157 | reHorizonTar := regexp.MustCompile(`lin_([0-9]+.*?)_`) 158 | productCode = reHorizonTar.ReplaceAllString(productCode, "") 159 | } else { 160 | // Remove version numbers at the end 161 | reHorizonVersion := regexp.MustCompile(`_([0-9.].*)`) 162 | productCode = reHorizonVersion.ReplaceAllString(productCode, "") 163 | } 164 | return productCode 165 | } 166 | 167 | // Duplicate NSX LE to a separate subproduct 168 | func duplicateNsxToNsxLe(subProductMap map[string]SubProductDetails, productCode, productName, majorVersion string, dlgList DlgList) { 169 | if _, ok := subProductMap[productCode + "_le"]; !ok { 170 | subProductMap[productCode + "_le"] = SubProductDetails{ 171 | ProductName: productName + " Limited Edition", 172 | ProductCode: productCode + "_le", 173 | DlgListByVersion: make(map[string]DlgList), 174 | } 175 | } 176 | dlgList.Name = dlgList.Name + " Limited Edition" 177 | dlgList.Code = dlgList.Code + "-LE" 178 | subProductMap[productCode + "_le"].DlgListByVersion[majorVersion] = dlgList 179 | } 180 | 181 | func (c *Client) GetSubProductsSlice(slug, dlgType, majorVersion string) (data []SubProductDetails, err error) { 182 | subProductMap, err := c.GetSubProductsMap(slug, dlgType, majorVersion) 183 | if err != nil { 184 | return 185 | } 186 | 187 | // Sort keys to output sorted slice 188 | keys := make([]string, len(subProductMap)) 189 | i := 0 190 | for key := range subProductMap { 191 | keys[i] = key 192 | i++ 193 | } 194 | sort.Strings(keys) 195 | 196 | // Append to array using sorted keys to fetch from map 197 | for _, key := range keys { 198 | data = append(data, subProductMap[key]) 199 | } 200 | 201 | return 202 | } 203 | 204 | func (c *Client) GetSubProduct(slug, subProduct, dlgType string) (data SubProductDetails, err error) { 205 | var subProductMap map[string]SubProductDetails 206 | subProductMap, err = c.GetSubProductsMap(slug, dlgType, "") 207 | if err != nil { 208 | return 209 | } 210 | 211 | if foundSubProduct, ok := subProductMap[subProduct]; !ok { 212 | err = ErrorInvalidSubProduct 213 | } else { 214 | data = foundSubProduct 215 | } 216 | 217 | return 218 | } 219 | 220 | func (c *Client) GetSubProductDetails(slug, subProduct, majorVersion, dlgType string) (data DlgList, err error) { 221 | var subProducts map[string]SubProductDetails 222 | subProducts, err = c.GetSubProductsMap(slug, dlgType, "") 223 | if err != nil { 224 | return 225 | } 226 | 227 | if subProduct, ok := subProducts[subProduct]; ok { 228 | if dlgList, ok := subProduct.DlgListByVersion[majorVersion]; ok { 229 | data = dlgList 230 | } else { 231 | err = ErrorInvalidSubProductMajorVersion 232 | } 233 | 234 | } else { 235 | err = ErrorInvalidSubProduct 236 | } 237 | 238 | return 239 | } 240 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the 14 | copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all other 17 | entities that control, are controlled by, or are under common control 18 | with that entity. For the purposes of this definition, "control" means 19 | (i) the power, direct or indirect, to cause the direction or management 20 | of such entity, whether by contract or otherwise, or (ii) ownership 21 | of fifty percent (50%) or more of the outstanding shares, or (iii) 22 | beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation source, 29 | and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical transformation 32 | or translation of a Source form, including but not limited to compiled 33 | object code, generated documentation, and conversions to other media 34 | types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a copyright 38 | notice that is included in or attached to the work (an example is provided 39 | in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object form, 42 | that is based on (or derived from) the Work and for which the editorial 43 | revisions, annotations, elaborations, or other modifications represent, 44 | as a whole, an original work of authorship. For the purposes of this 45 | License, Derivative Works shall not include works that remain separable 46 | from, or merely link (or bind by name) to the interfaces of, the Work 47 | and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including the 50 | original version of the Work and any modifications or additions to 51 | that Work or Derivative Works thereof, that is intentionally submitted 52 | to Licensor for inclusion in the Work by the copyright owner or by an 53 | individual or Legal Entity authorized to submit on behalf of the copyright 54 | owner. For the purposes of this definition, "submitted" means any form of 55 | electronic, verbal, or written communication sent to the Licensor or its 56 | representatives, including but not limited to communication on electronic 57 | mailing lists, source code control systems, and issue tracking systems 58 | that are managed by, or on behalf of, the Licensor for the purpose of 59 | discussing and improving the Work, but excluding communication that is 60 | conspicuously marked or otherwise designated in writing by the copyright 61 | owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. 68 | Subject to the terms and conditions of this License, each Contributor 69 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 70 | royalty-free, irrevocable copyright license to reproduce, prepare 71 | Derivative Works of, publicly display, publicly perform, sublicense, and 72 | distribute the Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. 75 | Subject to the terms and conditions of this License, each Contributor 76 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 77 | royalty- free, irrevocable (except as stated in this section) patent 78 | license to make, have made, use, offer to sell, sell, import, and 79 | otherwise transfer the Work, where such license applies only to those 80 | patent claims licensable by such Contributor that are necessarily 81 | infringed by their Contribution(s) alone or by combination of 82 | their Contribution(s) with the Work to which such Contribution(s) 83 | was submitted. If You institute patent litigation against any entity 84 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 85 | Work or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses granted 87 | to You under this License for that Work shall terminate as of the date 88 | such litigation is filed. 89 | 90 | 4. Redistribution. 91 | You may reproduce and distribute copies of the Work or Derivative Works 92 | thereof in any medium, with or without modifications, and in Source or 93 | Object form, provided that You meet the following conditions: 94 | 95 | a. You must give any other recipients of the Work or Derivative Works 96 | a copy of this License; and 97 | 98 | b. You must cause any modified files to carry prominent notices stating 99 | that You changed the files; and 100 | 101 | c. You must retain, in the Source form of any Derivative Works that 102 | You distribute, all copyright, patent, trademark, and attribution 103 | notices from the Source form of the Work, excluding those notices 104 | that do not pertain to any part of the Derivative Works; and 105 | 106 | d. If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one of 111 | the following places: within a NOTICE text file distributed as part 112 | of the Derivative Works; within the Source form or documentation, 113 | if provided along with the Derivative Works; or, within a display 114 | generated by the Derivative Works, if and wherever such third-party 115 | notices normally appear. The contents of the NOTICE file are for 116 | informational purposes only and do not modify the License. You 117 | may add Your own attribution notices within Derivative Works that 118 | You distribute, alongside or as an addendum to the NOTICE text 119 | from the Work, provided that such additional attribution notices 120 | cannot be construed as modifying the License. You may add Your own 121 | copyright statement to Your modifications and may provide additional 122 | or different license terms and conditions for use, reproduction, or 123 | distribution of Your modifications, or for any such Derivative Works 124 | as a whole, provided Your use, reproduction, and distribution of the 125 | Work otherwise complies with the conditions stated in this License. 126 | 127 | 5. Submission of Contributions. 128 | Unless You explicitly state otherwise, any Contribution intentionally 129 | submitted for inclusion in the Work by You to the Licensor shall be 130 | under the terms and conditions of this License, without any additional 131 | terms or conditions. Notwithstanding the above, nothing herein shall 132 | supersede or modify the terms of any separate license agreement you may 133 | have executed with Licensor regarding such Contributions. 134 | 135 | 6. Trademarks. 136 | This License does not grant permission to use the trade names, trademarks, 137 | service marks, or product names of the Licensor, except as required for 138 | reasonable and customary use in describing the origin of the Work and 139 | reproducing the content of the NOTICE file. 140 | 141 | 7. Disclaimer of Warranty. 142 | Unless required by applicable law or agreed to in writing, Licensor 143 | provides the Work (and each Contributor provides its Contributions) on 144 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 145 | express or implied, including, without limitation, any warranties or 146 | conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR 147 | A PARTICULAR PURPOSE. You are solely responsible for determining the 148 | appropriateness of using or redistributing the Work and assume any risks 149 | associated with Your exercise of permissions under this License. 150 | 151 | 8. Limitation of Liability. 152 | In no event and under no legal theory, whether in tort (including 153 | negligence), contract, or otherwise, unless required by applicable law 154 | (such as deliberate and grossly negligent acts) or agreed to in writing, 155 | shall any Contributor be liable to You for damages, including any direct, 156 | indirect, special, incidental, or consequential damages of any character 157 | arising as a result of this License or out of the use or inability to 158 | use the Work (including but not limited to damages for loss of goodwill, 159 | work stoppage, computer failure or malfunction, or any and all other 160 | commercial damages or losses), even if such Contributor has been advised 161 | of the possibility of such damages. 162 | 163 | 9. Accepting Warranty or Additional Liability. 164 | While redistributing the Work or Derivative Works thereof, You may 165 | choose to offer, and charge a fee for, acceptance of support, warranty, 166 | indemnity, or other liability obligations and/or rights consistent with 167 | this License. However, in accepting such obligations, You may act only 168 | on Your own behalf and on Your sole responsibility, not on behalf of 169 | any other Contributor, and only if You agree to indemnify, defend, and 170 | hold each Contributor harmless for any liability incurred by, or claims 171 | asserted against, such Contributor by reason of your accepting any such 172 | warranty or additional liability. 173 | 174 | END OF TERMS AND CONDITIONS 175 | 176 | APPENDIX: How to apply the Apache License to your work. 177 | 178 | To apply the Apache License to your work, attach the following 179 | boilerplate notice, with the fields enclosed by brackets "[]" 180 | replaced with your own identifying information. (Don't include 181 | the brackets!) The text should be enclosed in the appropriate 182 | comment syntax for the file format. We also recommend that a 183 | file or class name and description of purpose be included on the 184 | same "printed page" as the copyright notice for easier 185 | identification within third-party archives. 186 | 187 | Copyright [yyyy] [name of copyright owner] 188 | 189 | Licensed under the Apache License, Version 2.0 (the "License"); 190 | you may not use this file except in compliance with the License. 191 | You may obtain a copy of the License at 192 | 193 | http://www.apache.org/licenses/LICENSE-2.0 194 | 195 | Unless required by applicable law or agreed to in writing, software 196 | distributed under the License is distributed on an "AS IS" BASIS, 197 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 198 | See the License for the specific language governing permissions and 199 | limitations under the License. -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 10 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 11 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 12 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 13 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 14 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 15 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 16 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 17 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 18 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 19 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 20 | github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= 21 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 22 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= 23 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= 24 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 25 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 26 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 27 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 28 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 29 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 30 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 32 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 33 | github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY= 34 | github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= 35 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 36 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 37 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 38 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 39 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 40 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 41 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 42 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 43 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 44 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 45 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 46 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 48 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 49 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 50 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 51 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 52 | github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 53 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 54 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 55 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 56 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 57 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 58 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 59 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 60 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 61 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 62 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 63 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 64 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 65 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 66 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 67 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 68 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 69 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 70 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 71 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 72 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 73 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 74 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 75 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 76 | github.com/orirawlings/persistent-cookiejar v0.3.2 h1:6SZYaE3s8H+x1Xdw+E8mslO1OXLme6HZMv95pjiwEPU= 77 | github.com/orirawlings/persistent-cookiejar v0.3.2/go.mod h1:mndFQuhWIsBaloT/dZPibBxlWfFA9R1EraeTTcSOf8A= 78 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 79 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 81 | github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9Se4YQIRkDTCw1EJls9xTUCaCeRM= 82 | github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= 83 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 84 | github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= 85 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 86 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 87 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 88 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 89 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 90 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 91 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 92 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 93 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 94 | go4.org v0.0.0-20201209231011-d4a079459e60 h1:iqAGo78tVOJXELHQFRjR6TMwItrvXH4hrGJ32I/NFF8= 95 | go4.org v0.0.0-20201209231011-d4a079459e60/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= 96 | go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= 97 | go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 100 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 101 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 102 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 103 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 104 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 105 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 106 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 107 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 108 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 109 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 110 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 111 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 112 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 113 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 114 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 115 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 116 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 117 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 118 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 119 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 120 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 121 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 122 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 123 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 124 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 125 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 126 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 127 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 128 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 129 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 130 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 131 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 132 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 133 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 134 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 135 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 136 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 137 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 138 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 139 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 140 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 141 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 142 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 143 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 144 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 145 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 146 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 147 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 148 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 149 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 150 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 151 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 152 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 153 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 154 | golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= 155 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 156 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 157 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 158 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 159 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 160 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 161 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 162 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 163 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 164 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 165 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 166 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 167 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 168 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 169 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 171 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 172 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 173 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 175 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 178 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 181 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 183 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 188 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 189 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 190 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 191 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 192 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 193 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 194 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 195 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 196 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 197 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 198 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 199 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 200 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 201 | golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= 202 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 203 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 204 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 205 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 206 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 207 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 208 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 209 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 210 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 211 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 212 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 213 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 214 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 215 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 216 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 217 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 218 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 219 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 220 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 221 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 222 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 223 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 224 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 225 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 226 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 227 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 228 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 229 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 230 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 231 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 232 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 233 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 234 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 235 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 236 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 237 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 238 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 239 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 240 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 241 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 242 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 243 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 244 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 245 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 246 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 247 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 248 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 249 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 250 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 251 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 252 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 253 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 254 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 255 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 256 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 257 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 258 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 259 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 260 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 261 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 262 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 263 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 264 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 265 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 266 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 267 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 268 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 269 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 270 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 271 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 272 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 273 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 274 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 275 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 276 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 277 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 278 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 279 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 280 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 281 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 282 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 283 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 284 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 285 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 286 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 287 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 288 | gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs= 289 | gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= 290 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 291 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 292 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 293 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 294 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 295 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 296 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 297 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 298 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 299 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 300 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 301 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 302 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 303 | --------------------------------------------------------------------------------