├── .github └── workflows │ ├── docker.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── docs ├── data-sources │ ├── album.md │ ├── search_track.md │ └── track.md ├── index.md └── resources │ ├── library.md │ ├── library_albums.md │ └── playlist.md ├── examples ├── data-sources │ ├── spotify_search_track │ │ └── data-source.tf │ └── spotify_track │ │ └── data-source.tf ├── provider │ └── provider.tf └── resources │ ├── spotify_library │ └── resource.tf │ ├── spotify_library_albums │ └── resource.tf │ └── spotify_playlist │ ├── import.sh │ └── resource.tf ├── go.mod ├── go.sum ├── main.go ├── spotify ├── client.go ├── data_source_album.go ├── data_source_album_test.go ├── data_source_search_track.go ├── data_source_search_track_test.go ├── data_source_track.go ├── data_source_track_test.go ├── provider.go ├── provider_test.go ├── resource_library_albums.go ├── resource_library_albums_test.go ├── resource_library_tracks.go ├── resource_library_tracks_test.go ├── resource_playlist.go ├── resource_playlist_test.go ├── util.go └── util_test.go └── spotify_auth_proxy ├── Dockerfile ├── README.md ├── go.mod ├── go.sum └── main.go /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | branches: 11 | - 'main' 12 | 13 | env: 14 | REGISTRY: ghcr.io 15 | IMAGE_NAME: ${{ github.repository_owner }}/spotify-auth-proxy 16 | 17 | jobs: 18 | docker: 19 | runs-on: ubuntu-latest 20 | 21 | permissions: 22 | contents: read 23 | packages: write 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Docker meta 30 | id: meta 31 | uses: docker/metadata-action@v5 32 | with: 33 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 34 | tags: | 35 | type=ref,event=pr 36 | type=ref,event=branch 37 | type=semver,pattern={{version}} 38 | type=semver,pattern={{major}}.{{minor}} 39 | 40 | - name: Set up Docker Buildx 41 | uses: docker/setup-buildx-action@v3 42 | 43 | - name: Login to DockerHub 44 | uses: docker/login-action@v3 45 | if: ${{ github.event_name != 'pull_request' }} 46 | with: 47 | registry: ${{ env.REGISTRY }} 48 | username: ${{ github.actor }} 49 | password: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | - name: Build and push 52 | uses: docker/build-push-action@v5 53 | with: 54 | context: ./spotify_auth_proxy 55 | push: ${{ github.event_name != 'pull_request' }} 56 | tags: ${{ steps.meta.outputs.tags }} 57 | labels: ${{ steps.meta.outputs.labels }} 58 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: 1.16 24 | 25 | - name: Import GPG key 26 | id: import_gpg 27 | uses: crazy-max/ghaction-import-gpg@v3 28 | with: 29 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} 30 | passphrase: ${{ secrets.PASSPHRASE }} 31 | 32 | - name: Run GoReleaser 33 | uses: goreleaser/goreleaser-action@v2 34 | with: 35 | distribution: goreleaser 36 | version: latest 37 | args: release --rm-dist 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | branches: 9 | - 'main' 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Install Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: 1.16 19 | 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | 23 | - name: Run tests 24 | run: go test -v ./... -coverprofile=coverage.txt -covermode=atomic 25 | 26 | - name: Upload Code Coverage 27 | uses: codecov/codecov-action@v1 28 | 29 | - name: Run golangci-lint 30 | uses: golangci/golangci-lint-action@v2.5.2 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.env 2 | *.tfstate* 3 | *.terraform/ 4 | build/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | before: 4 | hooks: 5 | # this is just an example and not a requirement for provider building/publishing 6 | - go mod tidy 7 | builds: 8 | - env: 9 | # goreleaser does not work with CGO, it could also complicate 10 | # usage by users in CI/CD systems like Terraform Cloud where 11 | # they are unable to install libraries. 12 | - CGO_ENABLED=0 13 | mod_timestamp: '{{ .CommitTimestamp }}' 14 | flags: 15 | - -trimpath 16 | ldflags: 17 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 18 | goos: 19 | - freebsd 20 | - windows 21 | - linux 22 | - darwin 23 | goarch: 24 | - amd64 25 | - '386' 26 | - arm 27 | - arm64 28 | ignore: 29 | - goos: darwin 30 | goarch: '386' 31 | binary: '{{ .ProjectName }}_v{{ .Version }}' 32 | archives: 33 | - format: zip 34 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 35 | checksum: 36 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 37 | algorithm: sha256 38 | signs: 39 | - artifacts: checksum 40 | args: 41 | # if you are using this in a GitHub action or some other automated pipeline, you 42 | # need to pass the batch flag to indicate its not interactive. 43 | - "--batch" 44 | - "--local-user" 45 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 46 | - "--output" 47 | - "${signature}" 48 | - "--detach-sign" 49 | - "${artifact}" 50 | release: 51 | # If you want to manually examine the release before its live, uncomment this line: 52 | # draft: true 53 | changelog: 54 | skip: true 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Conrad Ludgate 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-provider-spotify 2 | 3 | [![docs](https://img.shields.io/static/v1?label=docs&message=terraform&color=informational&style=for-the-badge&logo=terraform)](https://registry.terraform.io/providers/conradludgate/spotify/latest/docs) 4 | ![downloads](https://img.shields.io/badge/dynamic/json?label=downloads&query=%24.data.attributes.downloads&url=https%3A%2F%2Fregistry.terraform.io%2Fv2%2Fproviders%2F1325&style=for-the-badge&color=brightgreen&logo=terraform) 5 | ![latest version](https://img.shields.io/badge/dynamic/json?label=version&query=%24.data[(%40.length-1)].attributes.version&url=https%3A%2F%2Fregistry.terraform.io%2Fv2%2Fproviders%2F1325%2Fprovider-versions&style=for-the-badge&color=orange&logo=terraform) 6 | [![code coverage](https://img.shields.io/codecov/c/gh/conradludgate/terraform-provider-spotify?style=for-the-badge)](https://app.codecov.io/gh/conradludgate/terraform-provider-spotify/) 7 | 8 | This is a terraform provider for managing your spotify playlists. 9 | 10 | Featured tutorial - https://learn.hashicorp.com/tutorials/terraform/spotify-playlist 11 | 12 | Featured interview - https://www.hashicorp.com/blog/build-your-summer-spotify-playlist-with-terraform 13 | 14 | > I am not affiliated with Hashicorp or Terraform. 15 | > 16 | > If you are having trouble with the provider, try updating to the latest version 17 | > before submitting a bug report 18 | 19 | ## Example 20 | 21 | ```tf 22 | resource "spotify_playlist" "playlist" { 23 | name = "My playlist" 24 | description = "My playlist is so awesome" 25 | public = false 26 | 27 | tracks = flatten([ 28 | data.spotify_track.overkill.id, 29 | data.spotify_track.blackwater.id, 30 | data.spotify_track.overkill.id, 31 | data.spotify_search_track.search.tracks[*].id, 32 | ]) 33 | } 34 | 35 | data "spotify_track" "overkill" { 36 | url = "https://open.spotify.com/track/4XdaaDFE881SlIaz31pTAG" 37 | } 38 | data "spotify_track" "blackwater" { 39 | spotify_id = "4lE6N1E0L8CssgKEUCgdbA" 40 | } 41 | 42 | data "spotify_search_track" "search" { 43 | name = "Somebody Told Me" 44 | artist = "The Killers" 45 | album = "Hot Fuss" 46 | } 47 | 48 | output "test" { 49 | value = data.spotify_search_track.search.tracks 50 | } 51 | ``` 52 | 53 | 54 | ## Installation 55 | 56 | Add the following to your terraform configuration 57 | 58 | ```tf 59 | terraform { 60 | required_providers { 61 | spotify = { 62 | source = "conradludgate/spotify" 63 | version = "~> 0.2.0" 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ## How to use 70 | 71 | First, you need an instance of a spotify oauth2 server running. This acts as a middleware between terraform and spotify to allow easy access to access tokens. 72 | 73 | ### Public proxy 74 | 75 | For a simple way to manage your spotify oauth2 tokens is to use https://oauth2.conrad.cafe. ([source code](https://github.com/conradludgate/oauth2-proxy)) 76 | 77 | Register a new account, create a spotify token with the following scopes 78 | 79 | * user-read-email 80 | * user-read-private 81 | * playlist-read-private 82 | * playlist-modify-private 83 | * playlist-modify-public 84 | * user-library-read 85 | * user-library-modify 86 | 87 | Then take note of the token id in the URL and the API key that is shown on the page 88 | 89 | Configure the terraform provider like so 90 | 91 | ```tf 92 | provider "spotify" { 93 | auth_server = "https://oauth2.conrad.cafe" 94 | api_key = var.spotify_api_key 95 | username = "your username" 96 | token_id = "your token id" 97 | } 98 | 99 | variable "spotify_api_key" { 100 | type = string 101 | } 102 | ``` 103 | 104 | ### Self hosted 105 | 106 | If you want a bit more control over your tokens, you can self host a simple instance of the oauth2 proxy designed specifically for this terraform provider 107 | 108 | See [spotify_auth_proxy](/spotify_auth_proxy) to get started. 109 | 110 | Once you have the server running, make note of the API Key it gives you. 111 | 112 | Configure the terraform provider like so 113 | 114 | ```tf 115 | variable "spotify_api_key" { 116 | type = string 117 | } 118 | 119 | provider "spotify" { 120 | api_key = var.spotify_api_key 121 | } 122 | ``` 123 | -------------------------------------------------------------------------------- /docs/data-sources/album.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "spotify_album Data Source - terraform-provider-spotify" 3 | subcategory: "" 4 | description: |- 5 | 6 | --- 7 | 8 | # Data Source `spotify_album` 9 | 10 | 11 | 12 | 13 | 14 | ## Schema 15 | 16 | ### Optional 17 | 18 | - **id** (String) The ID of this resource. 19 | - **spotify_id** (String) Spotify ID of the album 20 | - **url** (String) Spotify URL of the album 21 | 22 | ### Read-only 23 | 24 | - **artists** (List of String) The spotify IDs of the artists 25 | - **name** (String) The Name of the album 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/data-sources/search_track.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "spotify_search_track Data Source - terraform-provider-spotify" 3 | subcategory: "" 4 | description: |- 5 | 6 | --- 7 | 8 | # Data Source `spotify_search_track` 9 | 10 | 11 | 12 | ## Example Usage 13 | 14 | ```terraform 15 | resource "spotify_playlist" "ariana_grande" { 16 | name = "My Ariana Grande Playlist" 17 | 18 | tracks = flatten([ 19 | spotify_search_track.ariana_grande.tracks[*].id, 20 | ]) 21 | } 22 | 23 | data "spotify_search_track" "ariana_grande" { 24 | artist = "Ariana Grande" 25 | limit = 10 26 | } 27 | ``` 28 | 29 | ## Schema 30 | 31 | ### Optional 32 | 33 | - **album** (String) Name of the album 34 | - **artist** (String) Name of the artist 35 | - **explicit** (Boolean) Filter to allow explicit tracks 36 | - **id** (String) The ID of this resource. 37 | - **limit** (Number) 38 | - **name** (String) Name of the track 39 | - **year** (String) Year of release 40 | 41 | ### Read-only 42 | 43 | - **tracks** (List of Object) List of tracks found (see [below for nested schema](#nestedatt--tracks)) 44 | 45 | 46 | ### Nested Schema for `tracks` 47 | 48 | Read-only: 49 | 50 | - **album** (String) 51 | - **artists** (List of String) 52 | - **id** (String) 53 | - **name** (String) 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/data-sources/track.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "spotify_track Data Source - terraform-provider-spotify" 3 | subcategory: "" 4 | description: |- 5 | 6 | --- 7 | 8 | # Data Source `spotify_track` 9 | 10 | 11 | 12 | ## Example Usage 13 | 14 | ```terraform 15 | data "spotify_track" "overkill" { 16 | url = "https://open.spotify.com/track/4XdaaDFE881SlIaz31pTAG" 17 | 18 | ## Computed 19 | # name = "Overkill" 20 | # artists = ["0qPGd8tOMHlFZt8EA1uLFY"] 21 | # album = "64ey3KHg3uepidKmJrb4ka" 22 | } 23 | 24 | data "spotify_track" "blackwater" { 25 | spotify_id = "4lE6N1E0L8CssgKEUCgdbA" 26 | 27 | ## Computed 28 | # name = "Blackwater" 29 | # artists = ["0qPGd8tOMHlFZt8EA1uLFY"] 30 | # album = "1AUS845POFhV3oDytPImEZ" 31 | } 32 | ``` 33 | 34 | ## Schema 35 | 36 | ### Optional 37 | 38 | - **id** (String) The ID of this resource. 39 | - **spotify_id** (String) Spotify ID of the track 40 | - **url** (String) Spotify URL of the track 41 | 42 | ### Read-only 43 | 44 | - **album** (String) The spotify ID of the album 45 | - **artists** (List of String) The spotify IDs of the artists 46 | - **name** (String) The Name of the track 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "spotify Provider" 3 | subcategory: "" 4 | description: |- 5 | 6 | --- 7 | 8 | # spotify Provider 9 | 10 | 11 | 12 | ## Example Usage 13 | 14 | ```terraform 15 | provider "spotify" { 16 | api_key = var.spotify_api_key 17 | } 18 | 19 | # See https://github.com/conradludgate/terraform-provider-spotify#how-to-use 20 | # for how to get an api key 21 | variable "spotify_api_key" { 22 | type = string 23 | } 24 | ``` 25 | 26 | ## Schema 27 | 28 | ### Required 29 | 30 | - **api_key** (String) Oauth2 Proxy API Key 31 | 32 | ### Optional 33 | 34 | - **auth_server** (String) Oauth2 Proxy URL 35 | - **token_id** (String) Oauth2 Proxy token ID 36 | - **username** (String) Oauth2 Proxy username 37 | -------------------------------------------------------------------------------- /docs/resources/library.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "spotify_library Resource - terraform-provider-spotify" 3 | subcategory: "" 4 | description: |- 5 | 6 | --- 7 | 8 | # Resource `spotify_library` 9 | 10 | 11 | 12 | ## Example Usage 13 | 14 | ```terraform 15 | resource "spotify_library" "my_library" { 16 | tracks = [ 17 | data.spotify_track.overkill.id, 18 | data.spotify_track.blackwater.id, 19 | data.spotify_track.snowblind.id, 20 | ] 21 | } 22 | 23 | data "spotify_track" "overkill" { 24 | url = "https://open.spotify.com/track/4XdaaDFE881SlIaz31pTAG" 25 | } 26 | data "spotify_track" "blackwater" { 27 | url = "https://open.spotify.com/track/4lE6N1E0L8CssgKEUCgdbA" 28 | } 29 | data "spotify_track" "snowblind" { 30 | url = "https://open.spotify.com/track/7FCG2wIYG1XvGRUMACC2cD" 31 | } 32 | ``` 33 | 34 | ## Schema 35 | 36 | ### Required 37 | 38 | - **tracks** (Set of String) The list of track IDs to save to your 'liked tracks'. *Note, if used incorrectly you may unlike all of your tracks - use with caution* 39 | 40 | ### Optional 41 | 42 | - **id** (String) The ID of this resource. 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/resources/library_albums.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "spotify_library_albums Resource - terraform-provider-spotify" 3 | subcategory: "" 4 | description: |- 5 | 6 | --- 7 | 8 | # Resource `spotify_library_albums` 9 | 10 | 11 | 12 | ## Example Usage 13 | 14 | ```terraform 15 | resource "spotify_library_albums" "my_album" { 16 | albums = [ 17 | data.spotify_album.only_in_dreams.id, 18 | data.spotify_album.the_promised_land.id, 19 | ] 20 | } 21 | 22 | data "spotify_album" "only_in_dreams" { 23 | spotify_id = "35axN2yrxRiycF2pA8mZaB" 24 | } 25 | 26 | data "spotify_album" "the_promised_land" { 27 | url = "https://open.spotify.com/album/3nRnJkUJYFfxcOGgU6LNci" 28 | } 29 | ``` 30 | 31 | ## Schema 32 | 33 | ### Required 34 | 35 | - **albums** (Set of String) The list of track IDs to save to your 'liked albums'. *Note, if used incorrectly you may unlike all of your albums - use with caution* 36 | 37 | ### Optional 38 | 39 | - **id** (String) The ID of this resource. 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/resources/playlist.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "spotify_playlist Resource - terraform-provider-spotify" 3 | subcategory: "" 4 | description: |- 5 | Resource to manage a spotify playlist. 6 | --- 7 | 8 | # Resource `spotify_playlist` 9 | 10 | Resource to manage a spotify playlist. 11 | 12 | ## Example Usage 13 | 14 | ```terraform 15 | resource "spotify_playlist" "playlist" { 16 | name = "My playlist" 17 | description = "My playlist is so awesome" 18 | public = false 19 | 20 | tracks = [ 21 | data.spotify_track.overkill.id, 22 | data.spotify_track.blackwater.id, 23 | data.spotify_track.snowblind.id, 24 | ] 25 | } 26 | 27 | data "spotify_track" "overkill" { 28 | url = "https://open.spotify.com/track/4XdaaDFE881SlIaz31pTAG" 29 | } 30 | data "spotify_track" "blackwater" { 31 | url = "https://open.spotify.com/track/4lE6N1E0L8CssgKEUCgdbA" 32 | } 33 | data "spotify_track" "snowblind" { 34 | url = "https://open.spotify.com/track/7FCG2wIYG1XvGRUMACC2cD" 35 | } 36 | ``` 37 | 38 | ## Schema 39 | 40 | ### Required 41 | 42 | - **name** (String) The name of the resulting playlist 43 | - **tracks** (List of String) A set of tracks for the playlist to contain 44 | 45 | ### Optional 46 | 47 | - **description** (String) The description of the resulting playlist 48 | - **id** (String) The ID of this resource. 49 | - **public** (Boolean) Whether the playlist can be accessed publically 50 | 51 | ### Read-only 52 | 53 | - **snapshot_id** (String) 54 | 55 | ## Import 56 | 57 | Import is supported using the following syntax: 58 | 59 | ```shell 60 | # Using the playlist ID 61 | # https://open.spotify.com/playlist/37i9dQZF1DWVs8I62NcHks (a playlist share link) 62 | # ^^^^^^^^^^^^^^^^^^^^^^ 63 | terraform import spotify_playlist.example 37i9dQZF1DWVs8I62NcHks 64 | ``` 65 | -------------------------------------------------------------------------------- /examples/data-sources/spotify_search_track/data-source.tf: -------------------------------------------------------------------------------- 1 | resource "spotify_playlist" "ariana_grande" { 2 | name = "My Ariana Grande Playlist" 3 | 4 | tracks = flatten([ 5 | spotify_search_track.ariana_grande.tracks[*].id, 6 | ]) 7 | } 8 | 9 | data "spotify_search_track" "ariana_grande" { 10 | artist = "Ariana Grande" 11 | limit = 10 12 | } 13 | -------------------------------------------------------------------------------- /examples/data-sources/spotify_track/data-source.tf: -------------------------------------------------------------------------------- 1 | data "spotify_track" "overkill" { 2 | url = "https://open.spotify.com/track/4XdaaDFE881SlIaz31pTAG" 3 | 4 | ## Computed 5 | # name = "Overkill" 6 | # artists = ["0qPGd8tOMHlFZt8EA1uLFY"] 7 | # album = "64ey3KHg3uepidKmJrb4ka" 8 | } 9 | 10 | data "spotify_track" "blackwater" { 11 | spotify_id = "4lE6N1E0L8CssgKEUCgdbA" 12 | 13 | ## Computed 14 | # name = "Blackwater" 15 | # artists = ["0qPGd8tOMHlFZt8EA1uLFY"] 16 | # album = "1AUS845POFhV3oDytPImEZ" 17 | } 18 | -------------------------------------------------------------------------------- /examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | provider "spotify" { 2 | api_key = var.spotify_api_key 3 | } 4 | 5 | # See https://github.com/conradludgate/terraform-provider-spotify#how-to-use 6 | # for how to get an api key 7 | variable "spotify_api_key" { 8 | type = string 9 | } 10 | -------------------------------------------------------------------------------- /examples/resources/spotify_library/resource.tf: -------------------------------------------------------------------------------- 1 | resource "spotify_library" "my_library" { 2 | tracks = [ 3 | data.spotify_track.overkill.id, 4 | data.spotify_track.blackwater.id, 5 | data.spotify_track.snowblind.id, 6 | ] 7 | } 8 | 9 | data "spotify_track" "overkill" { 10 | url = "https://open.spotify.com/track/4XdaaDFE881SlIaz31pTAG" 11 | } 12 | data "spotify_track" "blackwater" { 13 | url = "https://open.spotify.com/track/4lE6N1E0L8CssgKEUCgdbA" 14 | } 15 | data "spotify_track" "snowblind" { 16 | url = "https://open.spotify.com/track/7FCG2wIYG1XvGRUMACC2cD" 17 | } 18 | -------------------------------------------------------------------------------- /examples/resources/spotify_library_albums/resource.tf: -------------------------------------------------------------------------------- 1 | resource "spotify_library_albums" "my_album" { 2 | albums = [ 3 | data.spotify_album.only_in_dreams.id, 4 | data.spotify_album.the_promised_land.id, 5 | ] 6 | } 7 | 8 | data "spotify_album" "only_in_dreams" { 9 | spotify_id = "35axN2yrxRiycF2pA8mZaB" 10 | } 11 | 12 | data "spotify_album" "the_promised_land" { 13 | url = "https://open.spotify.com/album/3nRnJkUJYFfxcOGgU6LNci" 14 | } 15 | -------------------------------------------------------------------------------- /examples/resources/spotify_playlist/import.sh: -------------------------------------------------------------------------------- 1 | # Using the playlist ID 2 | # https://open.spotify.com/playlist/37i9dQZF1DWVs8I62NcHks (a playlist share link) 3 | # ^^^^^^^^^^^^^^^^^^^^^^ 4 | terraform import spotify_playlist.example 37i9dQZF1DWVs8I62NcHks 5 | -------------------------------------------------------------------------------- /examples/resources/spotify_playlist/resource.tf: -------------------------------------------------------------------------------- 1 | resource "spotify_playlist" "playlist" { 2 | name = "My playlist" 3 | description = "My playlist is so awesome" 4 | public = false 5 | 6 | tracks = [ 7 | data.spotify_track.overkill.id, 8 | data.spotify_track.blackwater.id, 9 | data.spotify_track.snowblind.id, 10 | ] 11 | } 12 | 13 | data "spotify_track" "overkill" { 14 | url = "https://open.spotify.com/track/4XdaaDFE881SlIaz31pTAG" 15 | } 16 | data "spotify_track" "blackwater" { 17 | url = "https://open.spotify.com/track/4lE6N1E0L8CssgKEUCgdbA" 18 | } 19 | data "spotify_track" "snowblind" { 20 | url = "https://open.spotify.com/track/7FCG2wIYG1XvGRUMACC2cD" 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/conradludgate/terraform-provider-spotify 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.37.0 // indirect 7 | github.com/go-test/deep v1.0.3 8 | github.com/hashicorp/hcl/v2 v2.8.2 // indirect 9 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.9.0 10 | github.com/jarcoal/httpmock v1.0.8 11 | github.com/stretchr/testify v1.7.0 12 | github.com/zmb3/spotify/v2 v2.0.0 13 | golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 14 | golang.org/x/tools v0.0.0-20201028111035-eafbe7b904eb // indirect 15 | google.golang.org/api v0.34.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /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.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.61.0/go.mod h1:XukKJg4Y7QsUu0Hxg3qQKUWR4VuWivmyMK2+rUyxAqw= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8= 17 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 18 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 19 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 20 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 21 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 22 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 23 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 24 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 25 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 26 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 27 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 28 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 29 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 30 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 31 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 32 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 33 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 34 | cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= 35 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 36 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 37 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 38 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 39 | github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 40 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 41 | github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 42 | github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= 43 | github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= 44 | github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= 45 | github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= 46 | github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= 47 | github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= 48 | github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= 49 | github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 50 | github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= 51 | github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 52 | github.com/andybalholm/crlf v0.0.0-20171020200849-670099aa064f/go.mod h1:k8feO4+kXDxro6ErPXBRTJ/ro2mf0SsFG8s7doP9kJE= 53 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 54 | github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= 55 | github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= 56 | github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I= 57 | github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= 58 | github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= 59 | github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 60 | github.com/apparentlymart/go-textseg/v12 v12.0.0 h1:bNEQyAGak9tojivJNkoqWErVCQbjdL7GzRt3F8NvfJ0= 61 | github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= 62 | github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= 63 | github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 64 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 65 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 66 | github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= 67 | github.com/aws/aws-sdk-go v1.25.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 68 | github.com/aws/aws-sdk-go v1.37.0 h1:GzFnhOIsrGyQ69s7VgqtrG2BG8v7X7vwB3Xpbd/DBBk= 69 | github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= 70 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= 71 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= 72 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 73 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 74 | github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= 75 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 76 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 77 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 78 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 79 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 80 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 81 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 82 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 83 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 84 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 85 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 86 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 87 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 88 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 89 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 90 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 91 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 92 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 93 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 94 | github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= 95 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 96 | github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 97 | github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= 98 | github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 99 | github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= 100 | github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= 101 | github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= 102 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 103 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 104 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 105 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 106 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 107 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 108 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 109 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 110 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= 111 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 112 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 113 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 114 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 115 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 116 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 117 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 118 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 119 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 120 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 121 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 122 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 123 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 124 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 125 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 126 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 127 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 128 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 129 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 130 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 131 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 132 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 133 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 134 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 135 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 136 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 137 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 138 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 139 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 140 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 141 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 142 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 143 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 144 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 145 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 146 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 147 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 148 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 149 | github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= 150 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 151 | github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs= 152 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 153 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 154 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 155 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 156 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 157 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 158 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 159 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 160 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 161 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 162 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 163 | github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= 164 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 165 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 166 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 167 | github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= 168 | github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= 169 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 170 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 171 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 172 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= 173 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= 174 | github.com/hashicorp/go-getter v1.5.3 h1:NF5+zOlQegim+w/EUhSLh6QhXHmZMEeHLQzllkQ3ROU= 175 | github.com/hashicorp/go-getter v1.5.3/go.mod h1:BrrV/1clo8cCYu6mxvboYg+KutTiFnXjMEgDD8+i7ZI= 176 | github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= 177 | github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= 178 | github.com/hashicorp/go-hclog v0.15.0 h1:qMuK0wxsoW4D0ddCCYwPSTm4KQv1X1ke3WmPWZ0Mvsk= 179 | github.com/hashicorp/go-hclog v0.15.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= 180 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 181 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 182 | github.com/hashicorp/go-plugin v1.3.0/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYtXdgmf1AVNs0= 183 | github.com/hashicorp/go-plugin v1.4.1 h1:6UltRQlLN9iZO513VveELp5xyaFxVD2+1OVylE+2E+w= 184 | github.com/hashicorp/go-plugin v1.4.1/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= 185 | github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= 186 | github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= 187 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 188 | github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= 189 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 190 | github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 191 | github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= 192 | github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 193 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 194 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 195 | github.com/hashicorp/hcl/v2 v2.3.0/go.mod h1:d+FwDBbOLvpAM3Z6J7gPj/VoAGkNe/gm352ZhjJ/Zv8= 196 | github.com/hashicorp/hcl/v2 v2.8.2 h1:wmFle3D1vu0okesm8BTLVDyJ6/OL9DCLUwn0b2OptiY= 197 | github.com/hashicorp/hcl/v2 v2.8.2/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY= 198 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= 199 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 200 | github.com/hashicorp/terraform-exec v0.15.0 h1:cqjh4d8HYNQrDoEmlSGelHmg2DYDh5yayckvJ5bV18E= 201 | github.com/hashicorp/terraform-exec v0.15.0/go.mod h1:H4IG8ZxanU+NW0ZpDRNsvh9f0ul7C0nHP+rUR/CHs7I= 202 | github.com/hashicorp/terraform-json v0.13.0 h1:Li9L+lKD1FO5RVFRM1mMMIBDoUHslOniyEi5CM+FWGY= 203 | github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9ExU76+cpdY8zHwoazk= 204 | github.com/hashicorp/terraform-plugin-go v0.4.0 h1:LFbXNeLDo0J/wR0kUzSPq0RpdmFh2gNedzU0n/gzPAo= 205 | github.com/hashicorp/terraform-plugin-go v0.4.0/go.mod h1:7u/6nt6vaiwcWE2GuJKbJwNlDFnf5n95xKw4hqIVr58= 206 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.9.0 h1:me5GUReyzlmNzDEuUzQCr2qDjNluKdvYj/W4LItUqKQ= 207 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.9.0/go.mod h1:JRe/T0PPn9kGowtePeYnTbDm6ViYNcnyxf1esw5yu90= 208 | github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 209 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= 210 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 211 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 212 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 213 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 214 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 215 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 216 | github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k= 217 | github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= 218 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 219 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 220 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 221 | github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= 222 | github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= 223 | github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 224 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 225 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 226 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 227 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 228 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 229 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 230 | github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= 231 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 232 | github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= 233 | github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 234 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 235 | github.com/klauspost/compress v1.11.2 h1:MiK62aErc3gIiVEtyzKfeOHgW7atJb5g/KNX5m3c2nQ= 236 | github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 237 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 238 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 239 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 240 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 241 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 242 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 243 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 244 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 245 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 246 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 247 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 248 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 249 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 250 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 251 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 252 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 253 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 254 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 255 | github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= 256 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 257 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 258 | github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= 259 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 260 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 261 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 262 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 263 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 264 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 265 | github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 266 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 267 | github.com/mitchellh/go-testing-interface v1.0.4 h1:ZU1VNC02qyufSZsjjs7+khruk2fKvbQ3TwRV/IBCeFA= 268 | github.com/mitchellh/go-testing-interface v1.0.4/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 269 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 270 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 271 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 272 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 273 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 274 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 275 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 276 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 277 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 278 | github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758= 279 | github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs= 280 | github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= 281 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 282 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 283 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 284 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 285 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 286 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 287 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 288 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 289 | github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= 290 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 291 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 292 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 293 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 294 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 295 | github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 296 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 297 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 298 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 299 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 300 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 301 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 302 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 303 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 304 | github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= 305 | github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 306 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 307 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= 308 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 309 | github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 310 | github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 311 | github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= 312 | github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= 313 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 314 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 315 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 316 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 317 | github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= 318 | github.com/zclconf/go-cty v1.9.1 h1:viqrgQwFl5UpSxc046qblj78wZXVDFnSOufaOTER+cc= 319 | github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= 320 | github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= 321 | github.com/zmb3/spotify/v2 v2.0.0 h1:NHW9btztNZTrJ0+3yMNyfY5qcu1ck9s36wwzc7zrCic= 322 | github.com/zmb3/spotify/v2 v2.0.0/go.mod h1:+LVh9CafHu7SedyqYmEf12Rd01dIVlEL845yNhksW0E= 323 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 324 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 325 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 326 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 327 | go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= 328 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 329 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 330 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 331 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 332 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 333 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 334 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 335 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 336 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 337 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 338 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= 339 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 340 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 341 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 342 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 343 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 344 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 345 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 346 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 347 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 348 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 349 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 350 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 351 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 352 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 353 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 354 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 355 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 356 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 357 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 358 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 359 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 360 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 361 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= 362 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 363 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 364 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 365 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 366 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 367 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 368 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 369 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 370 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 371 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 372 | golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 373 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 374 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 375 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 376 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 377 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 378 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 379 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 380 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 381 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 382 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 383 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 384 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 385 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 386 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 387 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 388 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 389 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 390 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 391 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 392 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 393 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 394 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 395 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 396 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 397 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 398 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 399 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 400 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 401 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 402 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 403 | golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= 404 | golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c= 405 | golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 406 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 407 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 408 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 409 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 410 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 411 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 412 | golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 h1:Ati8dO7+U7mxpkPSxBZQEvzHVUYB/MqCklCN8ig5w/o= 413 | golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 414 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 415 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 416 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 417 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 418 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 419 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 420 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 421 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 422 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 423 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 424 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 425 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 426 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 427 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 428 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 429 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 430 | golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 431 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 432 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 433 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 434 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 435 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 436 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 437 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 438 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 439 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 440 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 441 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 442 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 443 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 444 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 445 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 446 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 447 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 448 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 449 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 450 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 451 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 452 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 453 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 454 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 455 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 456 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 457 | golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 458 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 459 | golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 h1:RX8C8PRZc2hTIod4ds8ij+/4RQX3AqhYj3uOHmyaz4E= 460 | golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 461 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 462 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 463 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 464 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 465 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 466 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 467 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 468 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 469 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 470 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 471 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 472 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 473 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 474 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 475 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 476 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 477 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 478 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 479 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 480 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 481 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 482 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 483 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 484 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 485 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 486 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 487 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 488 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 489 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 490 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 491 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 492 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 493 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 494 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 495 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 496 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 497 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 498 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 499 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 500 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 501 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 502 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 503 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 504 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 505 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 506 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 507 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 508 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 509 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 510 | golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 511 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 512 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 513 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 514 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 515 | golang.org/x/tools v0.0.0-20201028111035-eafbe7b904eb h1:KVWk3RW1AZlxWum4tYqegLgwJHb5oouozcGM8HfNQaw= 516 | golang.org/x/tools v0.0.0-20201028111035-eafbe7b904eb/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 517 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 518 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 519 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 520 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 521 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 522 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 523 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 524 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 525 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 526 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 527 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 528 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 529 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 530 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 531 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 532 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 533 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 534 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 535 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 536 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 537 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 538 | google.golang.org/api v0.34.0 h1:k40adF3uR+6x/+hO5Dh4ZFUqFp67vxvbpafFiJxl10A= 539 | google.golang.org/api v0.34.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 540 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 541 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 542 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 543 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 544 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 545 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 546 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 547 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 548 | google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 549 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 550 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 551 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 552 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 553 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 554 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 555 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 556 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 557 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 558 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 559 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 560 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 561 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 562 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 563 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 564 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 565 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 566 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 567 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 568 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 569 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 570 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 571 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 572 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 573 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 574 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 575 | google.golang.org/genproto v0.0.0-20200711021454-869866162049/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 576 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 577 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 578 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 579 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M= 580 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 581 | google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 582 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 583 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 584 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 585 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 586 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 587 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 588 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 589 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 590 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 591 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 592 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 593 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 594 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 595 | google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= 596 | google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 597 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 598 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 599 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 600 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 601 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 602 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 603 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 604 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 605 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 606 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 607 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 608 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 609 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 610 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 611 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 612 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 613 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 614 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 615 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 616 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 617 | gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 618 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 619 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 620 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 621 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 622 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 623 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 624 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 625 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 626 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 627 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 628 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 629 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 630 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 631 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 632 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 633 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 634 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 635 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 636 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 637 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 638 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/conradludgate/terraform-provider-spotify/spotify" 5 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" 6 | ) 7 | 8 | //go:generate tfplugindocs 9 | 10 | func main() { 11 | plugin.Serve(&plugin.ServeOpts{ 12 | ProviderFunc: spotify.Provider, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /spotify/client.go: -------------------------------------------------------------------------------- 1 | package spotify 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "path" 12 | 13 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 14 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 15 | "github.com/zmb3/spotify/v2" 16 | "golang.org/x/oauth2" 17 | ) 18 | 19 | // ClientConfigurer for spotify API access 20 | func ClientConfigurer(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { 21 | server, err := url.Parse(d.Get("auth_server").(string)) 22 | if err != nil { 23 | return nil, diag.Errorf("auth_server was not a valid url: %s", err.Error()) 24 | } 25 | server.Path = path.Join(server.Path, "api/v1/token") 26 | server.Path = path.Join(server.Path, d.Get("token_id").(string)) 27 | 28 | transport := &transport{ 29 | Endpoint: server.String(), 30 | Username: d.Get("username").(string), 31 | APIKey: d.Get("api_key").(string), 32 | } 33 | 34 | if err := transport.getToken(ctx); err != nil { 35 | return nil, diag.FromErr(err) 36 | } 37 | 38 | httpClient := &http.Client{Transport: transport} 39 | client := spotify.New(httpClient, spotify.WithRetry(true)) 40 | 41 | return client, nil 42 | } 43 | 44 | type transport struct { 45 | Endpoint string 46 | Username string 47 | APIKey string 48 | Base http.RoundTripper 49 | token *oauth2.Token 50 | } 51 | 52 | func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { 53 | if !t.token.Valid() { 54 | if err := t.getToken(req.Context()); err != nil { 55 | return nil, err 56 | } 57 | } 58 | 59 | t.token.SetAuthHeader(req) 60 | 61 | return t.base().RoundTrip(req) 62 | } 63 | 64 | func (t *transport) base() http.RoundTripper { 65 | if t.Base != nil { 66 | return t.Base 67 | } 68 | return http.DefaultTransport 69 | } 70 | 71 | func (t *transport) getToken(ctx context.Context) error { 72 | req, err := http.NewRequestWithContext(ctx, "POST", t.Endpoint, nil) 73 | if err != nil { 74 | return err 75 | } 76 | req.SetBasicAuth(t.Username, t.APIKey) 77 | resp, err := t.base().RoundTrip(req) 78 | if err != nil { 79 | return err 80 | } 81 | defer resp.Body.Close() 82 | body, err := ioutil.ReadAll(resp.Body) 83 | if err != nil { 84 | return err 85 | } 86 | if resp.StatusCode != http.StatusOK { 87 | return fmt.Errorf("%s", string(body)) 88 | } 89 | 90 | t.token = &oauth2.Token{} 91 | if err := json.Unmarshal(body, t.token); err != nil { 92 | return err 93 | } 94 | 95 | if !t.token.Valid() { 96 | return errors.New("could not get a valid token") 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /spotify/data_source_album.go: -------------------------------------------------------------------------------- 1 | package spotify 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | "github.com/zmb3/spotify/v2" 12 | ) 13 | 14 | func dataSourceAlbum() *schema.Resource { 15 | return &schema.Resource{ 16 | ReadContext: dataSourceAlbumRead, 17 | 18 | Schema: map[string]*schema.Schema{ 19 | "spotify_id": { 20 | Type: schema.TypeString, 21 | Optional: true, 22 | AtLeastOneOf: []string{"spotify_id", "url"}, 23 | Description: "Spotify ID of the album", 24 | }, 25 | "url": { 26 | Type: schema.TypeString, 27 | Optional: true, 28 | AtLeastOneOf: []string{"spotify_id", "url"}, 29 | Description: "Spotify URL of the album", 30 | }, 31 | "name": { 32 | Type: schema.TypeString, 33 | Computed: true, 34 | Description: "The Name of the album", 35 | }, 36 | "artists": { 37 | Type: schema.TypeList, 38 | Computed: true, 39 | Elem: &schema.Schema{Type: schema.TypeString}, 40 | Description: "The spotify IDs of the artists", 41 | }, 42 | }, 43 | } 44 | } 45 | 46 | func dataSourceAlbumRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 47 | client := m.(*spotify.Client) 48 | 49 | var id spotify.ID 50 | if u, ok := d.GetOk("url"); ok { 51 | u, err := url.Parse(u.(string)) 52 | if err != nil { 53 | return diag.FromErr(err) 54 | } 55 | if !strings.HasPrefix(u.Path, "/album/") { 56 | return diag.FromErr(errors.New("URL did not point to a spotify album")) 57 | } 58 | id = spotify.ID(strings.TrimPrefix(u.Path, "/album/")) 59 | } else { 60 | id = spotify.ID(d.Get("spotify_id").(string)) 61 | } 62 | 63 | album, err := client.GetAlbum(ctx, id) 64 | if err != nil { 65 | return diag.FromErr(err) 66 | } 67 | 68 | if err := d.Set("name", album.Name); err != nil { 69 | return diag.FromErr(err) 70 | } 71 | 72 | artists := make([]interface{}, 0, len(album.Artists)) 73 | for _, artist := range album.Artists { 74 | artists = append(artists, string(artist.ID)) 75 | } 76 | if err := d.Set("artists", artists); err != nil { 77 | return diag.FromErr(err) 78 | } 79 | d.SetId(string(album.ID)) 80 | 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /spotify/data_source_album_test.go: -------------------------------------------------------------------------------- 1 | package spotify_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | "github.com/jarcoal/httpmock" 9 | spotifyApi "github.com/zmb3/spotify/v2" 10 | ) 11 | 12 | func TestSpotify_DataSource_Album(t *testing.T) { 13 | httpmock.Activate() 14 | defer httpmock.DeactivateAndReset() 15 | 16 | apiKey := "some-api-key" 17 | accessToken := "some-access-token" 18 | 19 | resource.Test(t, resource.TestCase{ 20 | IsUnitTest: true, 21 | Providers: testAccProviders, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: fmt.Sprintf(` 25 | provider "spotify" { 26 | api_key = "%s" 27 | } 28 | 29 | data "spotify_album" "album-1" { 30 | spotify_id = "35axN2yrxRiycF2pA8mZaB" 31 | } 32 | 33 | data "spotify_album" "album-2" { 34 | url = "https://open.spotify.com/album/3nRnJkUJYFfxcOGgU6LNci" 35 | } 36 | `, apiKey), 37 | Check: resource.ComposeTestCheckFunc( 38 | resource.TestCheckResourceAttr("data.spotify_album.album-1", "id", "35axN2yrxRiycF2pA8mZaB"), 39 | resource.TestCheckResourceAttr("data.spotify_album.album-1", "name", "Only in Dreams"), 40 | resource.TestCheckResourceAttr("data.spotify_album.album-1", "artists.#", "1"), 41 | resource.TestCheckResourceAttr("data.spotify_album.album-1", "artists.0", "7GvVTb8yFV0ZrdI30Qce6T"), 42 | 43 | resource.TestCheckResourceAttr("data.spotify_album.album-2", "id", "3nRnJkUJYFfxcOGgU6LNci"), 44 | resource.TestCheckResourceAttr("data.spotify_album.album-2", "name", "The Promised Land"), 45 | resource.TestCheckResourceAttr("data.spotify_album.album-2", "artists.#", "1"), 46 | resource.TestCheckResourceAttr("data.spotify_album.album-2", "artists.0", "4UNnRb4LN2hGtbtMfPzMhg"), 47 | ), 48 | PreConfig: func() { 49 | RegisterAuthResponse(apiKey, accessToken) 50 | 51 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/albums/35axN2yrxRiycF2pA8mZaB", 52 | RespondWith( 53 | JSON(spotifyApi.FullAlbum{ 54 | SimpleAlbum: spotifyApi.SimpleAlbum{ 55 | ID: "35axN2yrxRiycF2pA8mZaB", 56 | Name: "Only in Dreams", 57 | Artists: []spotifyApi.SimpleArtist{ 58 | { 59 | ID: "7GvVTb8yFV0ZrdI30Qce6T", 60 | }, 61 | }, 62 | }, 63 | }), 64 | VerifyBearer(accessToken), 65 | ), 66 | ) 67 | 68 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/albums/3nRnJkUJYFfxcOGgU6LNci", 69 | RespondWith( 70 | JSON(spotifyApi.FullAlbum{ 71 | SimpleAlbum: spotifyApi.SimpleAlbum{ 72 | ID: "3nRnJkUJYFfxcOGgU6LNci", 73 | Name: "The Promised Land", 74 | Artists: []spotifyApi.SimpleArtist{ 75 | { 76 | ID: "4UNnRb4LN2hGtbtMfPzMhg", 77 | }, 78 | }, 79 | }, 80 | }), 81 | VerifyBearer(accessToken), 82 | ), 83 | ) 84 | }, 85 | }, 86 | }, 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /spotify/data_source_search_track.go: -------------------------------------------------------------------------------- 1 | package spotify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 12 | "github.com/zmb3/spotify/v2" 13 | ) 14 | 15 | func dataSourceSearchTrack() *schema.Resource { 16 | trackResource := &schema.Resource{ 17 | Schema: map[string]*schema.Schema{ 18 | "id": { 19 | Type: schema.TypeString, 20 | Computed: true, 21 | Description: "ID of the track", 22 | }, 23 | "name": { 24 | Type: schema.TypeString, 25 | Computed: true, 26 | Description: "Name of the track", 27 | }, 28 | "artists": { 29 | Type: schema.TypeList, 30 | Computed: true, 31 | Elem: &schema.Schema{Type: schema.TypeString}, 32 | Description: "IDs of the artists", 33 | }, 34 | "album": { 35 | Type: schema.TypeString, 36 | Computed: true, 37 | Description: "ID of the album that the track appears on", 38 | }, 39 | }, 40 | } 41 | 42 | return &schema.Resource{ 43 | ReadContext: dataSourceSearchTrackRead, 44 | 45 | Schema: map[string]*schema.Schema{ 46 | "name": { 47 | Type: schema.TypeString, 48 | Optional: true, 49 | Description: "Name of the track", 50 | }, 51 | "artist": { 52 | Type: schema.TypeString, 53 | Optional: true, 54 | Description: "Name of the artist", 55 | }, 56 | "album": { 57 | Type: schema.TypeString, 58 | Optional: true, 59 | Description: "Name of the album", 60 | }, 61 | "year": { 62 | Type: schema.TypeString, 63 | Optional: true, 64 | Description: "Year of release", 65 | }, 66 | "limit": { 67 | Type: schema.TypeInt, 68 | Default: 10, 69 | Optional: true, 70 | }, 71 | "explicit": { 72 | Type: schema.TypeBool, 73 | Default: true, 74 | Optional: true, 75 | Description: "Filter to allow explicit tracks", 76 | }, 77 | "tracks": { 78 | Type: schema.TypeList, 79 | Computed: true, 80 | Elem: trackResource, 81 | Description: "List of tracks found", 82 | }, 83 | }, 84 | } 85 | } 86 | 87 | func addSearchTerm(queries []string, key, field string) []string { 88 | if field == "" { 89 | return queries 90 | } 91 | return append(queries, fmt.Sprintf("%s:%s", key, field)) 92 | } 93 | 94 | func dataSourceSearchTrackRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 95 | client := m.(*spotify.Client) 96 | 97 | var queries []string 98 | queries = addSearchTerm(queries, "track", d.Get("name").(string)) 99 | queries = addSearchTerm(queries, "artist", d.Get("artist").(string)) 100 | queries = addSearchTerm(queries, "album", d.Get("album").(string)) 101 | queries = addSearchTerm(queries, "year", d.Get("year").(string)) 102 | 103 | limit := d.Get("limit").(int) 104 | 105 | results, err := client.Search(ctx, strings.Join(queries, " "), spotify.SearchTypeTrack, spotify.Limit(limit)) 106 | 107 | if err != nil { 108 | return diag.Errorf("could not perform search [%v]: %s", queries, err.Error()) 109 | } 110 | 111 | var tracks []interface{} 112 | for _, track := range results.Tracks.Tracks { 113 | var artists []interface{} 114 | for _, artist := range track.Artists { 115 | artists = append(artists, artist.ID.String()) 116 | } 117 | 118 | trackData := map[string]interface{}{ 119 | "id": track.ID.String(), 120 | "name": track.Name, 121 | "artists": artists, 122 | "album": track.Album.ID.String(), 123 | } 124 | if track.Explicit && d.Get("explicit").(bool) { 125 | tracks = append(tracks, trackData) 126 | } else if !track.Explicit { 127 | tracks = append(tracks, trackData) 128 | } 129 | } 130 | 131 | if err := d.Set("tracks", tracks); err != nil { 132 | return diag.FromErr(err) 133 | } 134 | 135 | // Sets an id in the state 136 | d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) 137 | 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /spotify/data_source_search_track_test.go: -------------------------------------------------------------------------------- 1 | package spotify_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | "github.com/jarcoal/httpmock" 9 | spotifyApi "github.com/zmb3/spotify/v2" 10 | ) 11 | 12 | func TestSpotify_DataSource_SearchTrack(t *testing.T) { 13 | httpmock.Activate() 14 | defer httpmock.DeactivateAndReset() 15 | 16 | apiKey := "some-api-key" 17 | accessToken := "some-access-token" 18 | 19 | resource.Test(t, resource.TestCase{ 20 | IsUnitTest: true, 21 | Providers: testAccProviders, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: fmt.Sprintf(` 25 | provider "spotify" { 26 | api_key = "%s" 27 | } 28 | 29 | data "spotify_search_track" "delta_heavy" { 30 | artist = "Delta Heavy" 31 | explicit = false 32 | } 33 | `, apiKey), 34 | Check: resource.ComposeTestCheckFunc( 35 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.#", "3"), 36 | 37 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.0.id", "track-1"), 38 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.0.name", "White Flag"), 39 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.0.artists.#", "1"), 40 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.0.artists.0", "artist-1"), 41 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.0.album", "album-1"), 42 | 43 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.1.id", "track-2"), 44 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.1.name", "Kaleidoscope"), 45 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.1.artists.#", "1"), 46 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.1.artists.0", "artist-1"), 47 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.1.album", "album-2"), 48 | 49 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.2.id", "track-4"), 50 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.2.name", "Revenge"), 51 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.2.artists.#", "2"), 52 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.2.artists.0", "artist-1"), 53 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.2.artists.1", "artist-3"), 54 | resource.TestCheckResourceAttr("data.spotify_search_track.delta_heavy", "tracks.2.album", "album-4"), 55 | ), 56 | PreConfig: func() { 57 | RegisterAuthResponse(apiKey, accessToken) 58 | 59 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/search?limit=10&q=artist%3ADelta+Heavy&type=track", 60 | RespondWith( 61 | JSON(spotifyApi.SearchResult{ 62 | Tracks: fullTrackPage([]track{ 63 | { 64 | id: "track-1", 65 | name: "White Flag", 66 | artists: []string{"artist-1"}, 67 | album: "album-1", 68 | }, 69 | { 70 | id: "track-2", 71 | name: "Kaleidoscope", 72 | artists: []string{"artist-1"}, 73 | album: "album-2", 74 | }, 75 | { 76 | id: "track-3", 77 | name: "Anarchy", 78 | artists: []string{"artist-1", "artist-2"}, 79 | album: "album-3", 80 | explicit: true, 81 | }, 82 | { 83 | id: "track-4", 84 | name: "Revenge", 85 | artists: []string{"artist-1", "artist-3"}, 86 | album: "album-4", 87 | }, 88 | }), 89 | }), 90 | VerifyBearer(accessToken), 91 | ), 92 | ) 93 | }, 94 | }, 95 | }, 96 | }) 97 | } 98 | 99 | type track struct { 100 | id string 101 | name string 102 | artists []string 103 | album string 104 | explicit bool 105 | } 106 | 107 | func fullTrackPage(tracks []track) *spotifyApi.FullTrackPage { 108 | fullTracks := make([]spotifyApi.FullTrack, len(tracks)) 109 | for i, track := range tracks { 110 | fullTracks[i].SimpleTrack.ID = spotifyApi.ID(track.id) 111 | fullTracks[i].SimpleTrack.Name = track.name 112 | fullTracks[i].Album.ID = spotifyApi.ID(track.album) 113 | fullTracks[i].Explicit = track.explicit 114 | for _, artist := range track.artists { 115 | fullTracks[i].Artists = append(fullTracks[i].Artists, spotifyApi.SimpleArtist{ 116 | ID: spotifyApi.ID(artist), 117 | }) 118 | } 119 | } 120 | return &spotifyApi.FullTrackPage{Tracks: fullTracks} 121 | } 122 | -------------------------------------------------------------------------------- /spotify/data_source_track.go: -------------------------------------------------------------------------------- 1 | package spotify 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | "github.com/zmb3/spotify/v2" 12 | ) 13 | 14 | func dataSourceTrack() *schema.Resource { 15 | return &schema.Resource{ 16 | ReadContext: dataSourceTrackRead, 17 | 18 | Schema: map[string]*schema.Schema{ 19 | "spotify_id": { 20 | Type: schema.TypeString, 21 | Optional: true, 22 | AtLeastOneOf: []string{"spotify_id", "url"}, 23 | Description: "Spotify ID of the track", 24 | }, 25 | "url": { 26 | Type: schema.TypeString, 27 | Optional: true, 28 | AtLeastOneOf: []string{"spotify_id", "url"}, 29 | Description: "Spotify URL of the track", 30 | }, 31 | "name": { 32 | Type: schema.TypeString, 33 | Computed: true, 34 | Description: "The Name of the track", 35 | }, 36 | "artists": { 37 | Type: schema.TypeList, 38 | Computed: true, 39 | Elem: &schema.Schema{Type: schema.TypeString}, 40 | Description: "The spotify IDs of the artists", 41 | }, 42 | "album": { 43 | Type: schema.TypeString, 44 | Computed: true, 45 | Description: "The spotify ID of the album", 46 | }, 47 | }, 48 | } 49 | } 50 | 51 | func dataSourceTrackRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 52 | client := m.(*spotify.Client) 53 | 54 | var id spotify.ID 55 | if u, ok := d.GetOk("url"); ok { 56 | u, err := url.Parse(u.(string)) 57 | if err != nil { 58 | return diag.FromErr(err) 59 | } 60 | if !strings.HasPrefix(u.Path, "/track/") { 61 | return diag.FromErr(errors.New("URL did not point to a spotify track")) 62 | } 63 | id = spotify.ID(strings.TrimPrefix(u.Path, "/track/")) 64 | } else { 65 | id = spotify.ID(d.Get("spotify_id").(string)) 66 | } 67 | 68 | track, err := client.GetTrack(ctx, id) 69 | if err != nil { 70 | return diag.FromErr(err) 71 | } 72 | 73 | if err := d.Set("name", track.Name); err != nil { 74 | return diag.FromErr(err) 75 | } 76 | if err := d.Set("album", string(track.Album.ID)); err != nil { 77 | return diag.FromErr(err) 78 | } 79 | 80 | artists := make([]interface{}, 0, len(track.Artists)) 81 | for _, artist := range track.Artists { 82 | artists = append(artists, string(artist.ID)) 83 | } 84 | if err := d.Set("artists", artists); err != nil { 85 | return diag.FromErr(err) 86 | } 87 | d.SetId(string(track.ID)) 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /spotify/data_source_track_test.go: -------------------------------------------------------------------------------- 1 | package spotify_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | "github.com/jarcoal/httpmock" 9 | spotifyApi "github.com/zmb3/spotify/v2" 10 | ) 11 | 12 | func TestSpotify_DataSource_Track(t *testing.T) { 13 | httpmock.Activate() 14 | defer httpmock.DeactivateAndReset() 15 | 16 | apiKey := "some-api-key" 17 | accessToken := "some-access-token" 18 | 19 | resource.Test(t, resource.TestCase{ 20 | IsUnitTest: true, 21 | Providers: testAccProviders, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: fmt.Sprintf(` 25 | provider "spotify" { 26 | api_key = "%s" 27 | } 28 | 29 | data "spotify_track" "track-1" { 30 | spotify_id = "4lE6N1E0L8CssgKEUCgdbA" 31 | } 32 | 33 | data "spotify_track" "track-2" { 34 | url = "https://open.spotify.com/track/4XdaaDFE881SlIaz31pTAG" 35 | } 36 | `, apiKey), 37 | Check: resource.ComposeTestCheckFunc( 38 | resource.TestCheckResourceAttr("data.spotify_track.track-1", "id", "4lE6N1E0L8CssgKEUCgdbA"), 39 | resource.TestCheckResourceAttr("data.spotify_track.track-1", "name", "Blackwater"), 40 | resource.TestCheckResourceAttr("data.spotify_track.track-1", "album", "1AUS845POFhV3oDytPImEZ"), 41 | resource.TestCheckResourceAttr("data.spotify_track.track-1", "artists.#", "1"), 42 | resource.TestCheckResourceAttr("data.spotify_track.track-1", "artists.0", "0qPGd8tOMHlFZt8EA1uLFY"), 43 | 44 | resource.TestCheckResourceAttr("data.spotify_track.track-2", "id", "4XdaaDFE881SlIaz31pTAG"), 45 | resource.TestCheckResourceAttr("data.spotify_track.track-2", "name", "Overkill"), 46 | resource.TestCheckResourceAttr("data.spotify_track.track-2", "album", "64ey3KHg3uepidKmJrb4ka"), 47 | resource.TestCheckResourceAttr("data.spotify_track.track-2", "artists.#", "1"), 48 | resource.TestCheckResourceAttr("data.spotify_track.track-2", "artists.0", "0qPGd8tOMHlFZt8EA1uLFY"), 49 | ), 50 | PreConfig: func() { 51 | RegisterAuthResponse(apiKey, accessToken) 52 | 53 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/tracks/4lE6N1E0L8CssgKEUCgdbA", 54 | RespondWith( 55 | JSON(spotifyApi.FullTrack{ 56 | SimpleTrack: spotifyApi.SimpleTrack{ 57 | ID: "4lE6N1E0L8CssgKEUCgdbA", 58 | Name: "Blackwater", 59 | Artists: []spotifyApi.SimpleArtist{ 60 | { 61 | ID: "0qPGd8tOMHlFZt8EA1uLFY", 62 | }, 63 | }, 64 | }, 65 | Album: spotifyApi.SimpleAlbum{ 66 | ID: "1AUS845POFhV3oDytPImEZ", 67 | }, 68 | }), 69 | VerifyBearer(accessToken), 70 | ), 71 | ) 72 | 73 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/tracks/4XdaaDFE881SlIaz31pTAG", 74 | RespondWith( 75 | JSON(spotifyApi.FullTrack{ 76 | SimpleTrack: spotifyApi.SimpleTrack{ 77 | ID: "4XdaaDFE881SlIaz31pTAG", 78 | Name: "Overkill", 79 | Artists: []spotifyApi.SimpleArtist{ 80 | { 81 | ID: "0qPGd8tOMHlFZt8EA1uLFY", 82 | }, 83 | }, 84 | }, 85 | Album: spotifyApi.SimpleAlbum{ 86 | ID: "64ey3KHg3uepidKmJrb4ka", 87 | }, 88 | }), 89 | VerifyBearer(accessToken), 90 | ), 91 | ) 92 | }, 93 | }, 94 | }, 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /spotify/provider.go: -------------------------------------------------------------------------------- 1 | package spotify 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 5 | ) 6 | 7 | // Provider for spotify 8 | func Provider() *schema.Provider { 9 | return &schema.Provider{ 10 | Schema: map[string]*schema.Schema{ 11 | "auth_server": { 12 | Type: schema.TypeString, 13 | Optional: true, 14 | Default: "http://localhost:27228", 15 | Description: "Oauth2 Proxy URL", 16 | }, 17 | "token_id": { 18 | Type: schema.TypeString, 19 | Optional: true, 20 | Default: "terraform", 21 | Description: "Oauth2 Proxy token ID", 22 | }, 23 | "username": { 24 | Type: schema.TypeString, 25 | Optional: true, 26 | Default: "SpotifyAuthProxy", 27 | Description: "Oauth2 Proxy username", 28 | }, 29 | "api_key": { 30 | Type: schema.TypeString, 31 | Required: true, 32 | Description: "Oauth2 Proxy API Key", 33 | }, 34 | }, 35 | ResourcesMap: map[string]*schema.Resource{ 36 | "spotify_playlist": resourcePlaylist(), 37 | "spotify_library": resourceLibraryTracks(), 38 | "spotify_library_albums": resourceLibraryAlbums(), 39 | }, 40 | DataSourcesMap: map[string]*schema.Resource{ 41 | "spotify_search_track": dataSourceSearchTrack(), 42 | "spotify_track": dataSourceTrack(), 43 | "spotify_album": dataSourceAlbum(), 44 | }, 45 | ConfigureContextFunc: ClientConfigurer, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /spotify/provider_test.go: -------------------------------------------------------------------------------- 1 | package spotify_test 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/conradludgate/terraform-provider-spotify/spotify" 12 | "github.com/go-test/deep" 13 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 14 | "github.com/jarcoal/httpmock" 15 | "golang.org/x/oauth2" 16 | ) 17 | 18 | var testAccProviders map[string]*schema.Provider 19 | var testAccProvider *schema.Provider 20 | 21 | func init() { 22 | testAccProvider = spotify.Provider() 23 | testAccProviders = map[string]*schema.Provider{ 24 | "spotify": testAccProvider, 25 | } 26 | } 27 | 28 | func RegisterAuthResponse(apiKey, accessToken string) { 29 | httpmock.RegisterResponder("POST", "http://localhost:27228/api/v1/token/terraform", 30 | RespondWith( 31 | JSON(oauth2.Token{AccessToken: accessToken, Expiry: time.Now().Add(time.Hour)}), 32 | VerifyBasicAuth("SpotifyAuthProxy", apiKey), 33 | ), 34 | ) 35 | } 36 | 37 | type Verifier func(req *http.Request) error 38 | 39 | func VerifyBearer(accessToken string) Verifier { 40 | return func(req *http.Request) error { 41 | if req.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", accessToken) { 42 | return errors.New("invalid access token") 43 | } 44 | return nil 45 | } 46 | } 47 | 48 | func VerifyBasicAuth(username, password string) Verifier { 49 | return func(req *http.Request) error { 50 | user, pass, ok := req.BasicAuth() 51 | if !ok { 52 | return errors.New("missing auth") 53 | } 54 | if user != username || pass != password { 55 | return errors.New("invalid auth") 56 | } 57 | return nil 58 | } 59 | } 60 | 61 | type object map[string]interface{} 62 | type array []interface{} 63 | 64 | func VerifyJSONBody(expected interface{}) Verifier { 65 | return func(req *http.Request) error { 66 | if req.Body == nil { 67 | return errors.New("no body") 68 | } 69 | 70 | if req.Header.Get("content-type") != "application/json" { 71 | return errors.New("no json body") 72 | } 73 | 74 | reqBody := make(map[string]interface{}) 75 | if err := json.NewDecoder(req.Body).Decode(&reqBody); err != nil { 76 | return errors.New("could not read body") 77 | } 78 | 79 | expJson, err := json.Marshal(expected) 80 | if err != nil { 81 | return errors.New("could not encode expected json") 82 | } 83 | 84 | expBody := make(map[string]interface{}) 85 | if err := json.Unmarshal(expJson, &expBody); err != nil { 86 | return errors.New("could not decode expected json") 87 | } 88 | 89 | if diff := deep.Equal(reqBody, expBody); diff != nil { 90 | return fmt.Errorf("unexpected request:\n\t%s", strings.Join(diff, "\n\t")) 91 | } 92 | 93 | return nil 94 | } 95 | } 96 | 97 | func RespondWith(responder httpmock.Responder, verifiers ...Verifier) httpmock.Responder { 98 | return func(req *http.Request) (*http.Response, error) { 99 | for _, verifier := range verifiers { 100 | if err := verifier(req); err != nil { 101 | return httpmock.NewStringResponse(http.StatusInternalServerError, err.Error()), nil 102 | } 103 | } 104 | 105 | return responder(req) 106 | } 107 | } 108 | 109 | func JSON(response interface{}) httpmock.Responder { 110 | return func(req *http.Request) (*http.Response, error) { 111 | return httpmock.NewJsonResponse(http.StatusOK, response) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /spotify/resource_library_albums.go: -------------------------------------------------------------------------------- 1 | package spotify 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/zmb3/spotify/v2" 9 | ) 10 | 11 | func resourceLibraryAlbums() *schema.Resource { 12 | return &schema.Resource{ 13 | CreateContext: resourceLibraryAlbumsCreate, 14 | ReadContext: resourceLibraryAlbumsRead, 15 | UpdateContext: resourceLibraryAlbumsUpdate, 16 | DeleteContext: resourceLibraryAlbumsDelete, 17 | Importer: &schema.ResourceImporter{ 18 | StateContext: schema.ImportStatePassthroughContext, 19 | }, 20 | 21 | Schema: map[string]*schema.Schema{ 22 | "albums": { 23 | Type: schema.TypeSet, 24 | Required: true, 25 | Elem: &schema.Schema{Type: schema.TypeString}, 26 | Set: schema.HashString, 27 | Description: "The list of track IDs to save to your 'liked albums'. *Note, if used incorrectly you may unlike all of your albums - use with caution*", 28 | }, 29 | }, 30 | } 31 | } 32 | 33 | func resourceLibraryAlbumsCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 34 | client := m.(*spotify.Client) 35 | 36 | trackIDs := spotifyIdsInterface(d.Get("albums").(*schema.Set).List()) 37 | 38 | for _, rng := range batches(len(trackIDs), 100) { 39 | if err := client.AddAlbumsToLibrary(ctx, trackIDs[rng.Start:rng.End]...); err != nil { 40 | return diag.Errorf("AddAlbumsToLibrary: %s", err.Error()) 41 | } 42 | } 43 | 44 | d.SetId("library") 45 | 46 | return nil 47 | } 48 | 49 | func resourceLibraryAlbumsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 50 | client := m.(*spotify.Client) 51 | 52 | trackIDs := schema.NewSet(schema.HashString, nil) 53 | 54 | Albums, err := client.CurrentUsersAlbums(ctx) 55 | if err != nil { 56 | return diag.Errorf("CurrentUsersAlbums: %s", err.Error()) 57 | } 58 | for err == nil { 59 | for _, track := range Albums.Albums { 60 | trackIDs.Add(string(track.ID)) 61 | } 62 | err = client.NextPage(ctx, Albums) 63 | } 64 | 65 | if err := d.Set("albums", trackIDs); err != nil { 66 | return diag.FromErr(err) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func resourceLibraryAlbumsUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 73 | client := m.(*spotify.Client) 74 | 75 | if d.HasChange("albums") { 76 | old, new := d.GetChange("albums") 77 | oldSet := old.(*schema.Set) 78 | newSet := new.(*schema.Set) 79 | add := newSet.Difference(oldSet).List() 80 | sub := oldSet.Difference(newSet).List() 81 | 82 | addTrackIDs := spotifyIdsInterface(add) 83 | subTrackIDs := spotifyIdsInterface(sub) 84 | 85 | for _, rng := range batches(len(add), 100) { 86 | if err := client.AddAlbumsToLibrary(ctx, addTrackIDs[rng.Start:rng.End]...); err != nil { 87 | return diag.Errorf("AddAlbumsToLibrary: %s", err.Error()) 88 | } 89 | } 90 | for _, rng := range batches(len(sub), 100) { 91 | if err := client.RemoveAlbumsFromLibrary(ctx, subTrackIDs[rng.Start:rng.End]...); err != nil { 92 | return diag.Errorf("AddAlbumsToLibrary: %s", err.Error()) 93 | } 94 | } 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func resourceLibraryAlbumsDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /spotify/resource_library_albums_test.go: -------------------------------------------------------------------------------- 1 | package spotify_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | "github.com/jarcoal/httpmock" 9 | spotifyApi "github.com/zmb3/spotify/v2" 10 | ) 11 | 12 | func TestSpotify_Resource_LibraryAlbums(t *testing.T) { 13 | httpmock.Activate() 14 | defer httpmock.DeactivateAndReset() 15 | 16 | apiKey := "some-api-key" 17 | accessToken := "some-access-token" 18 | 19 | resource.Test(t, resource.TestCase{ 20 | IsUnitTest: true, 21 | Providers: testAccProviders, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: fmt.Sprintf(` 25 | provider "spotify" { 26 | api_key = "%s" 27 | } 28 | 29 | resource "spotify_library_albums" "my_albums" { 30 | albums = ["album-1", "album-2"] 31 | } 32 | `, apiKey), 33 | Check: resource.ComposeTestCheckFunc( 34 | resource.TestCheckResourceAttr("spotify_library_albums.my_albums", "id", "library"), 35 | resource.TestCheckResourceAttr("spotify_library_albums.my_albums", "albums.#", "2"), 36 | resource.TestCheckResourceAttr("spotify_library_albums.my_albums", "albums.0", "album-1"), 37 | resource.TestCheckResourceAttr("spotify_library_albums.my_albums", "albums.1", "album-2"), 38 | ), 39 | PreConfig: func() { 40 | RegisterAuthResponse(apiKey, accessToken) 41 | 42 | httpmock.RegisterResponder("PUT", "https://api.spotify.com/v1/me/albums?ids=album-1,album-2", 43 | RespondWith( 44 | JSON(nil), 45 | VerifyBearer(accessToken), 46 | ).Once(), 47 | ) 48 | 49 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/me/albums", 50 | RespondWith( 51 | JSON(savedAlbumPage("album-1", "album-2")), 52 | VerifyBearer(accessToken), 53 | ), 54 | ) 55 | }, 56 | }, 57 | { 58 | Config: fmt.Sprintf(` 59 | provider "spotify" { 60 | api_key = "%s" 61 | } 62 | 63 | resource "spotify_library_albums" "my_albums" { 64 | albums = ["album-1", "album-3"] 65 | } 66 | `, apiKey), 67 | Check: resource.ComposeTestCheckFunc( 68 | resource.TestCheckResourceAttr("spotify_library_albums.my_albums", "id", "library"), 69 | resource.TestCheckResourceAttr("spotify_library_albums.my_albums", "albums.#", "2"), 70 | resource.TestCheckResourceAttr("spotify_library_albums.my_albums", "albums.0", "album-1"), 71 | resource.TestCheckResourceAttr("spotify_library_albums.my_albums", "albums.1", "album-3"), 72 | ), 73 | PreConfig: func() { 74 | httpmock.RegisterResponder("PUT", "https://api.spotify.com/v1/me/albums?ids=album-3", 75 | RespondWith( 76 | JSON(nil), 77 | VerifyBearer(accessToken), 78 | ).Once(), 79 | ) 80 | 81 | httpmock.RegisterResponder("DELETE", "https://api.spotify.com/v1/me/albums?ids=album-2", 82 | RespondWith( 83 | JSON(nil), 84 | VerifyBearer(accessToken), 85 | ).Once(), 86 | ) 87 | 88 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/me/albums", 89 | RespondWith( 90 | JSON(savedAlbumPage("album-1", "album-3")), 91 | VerifyBearer(accessToken), 92 | ), 93 | ) 94 | }, 95 | }, 96 | }, 97 | }) 98 | } 99 | 100 | func savedAlbumPage(albums ...string) spotifyApi.SavedAlbumPage { 101 | savedAlbums := make([]spotifyApi.SavedAlbum, len(albums)) 102 | for i, album := range albums { 103 | savedAlbums[i].ID = spotifyApi.ID(album) 104 | } 105 | return spotifyApi.SavedAlbumPage{Albums: savedAlbums} 106 | } 107 | -------------------------------------------------------------------------------- /spotify/resource_library_tracks.go: -------------------------------------------------------------------------------- 1 | package spotify 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/zmb3/spotify/v2" 9 | ) 10 | 11 | func resourceLibraryTracks() *schema.Resource { 12 | return &schema.Resource{ 13 | CreateContext: resourceLibraryTracksCreate, 14 | ReadContext: resourceLibraryTracksRead, 15 | UpdateContext: resourceLibraryTracksUpdate, 16 | DeleteContext: resourceLibraryTracksDelete, 17 | Importer: &schema.ResourceImporter{ 18 | StateContext: schema.ImportStatePassthroughContext, 19 | }, 20 | 21 | Schema: map[string]*schema.Schema{ 22 | "tracks": { 23 | Type: schema.TypeSet, 24 | Required: true, 25 | Elem: &schema.Schema{Type: schema.TypeString}, 26 | Set: schema.HashString, 27 | Description: "The list of track IDs to save to your 'liked tracks'. *Note, if used incorrectly you may unlike all of your tracks - use with caution*", 28 | }, 29 | }, 30 | } 31 | } 32 | 33 | func resourceLibraryTracksCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 34 | client := m.(*spotify.Client) 35 | 36 | trackIDs := spotifyIdsInterface(d.Get("tracks").(*schema.Set).List()) 37 | 38 | for _, rng := range batches(len(trackIDs), 100) { 39 | if err := client.AddTracksToLibrary(ctx, trackIDs[rng.Start:rng.End]...); err != nil { 40 | return diag.Errorf("AddTracksToLibrary: %s", err.Error()) 41 | } 42 | } 43 | 44 | d.SetId("library") 45 | 46 | return nil 47 | } 48 | 49 | func resourceLibraryTracksRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 50 | client := m.(*spotify.Client) 51 | 52 | trackIDs := schema.NewSet(schema.HashString, nil) 53 | 54 | tracks, err := client.CurrentUsersTracks(ctx) 55 | if err != nil { 56 | return diag.Errorf("CurrentUsersTracks: %s", err.Error()) 57 | } 58 | for err == nil { 59 | for _, track := range tracks.Tracks { 60 | trackIDs.Add(string(track.ID)) 61 | } 62 | err = client.NextPage(ctx, tracks) 63 | } 64 | 65 | if err := d.Set("tracks", trackIDs); err != nil { 66 | return diag.FromErr(err) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func resourceLibraryTracksUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 73 | client := m.(*spotify.Client) 74 | 75 | if d.HasChange("tracks") { 76 | old, new := d.GetChange("tracks") 77 | oldSet := old.(*schema.Set) 78 | newSet := new.(*schema.Set) 79 | add := newSet.Difference(oldSet).List() 80 | sub := oldSet.Difference(newSet).List() 81 | 82 | addTrackIDs := spotifyIdsInterface(add) 83 | subTrackIDs := spotifyIdsInterface(sub) 84 | 85 | for _, rng := range batches(len(add), 100) { 86 | if err := client.AddTracksToLibrary(ctx, addTrackIDs[rng.Start:rng.End]...); err != nil { 87 | return diag.Errorf("AddTracksToLibrary: %s", err.Error()) 88 | } 89 | } 90 | for _, rng := range batches(len(sub), 100) { 91 | if err := client.RemoveTracksFromLibrary(ctx, subTrackIDs[rng.Start:rng.End]...); err != nil { 92 | return diag.Errorf("AddTracksToLibrary: %s", err.Error()) 93 | } 94 | } 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func resourceLibraryTracksDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /spotify/resource_library_tracks_test.go: -------------------------------------------------------------------------------- 1 | package spotify_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | "github.com/jarcoal/httpmock" 9 | spotifyApi "github.com/zmb3/spotify/v2" 10 | ) 11 | 12 | func TestSpotify_Resource_Library(t *testing.T) { 13 | httpmock.Activate() 14 | defer httpmock.DeactivateAndReset() 15 | 16 | apiKey := "some-api-key" 17 | accessToken := "some-access-token" 18 | 19 | resource.Test(t, resource.TestCase{ 20 | IsUnitTest: true, 21 | Providers: testAccProviders, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: fmt.Sprintf(` 25 | provider "spotify" { 26 | api_key = "%s" 27 | } 28 | 29 | resource "spotify_library" "my_library" { 30 | tracks = ["track-1", "track-2"] 31 | } 32 | `, apiKey), 33 | Check: resource.ComposeTestCheckFunc( 34 | resource.TestCheckResourceAttr("spotify_library.my_library", "id", "library"), 35 | resource.TestCheckResourceAttr("spotify_library.my_library", "tracks.#", "2"), 36 | resource.TestCheckResourceAttr("spotify_library.my_library", "tracks.0", "track-1"), 37 | resource.TestCheckResourceAttr("spotify_library.my_library", "tracks.1", "track-2"), 38 | ), 39 | PreConfig: func() { 40 | RegisterAuthResponse(apiKey, accessToken) 41 | 42 | httpmock.RegisterResponder("PUT", "https://api.spotify.com/v1/me/tracks?ids=track-1,track-2", 43 | RespondWith( 44 | JSON(nil), 45 | VerifyBearer(accessToken), 46 | ).Once(), 47 | ) 48 | 49 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/me/tracks", 50 | RespondWith( 51 | JSON(savedTrackPage("track-1", "track-2")), 52 | VerifyBearer(accessToken), 53 | ), 54 | ) 55 | }, 56 | }, 57 | { 58 | Config: fmt.Sprintf(` 59 | provider "spotify" { 60 | api_key = "%s" 61 | } 62 | 63 | resource "spotify_library" "my_library" { 64 | tracks = ["track-1", "track-3"] 65 | } 66 | `, apiKey), 67 | Check: resource.ComposeTestCheckFunc( 68 | resource.TestCheckResourceAttr("spotify_library.my_library", "id", "library"), 69 | resource.TestCheckResourceAttr("spotify_library.my_library", "tracks.#", "2"), 70 | resource.TestCheckResourceAttr("spotify_library.my_library", "tracks.0", "track-1"), 71 | resource.TestCheckResourceAttr("spotify_library.my_library", "tracks.1", "track-3"), 72 | ), 73 | PreConfig: func() { 74 | httpmock.RegisterResponder("PUT", "https://api.spotify.com/v1/me/tracks?ids=track-3", 75 | RespondWith( 76 | JSON(nil), 77 | VerifyBearer(accessToken), 78 | ).Once(), 79 | ) 80 | 81 | httpmock.RegisterResponder("DELETE", "https://api.spotify.com/v1/me/tracks?ids=track-2", 82 | RespondWith( 83 | JSON(nil), 84 | VerifyBearer(accessToken), 85 | ).Once(), 86 | ) 87 | 88 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/me/tracks", 89 | RespondWith( 90 | JSON(savedTrackPage("track-1", "track-3")), 91 | VerifyBearer(accessToken), 92 | ), 93 | ) 94 | }, 95 | }, 96 | }, 97 | }) 98 | } 99 | 100 | func savedTrackPage(tracks ...string) spotifyApi.SavedTrackPage { 101 | savedTracks := make([]spotifyApi.SavedTrack, len(tracks)) 102 | for i, track := range tracks { 103 | savedTracks[i].FullTrack.ID = spotifyApi.ID(track) 104 | } 105 | return spotifyApi.SavedTrackPage{Tracks: savedTracks} 106 | } 107 | -------------------------------------------------------------------------------- /spotify/resource_playlist.go: -------------------------------------------------------------------------------- 1 | package spotify 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/zmb3/spotify/v2" 9 | ) 10 | 11 | func resourcePlaylist() *schema.Resource { 12 | return &schema.Resource{ 13 | CreateContext: resourcePlaylistCreate, 14 | ReadContext: resourcePlaylistRead, 15 | UpdateContext: resourcePlaylistUpdate, 16 | DeleteContext: resourcePlaylistDelete, 17 | Importer: &schema.ResourceImporter{ 18 | StateContext: schema.ImportStatePassthroughContext, 19 | }, 20 | 21 | Description: "Resource to manage a spotify playlist.", 22 | 23 | Schema: map[string]*schema.Schema{ 24 | "name": { 25 | Type: schema.TypeString, 26 | Required: true, 27 | Description: "The name of the resulting playlist", 28 | }, 29 | "description": { 30 | Type: schema.TypeString, 31 | Optional: true, 32 | Description: "The description of the resulting playlist", 33 | }, 34 | "public": { 35 | Type: schema.TypeBool, 36 | Optional: true, 37 | Default: true, 38 | Description: "Whether the playlist can be accessed publically", 39 | }, 40 | "tracks": { 41 | Type: schema.TypeList, 42 | Required: true, 43 | Elem: &schema.Schema{Type: schema.TypeString}, 44 | Description: "A set of tracks for the playlist to contain", 45 | }, 46 | "snapshot_id": { 47 | Type: schema.TypeString, 48 | Computed: true, 49 | }, 50 | }, 51 | } 52 | } 53 | 54 | func resourcePlaylistCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 55 | client := m.(*spotify.Client) 56 | 57 | user, err := client.CurrentUser(ctx) 58 | if err != nil { 59 | return diag.Errorf("GetCurrentUser: %s", err.Error()) 60 | } 61 | 62 | userID := string(user.ID) 63 | name := d.Get("name").(string) 64 | description := d.Get("description").(string) 65 | public := d.Get("public").(bool) 66 | 67 | playlist, err := client.CreatePlaylistForUser(ctx, userID, name, description, public, false) 68 | if err != nil { 69 | return diag.Errorf("CreatePlaylist: %s", err.Error()) 70 | } 71 | 72 | d.SetId(string(playlist.ID)) 73 | 74 | trackIDs := spotifyIdsInterface(d.Get("tracks").([]interface{})) 75 | 76 | snapshotID := playlist.SnapshotID 77 | for _, rng := range batches(len(trackIDs), 100) { 78 | var err error 79 | snapshotID, err = client.AddTracksToPlaylist(ctx, playlist.ID, trackIDs[rng.Start:rng.End]...) 80 | if err != nil { 81 | return diag.Errorf("AddTracksToPlaylist: %s", err.Error()) 82 | } 83 | } 84 | 85 | if err := d.Set("snapshot_id", snapshotID); err != nil { 86 | return diag.FromErr(err) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func resourcePlaylistRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 93 | client := m.(*spotify.Client) 94 | 95 | playlistID := spotify.ID(d.Id()) 96 | playlist, err := client.GetPlaylist(ctx, playlistID) 97 | 98 | if err != nil { 99 | return diag.Errorf("GetPlaylist: %s", err.Error()) 100 | } 101 | 102 | if err := d.Set("name", playlist.Name); err != nil { 103 | return diag.FromErr(err) 104 | } 105 | if err := d.Set("description", playlist.Description); err != nil { 106 | return diag.FromErr(err) 107 | } 108 | if err := d.Set("public", playlist.IsPublic); err != nil { 109 | return diag.FromErr(err) 110 | } 111 | if err := d.Set("snapshot_id", playlist.SnapshotID); err != nil { 112 | return diag.FromErr(err) 113 | } 114 | 115 | tracks, err := client.GetPlaylistTracks(ctx, playlistID) 116 | if err != nil { 117 | return diag.Errorf("GetPlaylistTracks: %s", err.Error()) 118 | } 119 | 120 | trackIDs := make([]string, 0, tracks.Total) 121 | for err == nil { 122 | for _, track := range tracks.Tracks { 123 | trackIDs = append(trackIDs, string(track.Track.ID)) 124 | } 125 | err = client.NextPage(ctx, tracks) 126 | } 127 | 128 | if err := d.Set("tracks", trackIDs); err != nil { 129 | return diag.FromErr(err) 130 | } 131 | 132 | return nil 133 | } 134 | 135 | func resourcePlaylistUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 136 | client := m.(*spotify.Client) 137 | 138 | id := spotify.ID(d.Id()) 139 | if d.HasChanges("name", "description", "public") { 140 | err := client.ChangePlaylistNameAccessAndDescription( 141 | ctx, 142 | id, 143 | d.Get("name").(string), 144 | d.Get("description").(string), 145 | d.Get("public").(bool), 146 | ) 147 | 148 | if err != nil { 149 | return diag.Errorf("ChangePlaylist: %s", err.Error()) 150 | } 151 | } 152 | 153 | if d.HasChange("tracks") { 154 | new := spotifyIdsInterface(d.Get("tracks").([]interface{})) 155 | 156 | var err error 157 | var snapshotID string 158 | for i, rng := range batches(len(new), 100) { 159 | if i == 0 { 160 | err = client.ReplacePlaylistTracks(ctx, id, new[rng.Start:rng.End]...) 161 | } else { 162 | snapshotID, err = client.AddTracksToPlaylist(ctx, id, new[rng.Start:rng.End]...) 163 | } 164 | 165 | if err != nil { 166 | return diag.Errorf("update playlist tracks: %s", err.Error()) 167 | } 168 | } 169 | 170 | if snapshotID != "" { 171 | if err := d.Set("snapshot_id", snapshotID); err != nil { 172 | return diag.FromErr(err) 173 | } 174 | } 175 | } 176 | 177 | return nil 178 | } 179 | 180 | func resourcePlaylistDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 181 | client := m.(*spotify.Client) 182 | 183 | id := spotify.ID(d.Id()) 184 | if err := client.UnfollowPlaylist(ctx, id); err != nil { 185 | return diag.FromErr(err) 186 | } 187 | 188 | return nil 189 | } 190 | -------------------------------------------------------------------------------- /spotify/resource_playlist_test.go: -------------------------------------------------------------------------------- 1 | package spotify_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | "github.com/jarcoal/httpmock" 9 | spotifyApi "github.com/zmb3/spotify/v2" 10 | ) 11 | 12 | func TestSpotify_Resource_Playlist(t *testing.T) { 13 | httpmock.Activate() 14 | defer httpmock.DeactivateAndReset() 15 | 16 | apiKey := "some-api-key" 17 | accessToken := "some-access-token" 18 | 19 | resource.Test(t, resource.TestCase{ 20 | IsUnitTest: true, 21 | Providers: testAccProviders, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: fmt.Sprintf(` 25 | provider "spotify" { 26 | api_key = "%s" 27 | } 28 | 29 | resource "spotify_playlist" "playlist" { 30 | name = "My Playlist" 31 | description = "A test playlist" 32 | 33 | tracks = ["track-1", "track-2"] 34 | } 35 | `, apiKey), 36 | Check: resource.ComposeTestCheckFunc( 37 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "id", "spotify-playlist-1"), 38 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "name", "My Playlist"), 39 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "description", "A test playlist"), 40 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "snapshot_id", "snapshot1"), 41 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "tracks.#", "2"), 42 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "tracks.0", "track-1"), 43 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "tracks.1", "track-2"), 44 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "public", "true"), 45 | ), 46 | PreConfig: func() { 47 | RegisterAuthResponse(apiKey, accessToken) 48 | 49 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/me", RespondWith( 50 | JSON(spotifyApi.PrivateUser{ 51 | User: spotifyApi.User{ 52 | ID: "user-1", 53 | }, 54 | }), 55 | VerifyBearer(accessToken), 56 | )) 57 | 58 | httpmock.RegisterResponder("POST", "https://api.spotify.com/v1/users/user-1/playlists", 59 | RespondWith( 60 | JSON(spotifyApi.FullPlaylist{ 61 | SimplePlaylist: spotifyApi.SimplePlaylist{ 62 | ID: spotifyApi.ID("spotify-playlist-1"), 63 | }, 64 | }), 65 | VerifyBearer(accessToken), 66 | VerifyJSONBody(object{ 67 | "name": "My Playlist", 68 | "description": "A test playlist", 69 | "public": true, 70 | "collaborative": false, 71 | }), 72 | ).Once(), 73 | ) 74 | 75 | httpmock.RegisterResponder("POST", "https://api.spotify.com/v1/playlists/spotify-playlist-1/tracks", 76 | RespondWith( 77 | JSON(object{"snapshot_id": "snapshot1"}), 78 | VerifyBearer(accessToken), 79 | VerifyJSONBody(object{ 80 | "uris": array{ 81 | "spotify:track:track-1", 82 | "spotify:track:track-2", 83 | }, 84 | }), 85 | ).Once(), 86 | ) 87 | 88 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/playlists/spotify-playlist-1", 89 | RespondWith( 90 | JSON(spotifyApi.FullPlaylist{ 91 | SimplePlaylist: spotifyApi.SimplePlaylist{ 92 | ID: spotifyApi.ID("spotify-playlist-1"), 93 | Name: "My Playlist", 94 | IsPublic: true, 95 | SnapshotID: "snapshot1", 96 | }, 97 | Description: "A test playlist", 98 | }), 99 | VerifyBearer(accessToken), 100 | ), 101 | ) 102 | 103 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/playlists/spotify-playlist-1/tracks", 104 | RespondWith( 105 | JSON(playlistTrackPage("track-1", "track-2")), 106 | VerifyBearer(accessToken), 107 | ), 108 | ) 109 | }, 110 | }, 111 | { 112 | Config: ` 113 | provider "spotify" { 114 | api_key = "some-api-key" 115 | } 116 | 117 | resource "spotify_playlist" "playlist" { 118 | name = "My New Playlist" 119 | description = "A test playlist" 120 | 121 | tracks = ["track-1", "track-3"] 122 | } 123 | `, 124 | Check: resource.ComposeTestCheckFunc( 125 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "id", "spotify-playlist-1"), 126 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "name", "My New Playlist"), 127 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "description", "A test playlist"), 128 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "snapshot_id", "snapshot3"), 129 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "tracks.#", "2"), 130 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "tracks.0", "track-1"), 131 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "tracks.1", "track-3"), 132 | resource.TestCheckResourceAttr("spotify_playlist.playlist", "public", "true"), 133 | ), 134 | PreConfig: func() { 135 | httpmock.RegisterResponder("PUT", "https://api.spotify.com/v1/playlists/spotify-playlist-1", 136 | RespondWith( 137 | JSON(nil), 138 | VerifyBearer(accessToken), 139 | VerifyJSONBody(object{ 140 | "name": "My New Playlist", 141 | "description": "A test playlist", 142 | "public": true, 143 | }), 144 | ).Once(), 145 | ) 146 | 147 | httpmock.RegisterResponder("PUT", "https://api.spotify.com/v1/playlists/spotify-playlist-1/tracks?uris=spotify:track:track-1,spotify:track:track-3", 148 | RespondWith( 149 | JSON(object{"snapshot_id": "snapshot2"}), 150 | VerifyBearer(accessToken), 151 | ).Once(), 152 | ) 153 | 154 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/playlists/spotify-playlist-1", 155 | RespondWith( 156 | JSON(spotifyApi.FullPlaylist{ 157 | SimplePlaylist: spotifyApi.SimplePlaylist{ 158 | ID: spotifyApi.ID("spotify-playlist-1"), 159 | Name: "My New Playlist", 160 | IsPublic: true, 161 | SnapshotID: "snapshot3", 162 | }, 163 | Description: "A test playlist", 164 | }), 165 | VerifyBearer(accessToken), 166 | ), 167 | ) 168 | 169 | httpmock.RegisterResponder("GET", "https://api.spotify.com/v1/playlists/spotify-playlist-1/tracks", 170 | RespondWith( 171 | JSON(playlistTrackPage("track-1", "track-3")), 172 | VerifyBearer(accessToken), 173 | ), 174 | ) 175 | 176 | httpmock.RegisterResponder("DELETE", "https://api.spotify.com/v1/playlists/spotify-playlist-1/followers", 177 | RespondWith( 178 | JSON(nil), 179 | VerifyBearer(accessToken), 180 | ), 181 | ) 182 | }, 183 | }, 184 | }, 185 | }) 186 | } 187 | 188 | func playlistTrackPage(tracks ...string) spotifyApi.PlaylistTrackPage { 189 | playlistTracks := make([]spotifyApi.PlaylistTrack, len(tracks)) 190 | for i, track := range tracks { 191 | playlistTracks[i].Track.ID = spotifyApi.ID(track) 192 | } 193 | return spotifyApi.PlaylistTrackPage{Tracks: playlistTracks} 194 | } 195 | -------------------------------------------------------------------------------- /spotify/util.go: -------------------------------------------------------------------------------- 1 | package spotify 2 | 3 | import "github.com/zmb3/spotify/v2" 4 | 5 | type Range struct { 6 | Start int 7 | End int 8 | } 9 | 10 | func batches(length, batch int) []Range { 11 | if length <= 0 { 12 | return nil 13 | } 14 | 15 | var ranges []Range 16 | i := 0 17 | for ; i < (length - batch); i += batch { 18 | ranges = append(ranges, Range{i, i + batch}) 19 | } 20 | return append(ranges, Range{i, length}) 21 | } 22 | 23 | func spotifyIdsInterface(s []interface{}) []spotify.ID { 24 | output := make([]spotify.ID, len(s)) 25 | for i, v := range s { 26 | output[i] = spotify.ID(v.(string)) 27 | } 28 | return output 29 | } 30 | -------------------------------------------------------------------------------- /spotify/util_test.go: -------------------------------------------------------------------------------- 1 | package spotify 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestBatches(t *testing.T) { 10 | ranges := batches(521, 100) 11 | assert.Equal(t, []Range{ 12 | {Start: 0, End: 100}, 13 | {Start: 100, End: 200}, 14 | {Start: 200, End: 300}, 15 | {Start: 300, End: 400}, 16 | {Start: 400, End: 500}, 17 | {Start: 500, End: 521}, 18 | }, ranges) 19 | } 20 | 21 | func TestBatchesFull(t *testing.T) { 22 | ranges := batches(100, 100) 23 | assert.Equal(t, []Range{ 24 | {Start: 0, End: 100}, 25 | }, ranges) 26 | } 27 | 28 | func TestBatchesPartial(t *testing.T) { 29 | ranges := batches(50, 100) 30 | assert.Equal(t, []Range{ 31 | {Start: 0, End: 50}, 32 | }, ranges) 33 | } 34 | 35 | func TestBatchesEmpty(t *testing.T) { 36 | ranges := batches(0, 100) 37 | assert.Empty(t, ranges) 38 | } 39 | -------------------------------------------------------------------------------- /spotify_auth_proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine AS build 2 | 3 | WORKDIR /src 4 | COPY go.mod go.sum ./ 5 | RUN go mod download 6 | COPY main.go . 7 | RUN CGO_ENABLED=0 go build -o /bin/spotify_auth_proxy 8 | 9 | FROM alpine:latest 10 | 11 | WORKDIR /home/spotify_auth_proxy 12 | RUN addgroup -S spotify && \ 13 | adduser -S spotify_auth_proxy -G spotify 14 | 15 | USER spotify_auth_proxy 16 | COPY --from=build /bin/spotify_auth_proxy .local/bin/spotify_auth_proxy 17 | ENTRYPOINT [".local/bin/spotify_auth_proxy"] 18 | -------------------------------------------------------------------------------- /spotify_auth_proxy/README.md: -------------------------------------------------------------------------------- 1 | # spotify_auth_proxy 2 | 3 | This is an instance of a 'Spotify auth server' which acts as an interface between a client and the Spotify oauth API. 4 | 5 | ## Installation 6 | 7 | With `go` installed, run 8 | 9 | ```sh 10 | go get -u github.com/conradludgate/terraform-provider-spotify/spotify_auth_proxy 11 | ``` 12 | 13 | ## Usage 14 | 15 | First, you need a Spotify client ID and secret. Visit https://developer.spotify.com/dashboard/ to create an application. 16 | 17 | If you plan to run this proxy locally, set the redirect URI of the application to `http://localhost:27228/spotify_callback`. 18 | If you're running it remotely, substitute an appropiate base url (eg `https://spotify.example.com/spotify_callback`) 19 | 20 | You will also need to register the callback URI with Spotify for your application. Visit https://developer.spotify.com/dashboard/, click on your application, find and click the "Edit Settings" button, and paste the `spotify_callback` URI above into "Redirect URIs". Scroll down and click "Save". 21 | 22 | To start the server, make sure the environment variables `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` are set from the values on the Spotify dashboard. 23 | If running remotely, also configure your base url 24 | 25 | ```sh 26 | export SPOTIFY_PROXY_BASE_URI=https://spotify.example.com 27 | ``` 28 | 29 | Finally, run 30 | 31 | ```sh 32 | spotify_auth_proxy 33 | ``` 34 | 35 | It should output the following: 36 | 37 | ``` 38 | APIKey: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 39 | Auth URL: ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ 40 | ``` 41 | 42 | Take note of these values. 43 | 44 | Now, open a browser and navigate to the Auth URL. It should redirect you to Spotify to log in. After you log in, the auth server will redirect you back to the page where it should confirm that you've authorized correctly. 45 | 46 | The API Key is how you will retrieve the access token. The server will handle the token expiration and refreshes for you. 47 | 48 | The API Key is random on each invocation, you can specify the `SPOTIFY_PROXY_API_KEY` environment variable to keep it consistent 49 | 50 | ## Docker 51 | 52 | Alternatively, you can use the Docker to run the Spotify auth proxy. 53 | 54 | First, create a file named `.env` and populate it with your `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` values. Your file should look similar to the following. 55 | 56 | ``` 57 | SPOTIFY_CLIENT_ID= 58 | SPOTIFY_CLIENT_SECRET= 59 | ``` 60 | 61 | Then, run the following command to start the auth proxy. 62 | 63 | ``` 64 | $ docker run --rm -it -p 27228:27228 --env-file ./.env ghcr.io/conradludgate/spotify-auth-proxy 65 | APIKey: OK7b1j... 66 | Token: aoIvJT... 67 | Auth: http://localhost:27228/authorize?token=aoIvJT... 68 | ``` 69 | -------------------------------------------------------------------------------- /spotify_auth_proxy/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/conradludgate/terraform-provider-spotify/spotify_auth_proxy 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/caarlos0/env/v6 v6.6.2 7 | golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c 8 | ) 9 | -------------------------------------------------------------------------------- /spotify_auth_proxy/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.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 17 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 18 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 19 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 20 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 28 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 29 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 30 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 31 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 32 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 33 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 34 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 35 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 36 | github.com/caarlos0/env/v6 v6.6.2 h1:BypLXDWQTA32rS4UM7pBz+/0BOuvs6C7LSeQAxMwyvI= 37 | github.com/caarlos0/env/v6 v6.6.2/go.mod h1:P0BVSgU9zfkxfSpFUs6KsO3uWR4k3Ac0P66ibAGTybM= 38 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 39 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 40 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 41 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 42 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 43 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 44 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 45 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 46 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 47 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 48 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 49 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 50 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 51 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 52 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 53 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 54 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 55 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 56 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 57 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 58 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 59 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 60 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 61 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 62 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 63 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 64 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 65 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 66 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 67 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 68 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 69 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 70 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 71 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 72 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 73 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 74 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 75 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 76 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 77 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 78 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 79 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 80 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 81 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 82 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 83 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 84 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 85 | github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= 86 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 87 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 88 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 89 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 90 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 91 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 92 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 93 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 94 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 95 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 96 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 97 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 98 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 99 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 100 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 101 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 102 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 103 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 104 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 105 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 106 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 107 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 108 | github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= 109 | github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 110 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 111 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 112 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 113 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 115 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 116 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 117 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 118 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 119 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 120 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 121 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 122 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 123 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 124 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 125 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 126 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 127 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 128 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 129 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 130 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 131 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 132 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 133 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 134 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 135 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 136 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 137 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 138 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 139 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 140 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 141 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 142 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 143 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 144 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 145 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 146 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 147 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 148 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 149 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 150 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 151 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 152 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 153 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 154 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 155 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 156 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 157 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 158 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 159 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 160 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 161 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 162 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 163 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 164 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 165 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 166 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 167 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 168 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 169 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 170 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 171 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 172 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 173 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 174 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 175 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 176 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 177 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 178 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 179 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 180 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 181 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 182 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 183 | golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= 184 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 185 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 186 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 187 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 188 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 189 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 190 | golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c h1:SgVl/sCtkicsS7psKkje4H9YtjdEl3xsYh7N+5TDHqY= 191 | golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 192 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 193 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 194 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 195 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 196 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 197 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 198 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 199 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 200 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 201 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 202 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 206 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 211 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 212 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 213 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 216 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 217 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 218 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 219 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 220 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 221 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 222 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 223 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 224 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 225 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 226 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 227 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 228 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 229 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 230 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 231 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 232 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 233 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 234 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 235 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 236 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 237 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 238 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 239 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 240 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 241 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 242 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 243 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 244 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 245 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 246 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 247 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 248 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 249 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 250 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 251 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 252 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 253 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 254 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 255 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 256 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 257 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 258 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 259 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 260 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 261 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 262 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 263 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 264 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 265 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 266 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 267 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 268 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 269 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 270 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 271 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 272 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 273 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 274 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 275 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 276 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 277 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 278 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 279 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 280 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 281 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 282 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 283 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 284 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 285 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 286 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 287 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 288 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 289 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 290 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 291 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 292 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 293 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 294 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 295 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 296 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 297 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 298 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 299 | google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= 300 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 301 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 302 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 303 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 304 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 305 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 306 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 307 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 308 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 309 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 310 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 311 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 312 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 313 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 314 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 315 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 316 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 317 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 318 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 319 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 320 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 321 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 322 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 323 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 324 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 325 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 326 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 327 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 328 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 329 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 330 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 331 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 332 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 333 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 334 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 335 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 336 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 337 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 338 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 339 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 340 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 341 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 342 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 343 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 344 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 345 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 346 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 347 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 348 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 349 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 350 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 351 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 352 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 353 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 354 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 355 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 356 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 357 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 358 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 359 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 360 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 361 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 362 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 363 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 364 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 365 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 366 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 367 | -------------------------------------------------------------------------------- /spotify_auth_proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/hmac" 6 | "crypto/rand" 7 | "crypto/sha256" 8 | "encoding/base64" 9 | "encoding/json" 10 | "fmt" 11 | "log" 12 | "net/http" 13 | "net/url" 14 | "os" 15 | "path" 16 | 17 | "github.com/caarlos0/env/v6" 18 | "golang.org/x/oauth2" 19 | "golang.org/x/oauth2/spotify" 20 | ) 21 | 22 | const id = "SpotifyAuthProxy" 23 | 24 | var ( 25 | source oauth2.TokenSource 26 | envCfg *EnvConfig 27 | token string 28 | 29 | config *oauth2.Config 30 | ) 31 | 32 | type EnvConfig struct { 33 | ClientID string `env:"SPOTIFY_CLIENT_ID,required"` 34 | ClientSecret string `env:"SPOTIFY_CLIENT_SECRET,required"` 35 | BaseURL *url.URL `env:"SPOTIFY_PROXY_BASE_URI" envDefault:"http://localhost:27228"` 36 | APIKey string `env:"SPOTIFY_PROXY_API_KEY"` 37 | } 38 | 39 | func main() { 40 | envCfg = new(EnvConfig) 41 | if err := env.Parse(envCfg); err != nil { 42 | fmt.Printf("%+v\n", err) 43 | os.Exit(1) 44 | } 45 | 46 | if envCfg.APIKey == "" { 47 | envCfg.APIKey = randString() 48 | fmt.Println("APIKey: ", envCfg.APIKey) 49 | } 50 | 51 | token = randString() 52 | 53 | authUrl := *envCfg.BaseURL 54 | authUrl.Path = path.Join(authUrl.Path, "authorize") 55 | authUrl.RawQuery = url.Values{"token": {token}}.Encode() 56 | 57 | fmt.Println("Auth URL:", authUrl.String()) 58 | 59 | redirectUrl := *envCfg.BaseURL 60 | redirectUrl.Path = path.Join(redirectUrl.Path, "spotify_callback") 61 | 62 | config = &oauth2.Config{ 63 | ClientID: envCfg.ClientID, 64 | ClientSecret: envCfg.ClientSecret, 65 | RedirectURL: redirectUrl.String(), 66 | Endpoint: spotify.Endpoint, 67 | Scopes: []string{ 68 | "user-read-email", 69 | "user-read-private", 70 | // "playlist-read-collaborative", 71 | "playlist-read-private", 72 | "playlist-modify-private", 73 | "playlist-modify-public", 74 | "user-library-read", 75 | "user-library-modify", 76 | // "ugc-image-upload", 77 | }, 78 | } 79 | 80 | http.HandleFunc("/authorize", Authorize) 81 | http.HandleFunc("/api/v1/token/terraform", APIToken) 82 | http.HandleFunc("/spotify_callback", SpotifyCallback) 83 | http.HandleFunc("/health", HealthCheck) 84 | 85 | log.Fatal(http.ListenAndServe(":27228", nil)) 86 | } 87 | 88 | // APIToken is the endpoint for refreshing API tokens 89 | func APIToken(w http.ResponseWriter, r *http.Request) { 90 | username, password, ok := r.BasicAuth() 91 | if !ok || username != id || password != envCfg.APIKey { 92 | w.WriteHeader(http.StatusUnauthorized) 93 | fmt.Fprintln(w, "APIToken: invalid authorization") 94 | return 95 | } 96 | 97 | if source == nil { 98 | w.WriteHeader(http.StatusBadRequest) 99 | fmt.Fprintln(w, "APIToken: token not available") 100 | return 101 | } 102 | 103 | token, err := source.Token() 104 | if err != nil { 105 | w.WriteHeader(http.StatusForbidden) 106 | fmt.Fprintln(w, "APIToken: could not retrieve token") 107 | return 108 | } 109 | 110 | if err := json.NewEncoder(w).Encode(token); err != nil { 111 | fmt.Fprintf(w, "APIToken: could not encode JSON response: %s\n", err) 112 | w.WriteHeader(http.StatusInternalServerError) 113 | } 114 | 115 | fmt.Println("Token Retrieved") 116 | } 117 | 118 | // Authorize takes a user through the auth flow to get a new access token 119 | func Authorize(w http.ResponseWriter, r *http.Request) { 120 | key := r.FormValue("token") 121 | if key != token { 122 | fmt.Fprintln(w, "Authorize: you are not authorized to sign in to this platform") 123 | w.WriteHeader(http.StatusUnauthorized) 124 | } 125 | 126 | state := randString() 127 | mac := hmac.New(sha256.New, []byte(envCfg.APIKey)) 128 | mac.Write([]byte(state)) 129 | state += base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) 130 | 131 | http.Redirect(w, r, config.AuthCodeURL(state), http.StatusSeeOther) 132 | } 133 | 134 | // SpotifyCallback handles the redirect from spotify after authorizing 135 | func SpotifyCallback(w http.ResponseWriter, r *http.Request) { 136 | if err := r.FormValue("error"); err != "" { 137 | fmt.Fprintf(w, "Could not complete authorization: %s\n", err) 138 | w.WriteHeader(http.StatusBadRequest) 139 | return 140 | } 141 | 142 | code := r.FormValue("code") 143 | if code == "" { 144 | fmt.Fprintln(w, "Could not complete authorization: no auth code present") 145 | w.WriteHeader(http.StatusBadRequest) 146 | return 147 | } 148 | 149 | state := r.FormValue("state") 150 | if len(state) < 64 { 151 | fmt.Fprintln(w, "Could not complete authorization: invalid state value") 152 | w.WriteHeader(http.StatusUnauthorized) 153 | return 154 | } 155 | 156 | state1 := []byte(state[:64]) 157 | stateMac, err := base64.RawURLEncoding.DecodeString(state[64:]) 158 | if err != nil { 159 | fmt.Fprintln(w, "Could not complete authorization: invalid state value") 160 | w.WriteHeader(http.StatusUnauthorized) 161 | return 162 | } 163 | 164 | mac := hmac.New(sha256.New, []byte(envCfg.APIKey)) 165 | mac.Write(state1) 166 | expectedMac := mac.Sum(nil) 167 | if !hmac.Equal(stateMac, expectedMac) { 168 | fmt.Fprintln(w, "Could not complete authorization: invalid state value") 169 | w.WriteHeader(http.StatusUnauthorized) 170 | return 171 | } 172 | 173 | token, err := config.Exchange(r.Context(), code) 174 | if err != nil { 175 | fmt.Fprintln(w, "Could not complete authorization: invalid auth code") 176 | w.WriteHeader(http.StatusUnauthorized) 177 | return 178 | } 179 | 180 | source = config.TokenSource(context.Background(), token) 181 | 182 | fmt.Fprintln(w, "Authorization successful") 183 | fmt.Println("Authorization successful") 184 | } 185 | 186 | func HealthCheck(w http.ResponseWriter, r *http.Request) { 187 | w.Header().Set("Content-Type", "application/json") 188 | status := "healthy" 189 | 190 | response := struct { 191 | Status string `json:"status"` 192 | }{ 193 | Status: status, 194 | } 195 | 196 | jsonResponse, err := json.Marshal(response) 197 | if err != nil { 198 | w.WriteHeader(http.StatusInternalServerError) 199 | fmt.Fprintln(w, "Error encoding JSON response:", err) 200 | return 201 | } 202 | 203 | w.WriteHeader(http.StatusOK) 204 | w.Header().Set("Content-Type", "application/json") 205 | w.Write(jsonResponse) 206 | } 207 | 208 | func randString() string { 209 | output := make([]byte, 48) 210 | n, err := rand.Reader.Read(output) 211 | if err != nil { 212 | panic(err) 213 | } 214 | if n != 48 { 215 | panic("could not read 48 bytes") 216 | } 217 | return base64.RawURLEncoding.EncodeToString(output) 218 | } 219 | --------------------------------------------------------------------------------