├── .github ├── dependabot.yml └── workflows │ ├── images.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── go.mod ├── go.sum └── main.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: "gomod" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | - package-ecosystem: "docker" 17 | directory: "/" 18 | schedule: 19 | interval: "daily" 20 | -------------------------------------------------------------------------------- /.github/workflows/images.yml: -------------------------------------------------------------------------------- 1 | name: Publish container images 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | 11 | jobs: 12 | containers: 13 | name: Build and push container image registries 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | packages: write 18 | contents: read 19 | 20 | steps: 21 | - name: Check out the repo 22 | uses: actions/checkout@v4 23 | 24 | - name: Log in to the Container registry 25 | uses: docker/login-action@v3 26 | with: 27 | registry: ghcr.io 28 | username: ${{ github.actor }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | if: startsWith(github.ref, 'refs/tags/v') || github.ref == format('refs/heads/{0}', github.event.repository.default_branch) 31 | 32 | - name: Extract metadata (tags, labels) for Docker 33 | id: meta 34 | uses: docker/metadata-action@v5 35 | with: 36 | images: ghcr.io/${{ github.repository }} 37 | 38 | - name: Build and push Docker images 39 | uses: docker/build-push-action@v6 40 | with: 41 | push: ${{ startsWith(github.ref, 'refs/tags/v') || github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} 42 | tags: ${{ steps.meta.outputs.tags }} 43 | labels: ${{ steps.meta.outputs.labels }} 44 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - master 9 | - main 10 | pull_request: 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v6 19 | with: 20 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 21 | version: latest 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | go: [ '1.23', '1.24' ] 16 | name: Go ${{ matrix.go }} test 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Setup go 20 | uses: actions/setup-go@v5.3.0 21 | with: 22 | go-version: ${{ matrix.go }} 23 | - run: go test ./... 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### IntelliJ IDEA ### 2 | .idea 3 | *.iws 4 | *.iml 5 | *.ipr 6 | 7 | /*.env 8 | /*.yml 9 | dist/ 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.1 as builder 2 | WORKDIR /build 3 | COPY . /build/ 4 | 5 | RUN go mod download 6 | RUN CGO_ENABLED=0 go build -o app 7 | 8 | FROM alpine:latest 9 | RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* 10 | WORKDIR /app 11 | COPY --from=builder /build/app /app 12 | EXPOSE 8080 13 | CMD ["/app/app"] 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2020 Andy Lo-A-Foe 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-registry 2 | This is a light weight Terraform Registry, more like a proxy. 3 | It currently only supports the `v1.provider` endpoint and Terraform provider releases hosted on Github. 4 | 5 | ## how it works 6 | The registry dynamically generates the correct response based on assets found in 7 | Github provider releases which conform to the Terraform asset conventions. 8 | There is one additional file required which should be called `signkey.asc` 9 | This file must contain the [ASCII Armored PGP public key](https://www.terraform.io/docs/registry/providers/publishing.html) which was 10 | used to sign the `..._SHA256SUMS.sig` signature file. 11 | If you don't have a PGP key yet, [you can generate one easily](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/generating-a-new-gpg-key). 12 | 13 | ## use cases 14 | - host your own private Terraform provider registry 15 | - easily release custom builds of providers e.g. releases from your own forks 16 | 17 | ## deployment 18 | Build a docker image and deploy it to your favorite hosting location 19 | 20 | ## endpoints 21 | | Endpoint | Description | 22 | |-----------|-------------| 23 | | `/.well-known/terraform.json` | The service discovery endpoint used by terraform | 24 | | `/v1/providers/:namespace/:type/*` | The `versions` and `download` action endpoints | 25 | 26 | ## example usage 27 | 28 | ```terraform 29 | terraform { 30 | required_providers { 31 | cloudfoundry = { 32 | source = "terraform-registry.us-east.philips-healthsuite.com/philips-forks/cloudfoundry" 33 | version = "0.12.2-202008131826" 34 | } 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | The above assumes a copy of the terraform-registry running at: 41 | 42 | https://terraform-registry.us-east.philips-healthsuite.com 43 | 44 | It references `philips-forks/cloudfoundry` version `0.12.2-202008131826` which maps to the following Github repository and release: 45 | 46 | https://github.com/philips-forks/terraform-provider-cloudfoundry/releases/tag/v0.12.2-202008131826 47 | 48 | Notice the `signkey.asc` which is included in this release. You can use [Goreleaser](https://goreleaser.com/quick-start/) with this [.goreleaser.yml](https://github.com/hashicorp/terraform-provider-scaffolding/blob/master/.goreleaser.yml) template to create arbitrary releases of providers. The provider pointer also does not include the `terraform-provider-` prefix. 49 | 50 | ## GitHub Enterprise Server 51 | 52 | If you want to use the registry against a GitHub Enterprise server, just specify additional environment variables. 53 | 54 | * `GITHUB_ENTERPRISE_URL` would be a value like `https://github.example.com`. 55 | * `GITHUB_ENTERPRISE_UPLOADS_URL` depends on your installation, it defaults to the base URL. 56 | 57 | See [go-github documentation](https://pkg.go.dev/github.com/google/go-github/v32@v32.1.0/github#NewEnterpriseClient) for details on both URLs. 58 | 59 | ## private repositories 60 | 61 | ### authenticating via Personal Access Token 62 | 63 | 1. Create token with `repo` scope [here](https://github.com/settings/tokens/new) 64 | 65 | If you are using GitHub SSO for your organization, press `Enable SSO` button on your token and authorize it for this organization. 66 | 67 | 2. Set token in `GITHUB_TOKEN` environment variable 68 | 69 | ## current limitations and TODOs 70 | - Only supports providers 71 | 72 | ## contact / getting help 73 | andy.lo-a-foe@philips.com 74 | 75 | ## license 76 | License is MIT 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module terraform-registry 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/ProtonMail/go-crypto v1.1.6 7 | github.com/google/go-github/v32 v32.1.0 8 | github.com/labstack/echo/v4 v4.13.3 9 | golang.org/x/oauth2 v0.26.0 10 | ) 11 | 12 | require ( 13 | github.com/cloudflare/circl v1.3.7 // indirect 14 | github.com/google/go-querystring v1.0.0 // indirect 15 | github.com/labstack/gommon v0.4.2 // indirect 16 | github.com/mattn/go-colorable v0.1.13 // indirect 17 | github.com/mattn/go-isatty v0.0.20 // indirect 18 | github.com/valyala/bytebufferpool v1.0.0 // indirect 19 | github.com/valyala/fasttemplate v1.2.2 // indirect 20 | golang.org/x/crypto v0.33.0 // indirect 21 | golang.org/x/net v0.35.0 // indirect 22 | golang.org/x/sys v0.30.0 // indirect 23 | golang.org/x/text v0.22.0 // indirect 24 | golang.org/x/time v0.8.0 // indirect 25 | ) 26 | 27 | replace golang.org/x/crypto/openpgp v0.0.0-20210817164053-32db794688a5 => github.com/ProtonMail/go-crypto/openpgp v0.0.0-20220517143526-88bb52951d5b 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= 2 | github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= 3 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 4 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 7 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 8 | github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II= 9 | github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= 10 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 11 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 12 | github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 13 | github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 14 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 15 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 16 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 17 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 18 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 19 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 20 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 23 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 24 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 25 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 26 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 27 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 28 | golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= 29 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 30 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 31 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 32 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 33 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 34 | golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= 35 | golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 40 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 41 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 42 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 43 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 44 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 45 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 46 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 47 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 48 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Andy Lo-A-Foe 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package main 23 | 24 | import ( 25 | "bufio" 26 | "bytes" 27 | "context" 28 | "fmt" 29 | "io/ioutil" 30 | "net/http" 31 | "os" 32 | "regexp" 33 | "strings" 34 | 35 | "github.com/ProtonMail/go-crypto/openpgp" 36 | "github.com/ProtonMail/go-crypto/openpgp/armor" 37 | "github.com/ProtonMail/go-crypto/openpgp/packet" 38 | 39 | "golang.org/x/oauth2" 40 | 41 | "github.com/google/go-github/v32/github" 42 | "github.com/labstack/echo/v4" 43 | "github.com/labstack/echo/v4/middleware" 44 | ) 45 | 46 | var ( 47 | shasumRegexp = regexp.MustCompile(`^(?P[^_]+)_(?P[^_]+)_SHA256SUMS`) 48 | binaryRegexp = regexp.MustCompile(`^(?P[^_]+)_(?P[^_]+)_(?P\w+)_(?P\w+)`) 49 | actionRegexp = regexp.MustCompile(`^(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/(?P\w+)`) 50 | ) 51 | 52 | func main() { 53 | e := echo.New() 54 | e.Use(middleware.Logger()) 55 | 56 | client, err := newClient() 57 | if err != nil { 58 | e.Logger.Error(err) 59 | os.Exit(1) 60 | } 61 | 62 | e.GET("/.well-known/terraform.json", serviceDiscoveryHandler()) 63 | e.GET("/v1/providers/:namespace/:type/*", client.providerHandler()) 64 | 65 | port := os.Getenv("PORT") 66 | 67 | if port == "" { 68 | port = "8080" 69 | } 70 | 71 | err = e.Start(fmt.Sprintf(":%s", port)) 72 | if err != nil { 73 | e.Logger.Error(err) 74 | os.Exit(1) 75 | } 76 | } 77 | 78 | func serviceDiscoveryHandler() echo.HandlerFunc { 79 | return func(c echo.Context) error { 80 | response := struct { 81 | Providers string `json:"providers.v1"` 82 | }{ 83 | Providers: "/v1/providers/", 84 | } 85 | return c.JSON(http.StatusOK, response) 86 | } 87 | } 88 | 89 | type Client struct { 90 | github *github.Client 91 | authenticated bool 92 | http *http.Client 93 | } 94 | 95 | type Platform struct { 96 | Os string `json:"os"` 97 | Arch string `json:"arch"` 98 | } 99 | 100 | type VersionResponse struct { 101 | ID string `json:"id"` 102 | Versions []Version `json:"versions"` 103 | Warnings interface{} `json:"warnings"` 104 | } 105 | 106 | type GPGPublicKey struct { 107 | KeyID string `json:"key_id"` 108 | ASCIIArmor string `json:"ascii_armor"` 109 | TrustSignature string `json:"trust_signature"` 110 | Source string `json:"source"` 111 | SourceURL interface{} `json:"source_url"` 112 | } 113 | 114 | type SigningKeys struct { 115 | GpgPublicKeys []GPGPublicKey `json:"gpg_public_keys,omitempty"` 116 | } 117 | 118 | type DownloadResponse struct { 119 | Protocols []string `json:"protocols,omitempty"` 120 | Os string `json:"os"` 121 | Arch string `json:"arch"` 122 | Filename string `json:"filename"` 123 | DownloadURL string `json:"download_url"` 124 | ShasumsURL string `json:"shasums_url"` 125 | ShasumsSignatureURL string `json:"shasums_signature_url"` 126 | Shasum string `json:"shasum"` 127 | SigningKeys SigningKeys `json:"signing_keys"` 128 | } 129 | 130 | type ErrorResponse struct { 131 | Status int `json:"status"` 132 | Message string `json:"message"` 133 | } 134 | 135 | type Version struct { 136 | Version string `json:"version"` 137 | Protocols []string `json:"protocols,omitempty"` 138 | Platforms []Platform `json:"platforms"` 139 | ReleaseAsset *github.ReleaseAsset `json:"-"` 140 | } 141 | 142 | func newClient() (*Client, error) { 143 | client := &Client{} 144 | 145 | if token, ok := os.LookupEnv("GITHUB_TOKEN"); ok { 146 | ctx := context.Background() 147 | ts := oauth2.StaticTokenSource( 148 | &oauth2.Token{AccessToken: token}, 149 | ) 150 | 151 | client.http = oauth2.NewClient(ctx, ts) 152 | client.authenticated = true 153 | } 154 | 155 | if serverURL := os.Getenv("GITHUB_ENTERPRISE_URL"); serverURL != "" { 156 | uploadURL := serverURL 157 | 158 | if url := os.Getenv("GITHUB_ENTERPRISE_UPLOADS_URL"); url != "" { 159 | uploadURL = url 160 | } 161 | 162 | ghClient, err := github.NewEnterpriseClient(serverURL, uploadURL, client.http) 163 | if err != nil { 164 | return nil, fmt.Errorf("could not create enterprise client: %w", err) 165 | } 166 | 167 | client.github = ghClient 168 | } else { 169 | client.github = github.NewClient(client.http) 170 | } 171 | 172 | return client, nil 173 | } 174 | 175 | func (client *Client) getURL(c echo.Context, asset *github.ReleaseAsset) (string, error) { 176 | if client.authenticated { 177 | namespace := c.Get("namespace").(string) 178 | provider := c.Get("provider").(string) 179 | 180 | _, url, err := client.github.Repositories.DownloadReleaseAsset(context.Background(), 181 | namespace, provider, *asset.ID, nil) 182 | if err != nil { 183 | return "", err 184 | } 185 | 186 | return url, nil 187 | } 188 | 189 | return *asset.BrowserDownloadURL, nil 190 | } 191 | 192 | func getShasum(asset string, shasumURL string) (string, error) { 193 | resp, err := http.Get(shasumURL) 194 | if err != nil { 195 | return "", err 196 | } 197 | defer resp.Body.Close() 198 | 199 | if resp.StatusCode != 200 { 200 | return "", fmt.Errorf("not found") 201 | } 202 | 203 | scanner := bufio.NewScanner(resp.Body) 204 | for scanner.Scan() { 205 | parts := strings.Split(scanner.Text(), " ") 206 | if len(parts) != 2 { 207 | continue 208 | } 209 | if parts[1] == asset { 210 | return parts[0], nil 211 | } 212 | } 213 | return "", fmt.Errorf("not found") 214 | } 215 | 216 | func (client *Client) providerHandler() echo.HandlerFunc { 217 | return func(c echo.Context) error { 218 | namespace := c.Param("namespace") 219 | typeParam := c.Param("type") 220 | param := c.Param("*") 221 | provider := "terraform-provider-" + typeParam 222 | 223 | repos, _, err := client.github.Repositories.ListReleases(context.Background(), 224 | namespace, provider, nil) 225 | if err != nil { 226 | return c.JSON(http.StatusBadRequest, &ErrorResponse{ 227 | Status: http.StatusBadRequest, 228 | Message: err.Error(), 229 | }) 230 | } 231 | versions, err := parseVersions(repos) 232 | if err != nil { 233 | return c.JSON(http.StatusBadRequest, &ErrorResponse{ 234 | Status: http.StatusBadRequest, 235 | Message: err.Error(), 236 | }) 237 | } 238 | switch param { 239 | case "versions": 240 | response := &VersionResponse{ 241 | ID: namespace + "/" + typeParam, 242 | Versions: versions, 243 | } 244 | return c.JSON(http.StatusOK, response) 245 | default: 246 | c.Set("namespace", namespace) 247 | c.Set("provider", provider) 248 | return client.performAction(c, param, repos) 249 | } 250 | } 251 | } 252 | 253 | func (client *Client) performAction(c echo.Context, param string, repos []*github.RepositoryRelease) error { 254 | match := actionRegexp.FindStringSubmatch(param) 255 | if len(match) < 2 { 256 | fmt.Printf("repos: %v\n", repos) 257 | return c.JSON(http.StatusBadRequest, &ErrorResponse{ 258 | Status: http.StatusBadRequest, 259 | Message: "invalid request", 260 | }) 261 | } 262 | result := make(map[string]string) 263 | for i, name := range actionRegexp.SubexpNames() { 264 | if i != 0 && name != "" { 265 | result[name] = match[i] 266 | } 267 | } 268 | provider := c.Get("provider").(string) 269 | version := result["version"] 270 | os := result["os"] 271 | arch := result["arch"] 272 | filename := fmt.Sprintf("%s_%s_%s_%s.zip", provider, version, os, arch) 273 | shasumFilename := fmt.Sprintf("%s_%s_SHA256SUMS", provider, version) 274 | shasumSigFilename := fmt.Sprintf("%s_%s_SHA256SUMS.sig", provider, version) 275 | signKeyFilename := "signkey.asc" 276 | 277 | downloadURL := "" 278 | shasumURL := "" 279 | shasumSigURL := "" 280 | signKeyURL := "" 281 | 282 | var repo *github.RepositoryRelease 283 | for _, r := range repos { 284 | for _, a := range r.Assets { 285 | if v, err := detectSHASUM(*a.Name); err == nil && version == v.Version { 286 | repo = r 287 | break 288 | } 289 | } 290 | } 291 | if repo == nil { 292 | return c.JSON(http.StatusBadRequest, &ErrorResponse{ 293 | Status: http.StatusBadRequest, 294 | Message: fmt.Sprintf("cannot find version: %s", version), 295 | }) 296 | } 297 | for _, a := range repo.Assets { 298 | if *a.Name == filename { 299 | downloadURL, _ = client.getURL(c, a) 300 | continue 301 | } 302 | if *a.Name == shasumFilename { 303 | shasumURL, _ = client.getURL(c, a) 304 | continue 305 | } 306 | if *a.Name == shasumSigFilename { 307 | shasumSigURL, _ = client.getURL(c, a) 308 | continue 309 | } 310 | if *a.Name == signKeyFilename { 311 | signKeyURL, _ = client.getURL(c, a) 312 | } 313 | } 314 | 315 | shasum, err := getShasum(filename, shasumURL) 316 | if err != nil { 317 | return c.JSON(http.StatusBadRequest, &ErrorResponse{ 318 | Status: http.StatusBadRequest, 319 | Message: fmt.Sprintf("failed getting shasum %v", err), 320 | }) 321 | } 322 | pgpPublicKey, pgpPublicKeyID, err := getPublicKey(signKeyURL) 323 | if err != nil { 324 | return c.JSON(http.StatusBadRequest, &ErrorResponse{ 325 | Status: http.StatusBadRequest, 326 | Message: fmt.Sprintf("failed getting pgp keys %v", err), 327 | }) 328 | } 329 | 330 | switch result["action"] { 331 | case "download": 332 | return c.JSON(http.StatusOK, &DownloadResponse{ 333 | Os: result["os"], 334 | Arch: result["arch"], 335 | Filename: filename, 336 | DownloadURL: downloadURL, 337 | ShasumsSignatureURL: shasumSigURL, 338 | ShasumsURL: shasumURL, 339 | Shasum: shasum, 340 | SigningKeys: SigningKeys{ 341 | GpgPublicKeys: []GPGPublicKey{ 342 | { 343 | KeyID: pgpPublicKeyID, 344 | ASCIIArmor: pgpPublicKey, 345 | }, 346 | }, 347 | }, 348 | }) 349 | default: 350 | return c.JSON(http.StatusBadRequest, &ErrorResponse{ 351 | Status: http.StatusBadRequest, 352 | Message: fmt.Sprintf("unsupported action %s", result["action"]), 353 | }) 354 | } 355 | } 356 | 357 | func getPublicKey(url string) (string, string, error) { 358 | resp, err := http.Get(url) 359 | if err != nil { 360 | return "", "", err 361 | } 362 | defer resp.Body.Close() 363 | 364 | if resp.StatusCode != 200 { 365 | return "", "", fmt.Errorf("not found") 366 | } 367 | 368 | data, err := ioutil.ReadAll(resp.Body) 369 | if err != nil { 370 | return "", "", err 371 | } 372 | // PGP 373 | armored := bytes.NewReader(data) 374 | block, err := armor.Decode(armored) 375 | if err != nil { 376 | return "", "", err 377 | } 378 | if block == nil || block.Type != openpgp.PublicKeyType { 379 | return "", "", fmt.Errorf("not a public key") 380 | } 381 | reader := packet.NewReader(block.Body) 382 | pkt, err := reader.Next() 383 | if err != nil { 384 | return "", "", err 385 | } 386 | key, _ := pkt.(*packet.PublicKey) 387 | 388 | return string(data), key.KeyIdString(), nil 389 | } 390 | 391 | func parseVersions(repos []*github.RepositoryRelease) ([]Version, error) { 392 | details := make([]Version, 0) 393 | for _, r := range repos { 394 | for _, a := range r.Assets { 395 | assetDetails, err := detectSHASUM(*a.Name) 396 | if err == nil { 397 | assetDetails.Platforms = collectPlatforms(r.Assets) 398 | details = append(details, *assetDetails) 399 | break 400 | } 401 | } 402 | } 403 | return details, nil 404 | } 405 | 406 | func detectSHASUM(name string) (*Version, error) { 407 | match := shasumRegexp.FindStringSubmatch(name) 408 | if len(match) < 2 { 409 | return nil, fmt.Errorf("nomatch %d", len(match)) 410 | } 411 | result := make(map[string]string) 412 | for i, name := range shasumRegexp.SubexpNames() { 413 | if i != 0 && name != "" { 414 | result[name] = match[i] 415 | } 416 | } 417 | return &Version{ 418 | Version: result["version"], 419 | }, nil 420 | } 421 | 422 | func collectPlatforms(assets []*github.ReleaseAsset) []Platform { 423 | platforms := make([]Platform, 0) 424 | for _, a := range assets { 425 | match := binaryRegexp.FindStringSubmatch(*a.Name) 426 | if len(match) < 2 { 427 | continue 428 | } 429 | result := make(map[string]string) 430 | for i, name := range binaryRegexp.SubexpNames() { 431 | if i != 0 && name != "" { 432 | result[name] = match[i] 433 | } 434 | } 435 | platforms = append(platforms, Platform{ 436 | Os: result["os"], 437 | Arch: result["arch"], 438 | }) 439 | } 440 | return platforms 441 | } 442 | --------------------------------------------------------------------------------