├── .github ├── publickey.pem └── workflows │ ├── dev-deployment.yml │ ├── lint.yml │ ├── prod-deployment.yml │ └── test.yml ├── .gitignore ├── .terraform-version ├── .terraform.lock.hcl ├── LICENSE ├── README.md ├── apigw.tf ├── dynamo.tf ├── iam.tf ├── keys ├── lambda.tf ├── main.tf ├── r53.tf ├── secrets.tf ├── src ├── .golangci.yml ├── go.mod ├── go.sum ├── internal │ ├── config │ │ └── config.go │ ├── github │ │ ├── client.go │ │ └── github.go │ ├── modules │ │ ├── repo.go │ │ ├── types.go │ │ └── versions.go │ ├── platform │ │ ├── platform.go │ │ └── platform_test.go │ ├── providers │ │ ├── RiskIdent │ │ │ └── 5180E94C4E6D9709.asc │ │ ├── errors.go │ │ ├── keys.go │ │ ├── keys │ │ │ ├── Ferlab-Ste-Justine │ │ │ │ └── 20211111.asc │ │ │ ├── claranet │ │ │ │ ├── 20230515-argocd.asc │ │ │ │ └── 20230515-rke.asc │ │ │ ├── gitlabhq │ │ │ │ └── 20220911.asc │ │ │ ├── opentofu │ │ │ │ ├── 20230928.asc │ │ │ │ └── 20231115-providers.asc │ │ │ ├── oracle │ │ │ │ └── 20230921.asc │ │ │ ├── scalr │ │ │ │ └── 20210514.asc │ │ │ ├── spacelift-io │ │ │ │ └── 20230908.asc │ │ │ └── umich-vci │ │ │ │ └── 20201014.asc │ │ ├── keys_test.go │ │ ├── manifest.go │ │ ├── providercache │ │ │ ├── fetch.go │ │ │ ├── handler.go │ │ │ └── store.go │ │ ├── repo.go │ │ ├── types │ │ │ ├── types.go │ │ │ └── types_test.go │ │ ├── utils.go │ │ ├── utils_test.go │ │ └── versions.go │ ├── secrets │ │ └── secretsmanager.go │ └── warnings │ │ ├── warnings.go │ │ └── warnings_test.go └── lambda │ ├── api │ ├── main.go │ ├── moduleDownload.go │ ├── moduleVersions.go │ ├── providerDownload.go │ ├── providerVersions.go │ ├── responses.go │ ├── router.go │ └── terraformWellKnown.go │ └── populate_provider_versions │ ├── handler.go │ └── main.go └── variables.tf /.github/publickey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoMR5oyPq7dzTT67lipV0 3 | zrPf6MfjRkzF8aaCDxmJtND+vrVGyYV7S9d/fU6syt7Beodhop6DD/HeVskC8agV 4 | 1pQkFrWtgKfNyC9rb9Q6M0XfbLQCR4X1lglpA2cQEiu3qd/febUKHVClH1FTTRhH 5 | s/LCtFf4mwfOfx1gJ6kTPSub7/ZKXzoQEZR616Irs6ZyQsMv080T3sSgllR/jRtM 6 | kYIcKcNpzzMnlAgOYzj1h2HfLdF9eTbA2RkEMbOLl4T1pxCxZg33HsMMzCHyNT3n 7 | MWbq3n3fH5fVAB4bT86nlPtCX8pO14z/1b3oHai1ksfI/3j6uO12Hj3clPKalp7K 8 | CwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /.github/workflows/dev-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Dev Deployment 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: ${{ github.ref }}-dev-deploy 10 | 11 | jobs: 12 | plan: 13 | name: Plan 14 | runs-on: ubuntu-latest 15 | environment: dev-plan 16 | outputs: 17 | tfstatus: ${{ steps.plan.outputs.exitcode }} 18 | permissions: 19 | id-token: write 20 | contents: read 21 | env: 22 | TF_IN_AUTOMATION: true 23 | 24 | # Note: These 3 secrets below are configured as github environment secrets 25 | # and not as repository secrets. This allows the usage of consistent names 26 | # for the secrets across all workflows. 27 | TF_VAR_github_api_token: ${{ secrets.REGISTRY_GITHUB_TOKEN }} 28 | TF_VAR_route53_zone_id: ${{ secrets.REGISTRY_ZONE_ID }} 29 | TF_VAR_domain_name: ${{ secrets.REGISTRY_DOMAIN }} 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | 34 | - uses: hashicorp/setup-terraform@v2 35 | with: 36 | terraform_version: 1.5.6 37 | 38 | - name: Configure AWS Credentials 39 | uses: aws-actions/configure-aws-credentials@v4 40 | with: 41 | # A role can be created by following the documentation here: 42 | # https://github.com/aws-actions/configure-aws-credentials#sample-iam-oidc-cloudformation-template 43 | role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} 44 | aws-region: ${{ secrets.AWS_REGION }} 45 | 46 | - name: Initialize Terraform 47 | run: terraform init 48 | 49 | - name: Select Terraform Workspace 50 | run: terraform workspace select dev 51 | 52 | - name: Plan changes 53 | id: plan 54 | run: terraform plan -input=false -detailed-exitcode -out=project.tfplan || true 55 | 56 | - name: Check for failure 57 | if: steps.plan.outputs.exitcode != 0 && steps.plan.outputs.exitcode != 2 58 | run: | 59 | echo "Terraform plan failed" 60 | exit 1 61 | 62 | - name: Show the plan 63 | run: terraform show project.tfplan 64 | 65 | - name: Generate the random password file 66 | if: steps.plan.outputs.exitcode == 2 67 | run: openssl rand -hex -out key.bin 64 68 | 69 | - name: Encrypt the plan file using the random key 70 | if: steps.plan.outputs.exitcode == 2 71 | run: openssl enc -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -salt -in project.tfplan -out project.tfplan.enc -pass file:./key.bin 72 | 73 | - name: Encrypt the random key with the public keyfile 74 | if: steps.plan.outputs.exitcode == 2 75 | run: openssl rsautl -encrypt -inkey .github/publickey.pem -pubin -in key.bin -out key.bin.enc 76 | 77 | - name: Archive encrypted artifacts 78 | if: steps.plan.outputs.exitcode == 2 79 | uses: actions/upload-artifact@v3 80 | with: 81 | name: artifacts 82 | retention-days: 2 83 | path: | 84 | project.tfplan.enc 85 | key.bin.enc 86 | artifacts 87 | 88 | apply: 89 | name: Apply 90 | needs: plan 91 | if: needs.plan.outputs.tfstatus == 2 92 | runs-on: ubuntu-latest 93 | environment: dev-apply 94 | permissions: 95 | id-token: write 96 | contents: read 97 | env: 98 | TF_IN_AUTOMATION: true 99 | 100 | TF_VAR_github_api_token: ${{ secrets.REGISTRY_GITHUB_TOKEN }} 101 | TF_VAR_route53_zone_id: ${{ secrets.REGISTRY_ZONE_ID }} 102 | TF_VAR_domain_name: ${{ secrets.REGISTRY_DOMAIN }} 103 | 104 | steps: 105 | - name: Check out repository code 106 | uses: actions/checkout@v4 107 | 108 | - name: Unarchive encrypted artifacts 109 | uses: actions/download-artifact@v3 110 | with: 111 | name: artifacts 112 | 113 | - name: Write the private key to a file 114 | run: echo $PRIVATE_KEY | base64 -d > .github/privatekey.pem 115 | env: 116 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 117 | 118 | - name: Decrypt the encrypted key 119 | run: openssl rsautl -decrypt -inkey .github/privatekey.pem -in key.bin.enc -out key.bin 120 | 121 | - name: Decrypt the plan file 122 | run: openssl enc -d -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -salt -in project.tfplan.enc -out project.tfplan -pass file:./key.bin 123 | 124 | - name: Configure AWS credentials 125 | uses: aws-actions/configure-aws-credentials@v4 126 | with: 127 | aws-region: eu-west-1 128 | role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} 129 | role-duration-seconds: 1800 130 | 131 | - name: Set up Terraform 132 | uses: hashicorp/setup-terraform@v2 133 | with: 134 | terraform_version: 1.5.6 135 | 136 | - name: Initialize Terraform 137 | run: terraform init 138 | 139 | - name: Select Terraform Workspace 140 | run: terraform workspace select dev 141 | 142 | - name: Apply changes 143 | run: terraform apply -input=false project.tfplan -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: actions/setup-go@v4 20 | with: { go-version-file: 'src/go.mod' } 21 | 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@v3 24 | with: 25 | version: v1.54 26 | working-directory: src 27 | args: --timeout=10m 28 | -------------------------------------------------------------------------------- /.github/workflows/prod-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Prod Deployment 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: ${{ github.ref }}-prod-deploy 10 | 11 | jobs: 12 | plan: 13 | name: Plan 14 | runs-on: ubuntu-latest 15 | environment: prod-plan 16 | outputs: 17 | tfstatus: ${{ steps.plan.outputs.exitcode }} 18 | permissions: 19 | id-token: write 20 | contents: read 21 | env: 22 | TF_IN_AUTOMATION: true 23 | 24 | # Note: These 3 secrets below are configured as github environment secrets 25 | # and not as repository secrets. This allows the usage of consistent names 26 | # for the secrets across all workflows. 27 | TF_VAR_github_api_token: ${{ secrets.REGISTRY_GITHUB_TOKEN }} 28 | TF_VAR_route53_zone_id: ${{ secrets.REGISTRY_ZONE_ID }} 29 | TF_VAR_domain_name: ${{ secrets.REGISTRY_DOMAIN }} 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | 34 | - uses: hashicorp/setup-terraform@v2 35 | with: 36 | terraform_version: 1.5.6 37 | 38 | - name: Configure AWS Credentials 39 | uses: aws-actions/configure-aws-credentials@v4 40 | with: 41 | # A role can be created by following the documentation here: 42 | # https://github.com/aws-actions/configure-aws-credentials#sample-iam-oidc-cloudformation-template 43 | role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} 44 | aws-region: ${{ secrets.AWS_REGION }} 45 | 46 | - name: Initialize Terraform 47 | run: terraform init 48 | 49 | - name: Select Terraform Workspace 50 | run: terraform workspace select prod 51 | 52 | - name: Plan changes 53 | id: plan 54 | run: terraform plan -input=false -detailed-exitcode -out=project.tfplan || true 55 | 56 | - name: Check for failure 57 | if: steps.plan.outputs.exitcode != 0 && steps.plan.outputs.exitcode != 2 58 | run: | 59 | echo "Terraform plan failed" 60 | exit 1 61 | 62 | - name: Show the plan 63 | run: terraform show project.tfplan 64 | 65 | - name: Generate the random password file 66 | if: steps.plan.outputs.exitcode == 2 67 | run: openssl rand -hex -out key.bin 64 68 | 69 | - name: Encrypt the plan file using the random key 70 | if: steps.plan.outputs.exitcode == 2 71 | run: openssl enc -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -salt -in project.tfplan -out project.tfplan.enc -pass file:./key.bin 72 | 73 | - name: Encrypt the random key with the public keyfile 74 | if: steps.plan.outputs.exitcode == 2 75 | run: openssl rsautl -encrypt -inkey .github/publickey.pem -pubin -in key.bin -out key.bin.enc 76 | 77 | - name: Archive encrypted artifacts 78 | if: steps.plan.outputs.exitcode == 2 79 | uses: actions/upload-artifact@v3 80 | with: 81 | name: artifacts 82 | retention-days: 2 83 | path: | 84 | project.tfplan.enc 85 | key.bin.enc 86 | artifacts 87 | 88 | apply: 89 | name: Apply 90 | needs: plan 91 | if: needs.plan.outputs.tfstatus == 2 92 | runs-on: ubuntu-latest 93 | environment: prod-apply 94 | permissions: 95 | id-token: write 96 | contents: read 97 | env: 98 | TF_IN_AUTOMATION: true 99 | 100 | TF_VAR_github_api_token: ${{ secrets.REGISTRY_GITHUB_TOKEN }} 101 | TF_VAR_route53_zone_id: ${{ secrets.REGISTRY_ZONE_ID }} 102 | TF_VAR_domain_name: ${{ secrets.REGISTRY_DOMAIN }} 103 | 104 | steps: 105 | - name: Check out repository code 106 | uses: actions/checkout@v4 107 | 108 | - name: Unarchive encrypted artifacts 109 | uses: actions/download-artifact@v3 110 | with: 111 | name: artifacts 112 | 113 | - name: Write the private key to a file 114 | run: echo $PRIVATE_KEY | base64 -d > .github/privatekey.pem 115 | env: 116 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 117 | 118 | - name: Decrypt the encrypted key 119 | run: openssl rsautl -decrypt -inkey .github/privatekey.pem -in key.bin.enc -out key.bin 120 | 121 | - name: Decrypt the plan file 122 | run: openssl enc -d -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -salt -in project.tfplan.enc -out project.tfplan -pass file:./key.bin 123 | 124 | - name: Configure AWS credentials 125 | uses: aws-actions/configure-aws-credentials@v4 126 | with: 127 | aws-region: eu-west-1 128 | role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} 129 | role-duration-seconds: 1800 130 | 131 | - name: Set up Terraform 132 | uses: hashicorp/setup-terraform@v2 133 | with: 134 | terraform_version: 1.5.6 135 | 136 | - name: Initialize Terraform 137 | run: terraform init 138 | 139 | - name: Select Terraform Workspace 140 | run: terraform workspace select prod 141 | 142 | - name: Apply changes 143 | run: terraform apply -input=false project.tfplan -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request_target: 7 | types: [opened, synchronize] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | name: Test 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | working-directory: src 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | 22 | - name: Install Go 23 | uses: actions/setup-go@v4 24 | with: { go-version-file: 'src/go.mod' } 25 | 26 | - name: Check formatting using gofmt 27 | run: gofmt -s -l -d . 28 | 29 | - name: Get dependencies 30 | run: go mod download 31 | 32 | - name: Run unit tests 33 | run: go test ./... 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | crash.*.log 11 | 12 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 13 | # password, private keys, and other secrets. These should not be part of version 14 | # control as they are data points which are potentially sensitive and subject 15 | # to change depending on the environment. 16 | *.tfvars 17 | *.tfvars.json 18 | 19 | # Ignore override files as they are usually used to override resources locally and so 20 | # are not checked in 21 | override.tf 22 | override.tf.json 23 | *_override.tf 24 | *_override.tf.json 25 | 26 | # Include override files you do wish to add to version control using negated pattern 27 | # !example_override.tf 28 | 29 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 30 | # example: *tfplan* 31 | 32 | # Ignore CLI configuration files 33 | .terraformrc 34 | terraform.rc 35 | 36 | # Build artifacts 37 | populate_provider_versions_bootstrap 38 | populate_provider_versions_bootstrap.zip 39 | 40 | api_function_bootstrap 41 | api_bootstrap.zip 42 | 43 | # JetBrains folder 44 | .idea 45 | -------------------------------------------------------------------------------- /.terraform-version: -------------------------------------------------------------------------------- 1 | 1.5.6 -------------------------------------------------------------------------------- /.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/archive" { 5 | version = "2.4.0" 6 | hashes = [ 7 | "h1:cJokkjeH1jfpG4QEHdRx0t2j8rr52H33A7C/oX73Ok4=", 8 | "zh:18e408596dd53048f7fc8229098d0e3ad940b92036a24287eff63e2caec72594", 9 | "zh:392d4216ecd1a1fd933d23f4486b642a8480f934c13e2cae3c13b6b6a7e34a7b", 10 | "zh:655dd1fa5ca753a4ace21d0de3792d96fff429445717f2ce31c125d19c38f3ff", 11 | "zh:70dae36c176aa2b258331ad366a471176417a94dd3b4985a911b8be9ff842b00", 12 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 13 | "zh:7d8c8e3925f1e21daf73f85983894fbe8868e326910e6df3720265bc657b9c9c", 14 | "zh:a032ec0f0aee27a789726e348e8ad20778c3a1c9190ef25e7cff602c8d175f44", 15 | "zh:b8e50de62ba185745b0fe9713755079ad0e9f7ac8638d204de6762cc36870410", 16 | "zh:c8ad0c7697a3d444df21ff97f3473a8604c8639be64afe3f31b8ec7ad7571e18", 17 | "zh:df736c5a2a7c3a82c5493665f659437a22f0baf8c2d157e45f4dd7ca40e739fc", 18 | "zh:e8ffbf578a0977074f6d08aa8734e36c726e53dc79894cfc4f25fadc4f45f1df", 19 | "zh:efea57ff23b141551f92b2699024d356c7ffd1a4ad62931da7ed7a386aef7f1f", 20 | ] 21 | } 22 | 23 | provider "registry.terraform.io/hashicorp/aws" { 24 | version = "5.15.0" 25 | hashes = [ 26 | "h1:CFUr3EXmKTr3G4Nl+Yxf24NnhKQQDCyeBG+SS4YFblE=", 27 | "zh:069d0037cd1f8791a27ec31a535ce47d02d4f220fe88f9c3caa8661c0a98892a", 28 | "zh:08c18e8f5f69736e86919e6c2a68c94f39f879511d51b2a8e58ad1776ee18854", 29 | "zh:41c9c95e225f72421fa4a1c3e5105f36b3b149cba1daf9bc88b0a993c1d19e07", 30 | "zh:51e6cf850de8a8ae0e3b4e55b45ca2e6632a149c5851158f3c2711af51adb277", 31 | "zh:5703eacc47d5a8169d1028f8cfcdf32cd12972ebea8780e870f520020280258a", 32 | "zh:6a77e0406126208ae217c416e4b59940cd989df4d7d5ac23dfe8043725ff8f6a", 33 | "zh:702cc6db865aeee571a639a81be3ed36326dcbda5c0a2ca91c9280772fce3e49", 34 | "zh:8279822c5a267869d4459e429ad7b3b8ffaa36de2f6ca29cf7779214783ddf3a", 35 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 36 | "zh:bcb74854b0742a03b46e526bc2a79f556988c7622d54ebb2ccefc72c9759e9bc", 37 | "zh:c7b0f4e94a9351a004a5555e91c8fe5b7da8cd2e03411cbd59d135ea8fceedd8", 38 | "zh:cec427b1ef0e0948fd16736c72de57438fafcd8eeb5aab3bb1131579d2d6d031", 39 | "zh:d5e4819851e52c15283064f6fa8cb8179a69cc981bee39e9b5ce5f027da8e251", 40 | "zh:dade91d49309813b7453b053429678c8e7185e5ac54b2f68edb2ffea20242149", 41 | "zh:e05e1395a738317a6761b592a5643ea5e660abd32de36ece68809cfd04a6a8e3", 42 | ] 43 | } 44 | 45 | provider "registry.terraform.io/hashicorp/null" { 46 | version = "3.2.1" 47 | hashes = [ 48 | "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", 49 | "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", 50 | "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", 51 | "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", 52 | "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", 53 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 54 | "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", 55 | "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", 56 | "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", 57 | "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", 58 | "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", 59 | "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", 60 | "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Registry 2 | 3 | A Simple set of Golang AWS Lambdas and supporting infrastructure definitions to act as a simple, stateless registry that sits on top of github repositories. 4 | 5 | This repository and code is meant to be temporary and is not intended for long term usage. 6 | 7 | ## Table of Contents 8 | 9 | - [Table of Contents](#table-of-contents) 10 | - [Registering public keys](#registering-public-keys) 11 | - [Adding a public key](#adding-a-public-key) 12 | - [Removing a public key](#removing-a-public-key) 13 | - [Contributing to the project](#contributing-to-the-project) 14 | - [Requirements](#requirements) 15 | - [Setup](#setup) 16 | - [Terraform Variables Configuration](#terraform-variables-configuration) 17 | - [Deployment](#deployment) 18 | - [DNS Configuration](#dns-configuration) 19 | - [API Routes and Curl Usage](#api-routes-and-curl-usage) 20 | - [License](#license) 21 | 22 | ## Registering public keys 23 | 24 | This section describes how to register public keys for the providers and is intended for authors of providers who want the users of their providers to be able to verify the authenticity of the provider binaries. 25 | 26 | ### Adding a public key 27 | 28 | All keys are stored in the `src/internal/providers/keys` directory. That directory contains subdirectories, each of which is named after the GitHub namespace (username or organization name) that hosts one or more providers. 29 | 30 | Inside that directory are one or more ASCII-armored public key files. The names of the files are not relevant to the registry code, but it is recommended that they have a `.asc` extension. It may also be a good idea to name the files using the registration date to make it easier for the reader to determine which key is the most recent. The contents of the file should be the ASCII-armored public key for the namespace. 31 | 32 | When the user requests any provider in a given namespace, the registry will return all the registered public keys for that namespace. The user can then use these keys to verify the signature of the provider binary. 33 | 34 | ### Removing a public key 35 | 36 | It is possible to remove a public key from the registry. To do so, simply delete the corresponding file from the `lambda/internal/provider/keys` directory. The next time the registry is deployed, the key will no longer be available. 37 | 38 | This will however have an impact on the users of the provider, which will no longer be able to verify the authenticity of the provider binaries. In case of a leak it is thus recommended to re-sign all the provider binaries with a new key, and to register the new key in the registry. 39 | 40 | ## Contributing to the project 41 | 42 | ** NOTE **: This project is still in development and is not yet accepting contributions. Please check back later. 43 | 44 | ### Requirements 45 | 46 | - **Go**: The AWS Lambda function is written in Go. Ensure you have Go installed. 47 | - **Terraform**: This project uses Terraform for infrastructure management. Make sure to have Terraform installed. 48 | - **AWS CLI**: Ensure that the AWS CLI is installed and configured with the necessary permissions. 49 | 50 | ### Setup 51 | 52 | 1. **Clone the Repository**: 53 | 54 | ```bash 55 | git clone 56 | cd 57 | ``` 58 | 59 | 2. **Set Up Go**: 60 | Navigate to the `src` directory and download the required Go modules. 61 | 62 | ```bash 63 | cd src 64 | go mod download 65 | ``` 66 | 67 | 3. **Initialize Terraform**: 68 | From the root of the project: 69 | 70 | ```bash 71 | terraform init 72 | ``` 73 | 74 | ### Terraform Variables Configuration 75 | 76 | Before deploying the infrastructure, ensure you've set the required Terraform variables: 77 | 78 | - **`github_api_token`**: Personal Access Token (PAT) from GitHub, required for interactions with the GitHub API. [Create a GitHub PAT](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) if you don't have one, it should have `public_repo, read:packages` access 79 | 80 | - **`route53_zone_id`**: a Route 53 hosted zone pre-configured with NS records pointing to a valid registered domain, e.g., "Z008B5091482A026MN9AUQ" 81 | 82 | - **`domain_name`**: The domain name you wish to manage. This should match or be a subdomain of the `route53_zone_name`. 83 | 84 | To provide values for these variables: 85 | 86 | - Use the `-var` flag during `terraform apply`, e.g., `terraform apply -var="github_api_token=YOUR_TOKEN"`. 87 | - Or, populate a `terraform.tfvars` file in the repository root: 88 | 89 | ```hcl 90 | github_api_token = "YOUR_GITHUB_API_TOKEN" 91 | route53_zone_id = "Z008ABCDEF482A026MN9AUQ" 92 | domain_name = "sub.example.com" 93 | ``` 94 | 95 | **Important**: Never commit sensitive data, especially the `github_api_token`, to your repository. Ensure secrets are managed securely. 96 | 97 | ### Deployment 98 | 99 | 1. **Setting Up AWS Credentials**: 100 | Ensure your AWS credentials are properly set up, either by using the `aws configure` command or by setting the necessary environment variables. 101 | 102 | 2. **Terraform Commands**: 103 | From the root of the project: 104 | 105 | a. **Planning**: 106 | 107 | ```bash 108 | terraform plan 109 | ``` 110 | 111 | b. **Deploying Infrastructure and Lambda**: 112 | 113 | ```bash 114 | terraform apply 115 | ``` 116 | 117 | Note: When you run `terraform apply`, Terraform will take care of building the Lambda function from the Go source code and deploying it to AWS. 118 | 119 | ### DNS Configuration 120 | 121 | After successfully applying the Terraform configuration, you will receive an output containing four nameservers. These nameservers are associated with the AWS Route 53 DNS settings for your service. 122 | 123 | To complete the setup, you need to configure a subdomain to use these four nameservers. Update your domain provider's DNS settings to point the subdomain to these nameservers. 124 | 125 | Ensure that you update the DNS settings in your domain provider's dashboard to use these nameservers for the relevant subdomain. 126 | 127 | ### API Routes and Curl Usage 128 | 129 | This project provides several routes that can be accessed and tested using the `curl` command. Here's a brief guide: 130 | 131 | 1. **Download Provider Version**: 132 | 133 | ```bash 134 | curl -X GET https:///v1/providers/{namespace}/{type}/{version}/download/{os}/{arch} 135 | ``` 136 | 137 | 2. **List Provider Versions**: 138 | 139 | ```bash 140 | curl -X GET https:///v1/providers/{namespace}/{type}/versions 141 | ``` 142 | 143 | 3. **List Module Versions**: 144 | 145 | ```bash 146 | curl -X GET https:///v1/modules/{namespace}/{name}/{system}/versions 147 | ``` 148 | 149 | 4. **Download Module Version**: 150 | 151 | ```bash 152 | curl -X GET https:///v1/modules/{namespace}/{name}/{system}/{version}/download 153 | ``` 154 | 155 | 5. **Terraform Well-Known Metadata**: 156 | 157 | ```bash 158 | curl -X GET https:///.well-known/terraform.json 159 | ``` 160 | 161 | Replace `` with the actual domain where your service is hosted. For dynamic parts of the route, such as `{namespace}` or `{type}`, replace them with appropriate values as per your requirements. 162 | 163 | ## License 164 | 165 | This project is licensed under the terms of the [LICENSE](LICENSE) file. 166 | 167 | --- 168 | 169 | For any additional queries or issues, please open a new issue in the repository. 170 | -------------------------------------------------------------------------------- /apigw.tf: -------------------------------------------------------------------------------- 1 | resource "aws_api_gateway_rest_api" "api" { 2 | name = "${var.domain_name}-opentofu-registry" 3 | description = "API Gateway for the OpenTofu Registry" 4 | } 5 | 6 | resource "aws_api_gateway_resource" "github" { 7 | rest_api_id = aws_api_gateway_rest_api.api.id 8 | parent_id = aws_api_gateway_rest_api.api.root_resource_id 9 | path_part = "github" 10 | } 11 | 12 | resource "aws_api_gateway_resource" "github_graphql_proxy" { 13 | rest_api_id = aws_api_gateway_rest_api.api.id 14 | parent_id = aws_api_gateway_resource.github.id 15 | path_part = "graphql" 16 | } 17 | 18 | resource "aws_api_gateway_resource" "github_rest" { 19 | rest_api_id = aws_api_gateway_rest_api.api.id 20 | parent_id = aws_api_gateway_resource.github.id 21 | path_part = "rest" 22 | } 23 | 24 | resource "aws_api_gateway_resource" "github_rest_proxy" { 25 | rest_api_id = aws_api_gateway_rest_api.api.id 26 | parent_id = aws_api_gateway_resource.github_rest.id 27 | path_part = "{proxy+}" 28 | } 29 | 30 | resource "aws_api_gateway_resource" "well_known" { 31 | rest_api_id = aws_api_gateway_rest_api.api.id 32 | parent_id = aws_api_gateway_rest_api.api.root_resource_id 33 | path_part = ".well-known" 34 | } 35 | 36 | resource "aws_api_gateway_resource" "terraform_json" { 37 | rest_api_id = aws_api_gateway_rest_api.api.id 38 | parent_id = aws_api_gateway_resource.well_known.id 39 | path_part = "terraform.json" 40 | } 41 | 42 | resource "aws_api_gateway_resource" "v1_resource" { 43 | rest_api_id = aws_api_gateway_rest_api.api.id 44 | parent_id = aws_api_gateway_rest_api.api.root_resource_id 45 | path_part = "v1" 46 | } 47 | 48 | resource "aws_api_gateway_resource" "providers_resource" { 49 | rest_api_id = aws_api_gateway_rest_api.api.id 50 | parent_id = aws_api_gateway_resource.v1_resource.id 51 | path_part = "providers" 52 | } 53 | 54 | resource "aws_api_gateway_resource" "namespace_resource" { 55 | rest_api_id = aws_api_gateway_rest_api.api.id 56 | parent_id = aws_api_gateway_resource.providers_resource.id 57 | path_part = "{namespace}" 58 | } 59 | 60 | resource "aws_api_gateway_resource" "provider_type_resource" { 61 | rest_api_id = aws_api_gateway_rest_api.api.id 62 | parent_id = aws_api_gateway_resource.namespace_resource.id 63 | path_part = "{type}" 64 | } 65 | 66 | resource "aws_api_gateway_resource" "provider_versions_resource" { 67 | rest_api_id = aws_api_gateway_rest_api.api.id 68 | parent_id = aws_api_gateway_resource.provider_type_resource.id 69 | path_part = "versions" 70 | } 71 | 72 | resource "aws_api_gateway_resource" "provider_version_resource" { 73 | rest_api_id = aws_api_gateway_rest_api.api.id 74 | parent_id = aws_api_gateway_resource.provider_type_resource.id 75 | path_part = "{version}" 76 | } 77 | 78 | resource "aws_api_gateway_resource" "provider_download_resource" { 79 | rest_api_id = aws_api_gateway_rest_api.api.id 80 | parent_id = aws_api_gateway_resource.provider_version_resource.id 81 | path_part = "download" 82 | } 83 | 84 | resource "aws_api_gateway_resource" "provider_os_resource" { 85 | rest_api_id = aws_api_gateway_rest_api.api.id 86 | parent_id = aws_api_gateway_resource.provider_download_resource.id 87 | path_part = "{os}" 88 | } 89 | 90 | resource "aws_api_gateway_resource" "provider_arch_resource" { 91 | rest_api_id = aws_api_gateway_rest_api.api.id 92 | parent_id = aws_api_gateway_resource.provider_os_resource.id 93 | path_part = "{arch}" 94 | } 95 | 96 | resource "aws_api_gateway_resource" "modules_resource" { 97 | rest_api_id = aws_api_gateway_rest_api.api.id 98 | parent_id = aws_api_gateway_resource.v1_resource.id 99 | path_part = "modules" 100 | } 101 | 102 | resource "aws_api_gateway_resource" "modules_namespace_resource" { 103 | rest_api_id = aws_api_gateway_rest_api.api.id 104 | parent_id = aws_api_gateway_resource.modules_resource.id 105 | path_part = "{namespace}" 106 | } 107 | 108 | resource "aws_api_gateway_resource" "modules_name_resource" { 109 | rest_api_id = aws_api_gateway_rest_api.api.id 110 | parent_id = aws_api_gateway_resource.modules_namespace_resource.id 111 | path_part = "{name}" 112 | } 113 | 114 | resource "aws_api_gateway_resource" "modules_system_resource" { 115 | rest_api_id = aws_api_gateway_rest_api.api.id 116 | parent_id = aws_api_gateway_resource.modules_name_resource.id 117 | path_part = "{system}" 118 | } 119 | 120 | resource "aws_api_gateway_resource" "module_version_resource" { 121 | rest_api_id = aws_api_gateway_rest_api.api.id 122 | parent_id = aws_api_gateway_resource.modules_system_resource.id 123 | path_part = "{version}" 124 | } 125 | 126 | resource "aws_api_gateway_resource" "module_download_resource" { 127 | rest_api_id = aws_api_gateway_rest_api.api.id 128 | parent_id = aws_api_gateway_resource.module_version_resource.id 129 | path_part = "download" 130 | } 131 | 132 | resource "aws_api_gateway_resource" "module_versions_resource" { 133 | rest_api_id = aws_api_gateway_rest_api.api.id 134 | parent_id = aws_api_gateway_resource.modules_system_resource.id 135 | path_part = "versions" 136 | } 137 | 138 | resource "aws_api_gateway_method" "provider_download_method" { 139 | rest_api_id = aws_api_gateway_rest_api.api.id 140 | resource_id = aws_api_gateway_resource.provider_arch_resource.id 141 | http_method = "GET" 142 | authorization = "NONE" 143 | 144 | request_parameters = { 145 | "method.request.path.namespace" = true, 146 | "method.request.path.type" = true, 147 | "method.request.path.version" = true, 148 | "method.request.path.os" = true, 149 | "method.request.path.arch" = true, 150 | } 151 | } 152 | 153 | resource "aws_api_gateway_integration" "provider_download_integration" { 154 | rest_api_id = aws_api_gateway_rest_api.api.id 155 | resource_id = aws_api_gateway_resource.provider_arch_resource.id 156 | http_method = aws_api_gateway_method.provider_download_method.http_method 157 | 158 | integration_http_method = "POST" 159 | type = "AWS_PROXY" 160 | uri = aws_lambda_function.api_function.invoke_arn 161 | 162 | cache_key_parameters = [ 163 | "method.request.path.namespace", 164 | "method.request.path.type", 165 | "method.request.path.version", 166 | "method.request.path.os", 167 | "method.request.path.arch", 168 | ] 169 | } 170 | 171 | resource "aws_api_gateway_method" "provider_list_versions_method" { 172 | rest_api_id = aws_api_gateway_rest_api.api.id 173 | resource_id = aws_api_gateway_resource.provider_versions_resource.id 174 | http_method = "GET" 175 | authorization = "NONE" 176 | 177 | request_parameters = { 178 | "method.request.path.namespace" = true, 179 | "method.request.path.type" = true, 180 | } 181 | } 182 | 183 | resource "aws_api_gateway_integration" "provider_list_versions_integration" { 184 | rest_api_id = aws_api_gateway_rest_api.api.id 185 | resource_id = aws_api_gateway_resource.provider_versions_resource.id 186 | http_method = aws_api_gateway_method.provider_list_versions_method.http_method 187 | 188 | integration_http_method = "POST" 189 | type = "AWS_PROXY" 190 | uri = aws_lambda_function.api_function.invoke_arn 191 | 192 | cache_key_parameters = [ 193 | "method.request.path.namespace", 194 | "method.request.path.type", 195 | ] 196 | } 197 | 198 | resource "aws_api_gateway_method" "module_download_method" { 199 | rest_api_id = aws_api_gateway_rest_api.api.id 200 | resource_id = aws_api_gateway_resource.module_download_resource.id 201 | http_method = "GET" 202 | authorization = "NONE" 203 | 204 | request_parameters = { 205 | "method.request.path.namespace" = true, 206 | "method.request.path.name" = true, 207 | "method.request.path.system" = true, 208 | "method.request.path.version" = true, 209 | } 210 | } 211 | 212 | resource "aws_api_gateway_integration" "module_download_integration" { 213 | rest_api_id = aws_api_gateway_rest_api.api.id 214 | resource_id = aws_api_gateway_resource.module_download_resource.id 215 | http_method = aws_api_gateway_method.module_download_method.http_method 216 | 217 | integration_http_method = "POST" 218 | type = "AWS_PROXY" 219 | uri = aws_lambda_function.api_function.invoke_arn 220 | 221 | cache_key_parameters = [ 222 | "method.request.path.namespace", 223 | "method.request.path.name", 224 | "method.request.path.system", 225 | "method.request.path.version", 226 | ] 227 | } 228 | 229 | resource "aws_api_gateway_method" "module_list_versions_method" { 230 | rest_api_id = aws_api_gateway_rest_api.api.id 231 | resource_id = aws_api_gateway_resource.module_versions_resource.id 232 | http_method = "GET" 233 | authorization = "NONE" 234 | 235 | request_parameters = { 236 | "method.request.path.namespace" = true, 237 | "method.request.path.name" = true, 238 | "method.request.path.system" = true, 239 | } 240 | } 241 | 242 | resource "aws_api_gateway_integration" "module_list_versions_integration" { 243 | rest_api_id = aws_api_gateway_rest_api.api.id 244 | resource_id = aws_api_gateway_resource.module_versions_resource.id 245 | http_method = aws_api_gateway_method.module_list_versions_method.http_method 246 | 247 | integration_http_method = "POST" 248 | type = "AWS_PROXY" 249 | uri = aws_lambda_function.api_function.invoke_arn 250 | 251 | cache_key_parameters = [ 252 | "method.request.path.namespace", 253 | "method.request.path.name", 254 | "method.request.path.system", 255 | ] 256 | } 257 | 258 | resource "aws_api_gateway_method" "metadata_method" { 259 | rest_api_id = aws_api_gateway_rest_api.api.id 260 | resource_id = aws_api_gateway_resource.terraform_json.id 261 | http_method = "GET" 262 | authorization = "NONE" 263 | } 264 | 265 | resource "aws_api_gateway_integration" "metadata_integration" { 266 | rest_api_id = aws_api_gateway_rest_api.api.id 267 | resource_id = aws_api_gateway_resource.terraform_json.id 268 | http_method = aws_api_gateway_method.metadata_method.http_method 269 | 270 | integration_http_method = "POST" 271 | type = "AWS_PROXY" 272 | uri = aws_lambda_function.api_function.invoke_arn 273 | } 274 | 275 | resource "aws_api_gateway_method" "github_rest_method" { 276 | rest_api_id = aws_api_gateway_rest_api.api.id 277 | resource_id = aws_api_gateway_resource.github_rest_proxy.id 278 | http_method = "GET" 279 | authorization = "NONE" 280 | 281 | request_parameters = { 282 | "method.request.path.proxy" = true, 283 | "method.request.header.Authorization" = true 284 | } 285 | } 286 | 287 | resource "aws_api_gateway_integration" "github_rest_integration" { 288 | rest_api_id = aws_api_gateway_rest_api.api.id 289 | resource_id = aws_api_gateway_resource.github_rest_proxy.id 290 | http_method = aws_api_gateway_method.github_rest_method.http_method 291 | 292 | integration_http_method = "GET" 293 | type = "HTTP_PROXY" 294 | uri = "https://api.github.com/{proxy}" 295 | 296 | request_parameters = { 297 | "integration.request.path.proxy" = "method.request.path.proxy" 298 | } 299 | 300 | cache_key_parameters = [ 301 | "method.request.path.proxy" 302 | ] 303 | } 304 | 305 | resource "aws_api_gateway_method" "github_graphql_method" { 306 | rest_api_id = aws_api_gateway_rest_api.api.id 307 | resource_id = aws_api_gateway_resource.github_graphql_proxy.id 308 | http_method = "POST" 309 | authorization = "NONE" 310 | 311 | request_parameters = { 312 | "method.request.header.Authorization" = true 313 | } 314 | } 315 | 316 | resource "aws_api_gateway_integration" "github_graphql_integration" { 317 | rest_api_id = aws_api_gateway_rest_api.api.id 318 | resource_id = aws_api_gateway_resource.github_graphql_proxy.id 319 | http_method = aws_api_gateway_method.github_graphql_method.http_method 320 | 321 | integration_http_method = "POST" 322 | type = "HTTP_PROXY" 323 | uri = "https://api.github.com/graphql" 324 | 325 | request_parameters = { 326 | "integration.request.header.body" = "method.request.body" 327 | } 328 | 329 | cache_key_parameters = [ 330 | "integration.request.header.body" 331 | ] 332 | } 333 | 334 | 335 | resource "aws_api_gateway_deployment" "deployment" { 336 | depends_on = [ 337 | aws_api_gateway_method.provider_download_method, 338 | aws_api_gateway_integration.provider_download_integration, 339 | 340 | aws_api_gateway_method.provider_list_versions_method, 341 | aws_api_gateway_integration.provider_list_versions_integration, 342 | 343 | aws_api_gateway_method.module_download_method, 344 | aws_api_gateway_integration.module_download_integration, 345 | 346 | aws_api_gateway_method.module_list_versions_method, 347 | aws_api_gateway_integration.module_list_versions_integration, 348 | 349 | aws_api_gateway_method.metadata_method, 350 | aws_api_gateway_integration.metadata_integration, 351 | 352 | aws_api_gateway_method.github_rest_method, 353 | aws_api_gateway_integration.github_rest_integration, 354 | 355 | aws_api_gateway_method.github_graphql_method, 356 | aws_api_gateway_integration.github_graphql_integration 357 | ] 358 | rest_api_id = aws_api_gateway_rest_api.api.id 359 | 360 | triggers = { 361 | # Ensure that redeployment happens every time 362 | redeployment = timestamp() 363 | } 364 | 365 | lifecycle { 366 | create_before_destroy = true 367 | } 368 | } 369 | 370 | resource "aws_cloudwatch_log_group" "apigw_log_group" { 371 | name = "/aws/lambda/${replace(var.domain_name, ".", "-")}-apigw" 372 | retention_in_days = 7 373 | } 374 | 375 | resource "aws_api_gateway_stage" "stage" { 376 | deployment_id = aws_api_gateway_deployment.deployment.id 377 | rest_api_id = aws_api_gateway_rest_api.api.id 378 | stage_name = "${replace(var.domain_name, ".", "-")}-opentofu-registry" 379 | 380 | xray_tracing_enabled = true 381 | 382 | 383 | access_log_settings { 384 | destination_arn = aws_cloudwatch_log_group.apigw_log_group.arn 385 | format = "{ \"requestId\":\"$context.requestId\", \"ip\": \"$context.identity.sourceIp\", \"caller\":\"$context.identity.caller\", \"user\":\"$context.identity.user\",\"requestTime\":\"$context.requestTime\", \"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\", \"status\":\"$context.status\",\"protocol\":\"$context.protocol\", \"responseLength\":\"$context.responseLength\" }" 386 | } 387 | 388 | cache_cluster_enabled = true 389 | cache_cluster_size = "0.5" 390 | } 391 | 392 | 393 | resource "aws_api_gateway_method_settings" "download_method_settings" { 394 | rest_api_id = aws_api_gateway_rest_api.api.id 395 | stage_name = aws_api_gateway_stage.stage.stage_name 396 | 397 | # This encodes `/` as `~1` to provide the correct path for the method 398 | method_path = "~1v1~1providers~1{namespace}~1{type}~1{version}~1download~1{os}~1{arch}/GET" 399 | 400 | settings { 401 | metrics_enabled = true 402 | logging_level = "INFO" 403 | data_trace_enabled = true 404 | caching_enabled = true 405 | // 60 minutes to keep it consistent with the provider versions cache TTL 406 | cache_ttl_in_seconds = (60 * 60) 407 | require_authorization_for_cache_control = false 408 | } 409 | } 410 | 411 | resource "aws_api_gateway_method_settings" "provider_list_versions_method_settings" { 412 | rest_api_id = aws_api_gateway_rest_api.api.id 413 | stage_name = aws_api_gateway_stage.stage.stage_name 414 | 415 | # This encodes `/` as `~1` to provide the correct path for the method 416 | method_path = "~1v1~1providers~1{namespace}~1{type}~1versions/GET" 417 | 418 | settings { 419 | metrics_enabled = true 420 | logging_level = "INFO" 421 | data_trace_enabled = true 422 | caching_enabled = true 423 | // 60 minutes, to ensure we're over the (current) one hour limit of backend cache TTL 424 | cache_ttl_in_seconds = (60 * 60) 425 | require_authorization_for_cache_control = false 426 | } 427 | } 428 | 429 | resource "aws_api_gateway_method_settings" "module_download_method_settings" { 430 | rest_api_id = aws_api_gateway_rest_api.api.id 431 | stage_name = aws_api_gateway_stage.stage.stage_name 432 | 433 | # This encodes `/` as `~1` to provide the correct path for the method 434 | method_path = "~1v1~modules~1{namespace}~1{name}~1{system}~1{version}~1download/GET" 435 | 436 | settings { 437 | metrics_enabled = true 438 | logging_level = "INFO" 439 | data_trace_enabled = true 440 | caching_enabled = true 441 | // 60 minutes to keep it consistent with the provider versions cache TTL 442 | cache_ttl_in_seconds = (60 * 60) 443 | require_authorization_for_cache_control = false 444 | } 445 | } 446 | 447 | resource "aws_api_gateway_method_settings" "module_list_versions_method_settings" { 448 | rest_api_id = aws_api_gateway_rest_api.api.id 449 | stage_name = aws_api_gateway_stage.stage.stage_name 450 | 451 | # This encodes `/` as `~1` to provide the correct path for the method 452 | method_path = "~1v1~modules~1{namespace}~1{name}~1{system}~1versions/GET" 453 | 454 | settings { 455 | metrics_enabled = true 456 | logging_level = "INFO" 457 | data_trace_enabled = true 458 | caching_enabled = true 459 | // 60 minutes to keep it consistent with the provider versions cache TTL 460 | cache_ttl_in_seconds = (60 * 60) 461 | require_authorization_for_cache_control = false 462 | } 463 | } 464 | 465 | resource "aws_api_gateway_method_settings" "well_known_method_settings" { 466 | rest_api_id = aws_api_gateway_rest_api.api.id 467 | stage_name = aws_api_gateway_stage.stage.stage_name 468 | 469 | # This encodes `/` as `~1` to provide the correct path for the method 470 | method_path = ".well-known~1terraform.json/GET" 471 | 472 | settings { 473 | metrics_enabled = true 474 | logging_level = "INFO" 475 | data_trace_enabled = true 476 | caching_enabled = true 477 | // 60 minutes to keep it consistent with the provider versions cache TTL 478 | cache_ttl_in_seconds = (60 * 60) 479 | require_authorization_for_cache_control = false 480 | } 481 | } 482 | 483 | resource "aws_api_gateway_method_settings" "github_rest_method_settings" { 484 | rest_api_id = aws_api_gateway_rest_api.api.id 485 | stage_name = aws_api_gateway_stage.stage.stage_name 486 | 487 | # This encodes `/` as `~1` to provide the correct path for the method 488 | method_path = "~1github~1rest~1/GET" 489 | 490 | settings { 491 | metrics_enabled = true 492 | logging_level = "INFO" 493 | data_trace_enabled = true 494 | caching_enabled = true 495 | // 50 minutes to keep it consistent with the other caching layers' TTL 496 | cache_ttl_in_seconds = (50 * 60) 497 | require_authorization_for_cache_control = false 498 | } 499 | } 500 | 501 | resource "aws_api_gateway_method_settings" "github_graphql_method_settings" { 502 | rest_api_id = aws_api_gateway_rest_api.api.id 503 | stage_name = aws_api_gateway_stage.stage.stage_name 504 | 505 | # This encodes `/` as `~1` to provide the correct path for the method 506 | method_path = "~1github~1graphql~1{proxy}/POST" 507 | 508 | settings { 509 | metrics_enabled = true 510 | logging_level = "INFO" 511 | data_trace_enabled = true 512 | caching_enabled = true 513 | // 50 minutes to keep it consistent with the other caching layers' TTL 514 | cache_ttl_in_seconds = (50 * 60) 515 | require_authorization_for_cache_control = false 516 | } 517 | } 518 | 519 | resource "aws_api_gateway_domain_name" "domain" { 520 | domain_name = var.domain_name 521 | certificate_arn = aws_acm_certificate.api.arn 522 | 523 | depends_on = [aws_acm_certificate_validation.api] 524 | } 525 | 526 | resource "aws_api_gateway_base_path_mapping" "base_path_mapping" { 527 | api_id = aws_api_gateway_rest_api.api.id 528 | stage_name = aws_api_gateway_stage.stage.stage_name 529 | domain_name = aws_api_gateway_domain_name.domain.domain_name 530 | } 531 | 532 | 533 | output "base_url" { 534 | description = "Base URL for API Gateway stage." 535 | value = "https://${aws_api_gateway_rest_api.api.id}.execute-api.${var.region}.amazonaws.com/${aws_api_gateway_stage.stage.stage_name}/" 536 | } 537 | -------------------------------------------------------------------------------- /dynamo.tf: -------------------------------------------------------------------------------- 1 | resource "aws_dynamodb_table" "provider_versions" { 2 | name = "${var.domain_name}-provider-versions" 3 | billing_mode = "PAY_PER_REQUEST" 4 | 5 | hash_key = "provider" 6 | 7 | attribute { 8 | name = "provider" 9 | type = "S" 10 | } 11 | } -------------------------------------------------------------------------------- /iam.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "assume_lambda_role" { 2 | statement { 3 | actions = ["sts:AssumeRole"] 4 | 5 | principals { 6 | type = "Service" 7 | identifiers = [ 8 | "apigateway.amazonaws.com", 9 | "lambda.amazonaws.com" 10 | ] 11 | } 12 | } 13 | } 14 | 15 | data "aws_iam_policy_document" "github_api_token_secrets_iam_policy" { 16 | statement { 17 | effect = "Allow" 18 | actions = [ 19 | "secretsmanager:GetSecretValue", 20 | ] 21 | 22 | resources = [ 23 | aws_secretsmanager_secret.github_api_token.arn, 24 | ] 25 | } 26 | } 27 | 28 | resource "aws_iam_policy" "lambda_secrets_policy" { 29 | name = "${var.domain_name}-RegistryLambdaSecretsPolicy" 30 | description = "Policy for lambda to pull its secrets" 31 | policy = data.aws_iam_policy_document.github_api_token_secrets_iam_policy.json 32 | } 33 | 34 | resource "aws_iam_role_policy_attachment" "lambda_secrets_policy_attachment" { 35 | role = aws_iam_role.lambda.id 36 | policy_arn = aws_iam_policy.lambda_secrets_policy.arn 37 | } 38 | 39 | resource "aws_iam_role" "lambda" { 40 | name = "${var.domain_name}-RegistryLambdaRole" 41 | description = "Role for the registry to assume lambda" 42 | assume_role_policy = data.aws_iam_policy_document.assume_lambda_role.json 43 | } 44 | 45 | data "aws_iam_policy_document" "allow_lambda_logging" { 46 | # Allow CloudWatch logging 47 | statement { 48 | effect = "Allow" 49 | actions = [ 50 | "logs:CreateLogGroup", 51 | "logs:CreateLogStream", 52 | "logs:DescribeLogGroups", 53 | "logs:DescribeLogStreams", 54 | "logs:PutLogEvents" 55 | ] 56 | 57 | resources = [ 58 | "arn:aws:logs:*:*:*", 59 | ] 60 | } 61 | 62 | # Allow X-Ray tracing 63 | statement { 64 | effect = "Allow" 65 | actions = [ 66 | "xray:PutTraceSegments", 67 | "xray:PutTelemetryRecords" 68 | ] 69 | 70 | resources = [ 71 | "*", 72 | ] 73 | } 74 | } 75 | 76 | resource "aws_iam_policy" "function_logging_policy" { 77 | name = "${var.domain_name}-RegistryLambdaCWLoggingPolicy" 78 | description = "Policy for the registry lambda to use cloudwatch logging" 79 | policy = data.aws_iam_policy_document.allow_lambda_logging.json 80 | } 81 | 82 | resource "aws_iam_role_policy_attachment" "lambda_logging_policy_attachment" { 83 | role = aws_iam_role.lambda.id 84 | policy_arn = aws_iam_policy.function_logging_policy.arn 85 | } 86 | 87 | data "aws_iam_policy_document" "dynamodb_policy" { 88 | statement { 89 | effect = "Allow" 90 | actions = [ 91 | "dynamodb:DescribeTable", 92 | "dynamodb:Query", 93 | "dynamodb:Scan", 94 | "dynamodb:GetItem", 95 | "dynamodb:BatchGetItem", 96 | "dynamodb:PutItem", 97 | "dynamodb:UpdateItem", 98 | "dynamodb:DeleteItem", 99 | "dynamodb:BatchWriteItem" 100 | ] 101 | 102 | resources = [ 103 | aws_dynamodb_table.provider_versions.arn 104 | ] 105 | } 106 | } 107 | 108 | resource "aws_iam_policy" "lambda_dynamo_policy" { 109 | name = "${var.domain_name}-RegistryLambdaDynamoPolicy" 110 | description = "Policy for lambda to Read and Write to the provider versions DynamoDB table" 111 | policy = data.aws_iam_policy_document.dynamodb_policy.json 112 | } 113 | 114 | resource "aws_iam_role_policy_attachment" "lambda_dynamo_policy_attachment" { 115 | role = aws_iam_role.lambda.id 116 | policy_arn = aws_iam_policy.lambda_dynamo_policy.arn 117 | } 118 | 119 | // allow the api_function lambda to invoke the populate_provider_versions_function lambda 120 | data "aws_iam_policy_document" "populate_provider_versions_policy" { 121 | statement { 122 | effect = "Allow" 123 | actions = [ 124 | "lambda:InvokeFunction" 125 | ] 126 | 127 | resources = [ 128 | aws_lambda_function.populate_provider_versions_function.arn 129 | ] 130 | } 131 | } 132 | 133 | resource "aws_iam_policy" "lambda_populate_provider_versions_policy" { 134 | name = "${var.domain_name}-RegistryLambdaPopulateProviderVersionsPolicy" 135 | description = "Policy for the registry lambda to invoke the populate provider versions lambda" 136 | policy = data.aws_iam_policy_document.populate_provider_versions_policy.json 137 | } 138 | 139 | resource "aws_iam_role_policy_attachment" "lambda_populate_provider_versions_policy_attachment" { 140 | role = aws_iam_role.lambda.id 141 | policy_arn = aws_iam_policy.lambda_populate_provider_versions_policy.arn 142 | } 143 | 144 | 145 | -------------------------------------------------------------------------------- /keys: -------------------------------------------------------------------------------- 1 | src/internal/providers/keys -------------------------------------------------------------------------------- /lambda.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "api_function_binary" { 2 | provisioner "local-exec" { 3 | command = "GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOFLAGS=-trimpath go build -mod=readonly -tags lambda.norpc -ldflags='-s -w' -o ../api_function_bootstrap/bootstrap ./lambda/api" 4 | working_dir = "./src" 5 | } 6 | 7 | triggers = { 8 | always_run = timestamp() 9 | } 10 | } 11 | 12 | resource "null_resource" "populate_provider_versions_binary" { 13 | provisioner "local-exec" { 14 | command = "GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOFLAGS=-trimpath go build -mod=readonly -tags lambda.norpc -ldflags='-s -w' -o ../populate_provider_versions_bootstrap/bootstrap ./lambda/populate_provider_versions" 15 | working_dir = "./src" 16 | } 17 | 18 | triggers = { 19 | always_run = timestamp() 20 | } 21 | } 22 | 23 | data "archive_file" "api_function_archive" { 24 | depends_on = [null_resource.api_function_binary] 25 | 26 | type = "zip" 27 | source_file = "./api_function_bootstrap/bootstrap" 28 | output_path = "api_bootstrap.zip" 29 | } 30 | 31 | data "archive_file" "populate_provider_versions_archive" { 32 | depends_on = [null_resource.populate_provider_versions_binary] 33 | 34 | type = "zip" 35 | source_file = "./populate_provider_versions_bootstrap/bootstrap" 36 | output_path = "populate_provider_versions_bootstrap.zip" 37 | } 38 | 39 | // create the lambda function from zip file 40 | resource "aws_lambda_function" "api_function" { 41 | function_name = "${replace(var.domain_name, ".", "-")}-registry-handler" 42 | description = "A basic lambda to handle registry api events" 43 | role = aws_iam_role.lambda.arn 44 | handler = "registry-handler" 45 | memory_size = 128 46 | timeout = 60 47 | 48 | filename = data.archive_file.api_function_archive.output_path 49 | source_code_hash = data.archive_file.api_function_archive.output_base64sha256 50 | 51 | runtime = "provided.al2" 52 | 53 | publish = true 54 | 55 | tracing_config { 56 | mode = "Active" 57 | } 58 | 59 | environment { 60 | variables = { 61 | GITHUB_TOKEN_SECRET_ASM_NAME = aws_secretsmanager_secret.github_api_token.name 62 | PROVIDER_NAMESPACE_REDIRECTS = jsonencode(var.provider_namespace_redirects) 63 | PROVIDER_VERSIONS_TABLE_NAME = aws_dynamodb_table.provider_versions.name 64 | POPULATE_PROVIDER_VERSIONS_FUNCTION_NAME = aws_lambda_function.populate_provider_versions_function.function_name 65 | GITHUB_API_GW_URL = var.domain_name 66 | } 67 | } 68 | } 69 | 70 | // ensure we have provisioned concurrency for the lambda function 71 | resource "aws_lambda_provisioned_concurrency_config" "api_function" { 72 | function_name = aws_lambda_function.api_function.function_name 73 | provisioned_concurrent_executions = 1 74 | qualifier = aws_lambda_function.api_function.version 75 | } 76 | 77 | // create the lambda function from zip file 78 | resource "aws_lambda_function" "populate_provider_versions_function" { 79 | function_name = "${replace(var.domain_name, ".", "-")}-populate-provider-versions" 80 | description = "A basic lambda to handle populating provider versions in dynamodb" 81 | role = aws_iam_role.lambda.arn 82 | handler = "populate-provider-versions" 83 | memory_size = 128 84 | timeout = 10 * 60 85 | 86 | filename = data.archive_file.populate_provider_versions_archive.output_path 87 | source_code_hash = data.archive_file.api_function_archive.output_base64sha256 88 | 89 | runtime = "provided.al2" 90 | 91 | tracing_config { 92 | mode = "Active" 93 | } 94 | 95 | environment { 96 | variables = { 97 | PROVIDER_VERSIONS_TABLE_NAME = aws_dynamodb_table.provider_versions.name 98 | GITHUB_TOKEN_SECRET_ASM_NAME = aws_secretsmanager_secret.github_api_token.name 99 | GITHUB_API_GW_URL = var.domain_name 100 | } 101 | } 102 | } 103 | 104 | resource "aws_lambda_permission" "api_gateway_invoke_lambda_permission" { 105 | statement_id = "AllowAPIGatewayInvoke" 106 | action = "lambda:InvokeFunction" 107 | function_name = aws_lambda_function.api_function.function_name 108 | principal = "apigateway.amazonaws.com" 109 | 110 | # The /*/* portion grants access from any method on any resource 111 | # within the API Gateway "REST API". 112 | source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*" 113 | } 114 | 115 | resource "aws_cloudwatch_log_group" "log_group" { 116 | name = "/aws/lambda/${aws_lambda_function.api_function.function_name}" 117 | retention_in_days = 7 118 | } 119 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | } 6 | archive = { 7 | source = "hashicorp/archive" 8 | } 9 | null = { 10 | source = "hashicorp/null" 11 | } 12 | } 13 | 14 | backend "s3" { 15 | bucket = "registry-tfstate" 16 | key = "terraform.tfstate" 17 | dynamodb_table = "terraform_locks" 18 | 19 | region = "eu-west-1" 20 | } 21 | 22 | required_version = "1.5.6" 23 | } 24 | 25 | provider "aws" { 26 | region = var.region 27 | 28 | default_tags { 29 | tags = { 30 | Project = "registry" 31 | } 32 | } 33 | } 34 | 35 | provider "aws" { 36 | region = "us-east-1" 37 | 38 | alias = "us-east-1" 39 | 40 | default_tags { 41 | tags = { 42 | Project = "registry" 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /r53.tf: -------------------------------------------------------------------------------- 1 | resource "aws_acm_certificate" "api" { 2 | provider = aws.us-east-1 3 | 4 | domain_name = var.domain_name 5 | validation_method = "DNS" 6 | } 7 | 8 | data "aws_route53_zone" "public" { 9 | zone_id = var.route53_zone_id 10 | } 11 | 12 | resource "aws_route53_record" "api_validation" { 13 | for_each = { 14 | for dvo in aws_acm_certificate.api.domain_validation_options : dvo.domain_name => { 15 | name = dvo.resource_record_name 16 | record = dvo.resource_record_value 17 | type = dvo.resource_record_type 18 | } 19 | } 20 | 21 | allow_overwrite = true 22 | name = each.value.name 23 | records = [each.value.record] 24 | ttl = 60 25 | type = each.value.type 26 | zone_id = data.aws_route53_zone.public.zone_id 27 | } 28 | 29 | resource "aws_acm_certificate_validation" "api" { 30 | provider = aws.us-east-1 31 | 32 | certificate_arn = aws_acm_certificate.api.arn 33 | validation_record_fqdns = [for record in aws_route53_record.api_validation : record.fqdn] 34 | } 35 | 36 | resource "aws_route53_record" "api" { 37 | name = aws_api_gateway_domain_name.domain.domain_name 38 | type = "A" 39 | zone_id = data.aws_route53_zone.public.zone_id 40 | 41 | alias { 42 | name = aws_api_gateway_domain_name.domain.cloudfront_domain_name 43 | zone_id = aws_api_gateway_domain_name.domain.cloudfront_zone_id 44 | evaluate_target_health = false 45 | } 46 | } 47 | 48 | output "nameservers" { 49 | value = data.aws_route53_zone.public.name_servers 50 | description = "The name servers for the hosted zone." 51 | } 52 | -------------------------------------------------------------------------------- /secrets.tf: -------------------------------------------------------------------------------- 1 | resource "aws_secretsmanager_secret" "github_api_token" { 2 | name = "${var.domain_name}-github_api_token" 3 | } 4 | 5 | resource "aws_secretsmanager_secret_version" "github_api_token" { 6 | secret_id = aws_secretsmanager_secret.github_api_token.id 7 | secret_string = var.github_api_token 8 | } 9 | -------------------------------------------------------------------------------- /src/.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 1m 3 | 4 | linters-settings: 5 | errcheck: 6 | check-type-assertions: true 7 | exclude-functions: 8 | github.com/aws/aws-xray-sdk-go/xray.AddAnnotation 9 | 10 | exhaustive: 11 | check: 12 | - switch 13 | - map 14 | 15 | funlen: 16 | lines: 100 17 | statements: 50 18 | ignore-comments: true 19 | 20 | govet: 21 | enable-all: true 22 | disable: 23 | - fieldalignment # rule is too strict 24 | 25 | nolintlint: 26 | require-explanation: true 27 | require-specific: true 28 | 29 | linters: 30 | disable-all: true 31 | enable: 32 | - asasalint 33 | - asciicheck 34 | - bidichk 35 | - bodyclose 36 | - cyclop 37 | - dupl 38 | - durationcheck 39 | - errcheck 40 | - errname 41 | - errorlint 42 | - execinquery 43 | - exhaustive 44 | - exportloopref 45 | # - forbidigo # Disabling for now until we introduce structured logging 46 | - funlen 47 | - gocheckcompilerdirectives 48 | - gochecknoglobals 49 | - gochecknoinits 50 | - gocognit 51 | - goconst 52 | - gocritic 53 | - gocyclo 54 | - goimports 55 | - gomnd 56 | - gomoddirectives 57 | - gomodguard 58 | - goprintffuncname 59 | - gosec 60 | - gosimple 61 | - govet 62 | - ineffassign 63 | - loggercheck 64 | - makezero 65 | - mirror 66 | - musttag 67 | - nakedret 68 | - nestif 69 | - nilerr 70 | - nilnil 71 | - noctx 72 | - nolintlint 73 | # - nonamedreturns # Disabling for now until we introduce structured logging (and pull out xray) 74 | - nosprintfhostport 75 | - predeclared 76 | - promlinter 77 | - reassign 78 | - revive 79 | - rowserrcheck 80 | - sqlclosecheck 81 | - staticcheck 82 | - stylecheck 83 | - tenv 84 | - testableexamples 85 | - tparallel 86 | - typecheck 87 | - unconvert 88 | - unparam 89 | - unused 90 | - usestdlibvars 91 | - wastedassign 92 | - whitespace -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/opentofu/registry 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/ProtonMail/gopenpgp/v2 v2.7.3 7 | github.com/aws/aws-lambda-go v1.41.0 8 | github.com/aws/aws-sdk-go-v2 v1.21.0 9 | github.com/aws/aws-sdk-go-v2/config v1.18.39 10 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.39 11 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.21.5 12 | github.com/aws/aws-sdk-go-v2/service/lambda v1.39.5 13 | github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.3 14 | github.com/aws/aws-xray-sdk-go v1.8.1 15 | github.com/google/go-github/v54 v54.0.0 16 | github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278 17 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 18 | golang.org/x/oauth2 v0.11.0 19 | ) 20 | 21 | require ( 22 | github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect 23 | github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect 24 | github.com/andybalholm/brotli v1.0.4 // indirect 25 | github.com/aws/aws-sdk-go v1.44.114 // indirect 26 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 // indirect 27 | github.com/aws/aws-sdk-go-v2/credentials v1.13.37 // indirect 28 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect 29 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect 30 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect 31 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.15.5 // indirect 33 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14 // indirect 34 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.35 // indirect 35 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect 36 | github.com/aws/aws-sdk-go-v2/service/sso v1.13.6 // indirect 37 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.6 // indirect 38 | github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 // indirect 39 | github.com/aws/smithy-go v1.14.2 // indirect 40 | github.com/cloudflare/circl v1.3.3 // indirect 41 | github.com/golang/protobuf v1.5.3 // indirect 42 | github.com/google/go-querystring v1.1.0 // indirect 43 | github.com/jmespath/go-jmespath v0.4.0 // indirect 44 | github.com/klauspost/compress v1.15.0 // indirect 45 | github.com/pkg/errors v0.9.1 // indirect 46 | github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect 47 | github.com/valyala/bytebufferpool v1.0.0 // indirect 48 | github.com/valyala/fasthttp v1.34.0 // indirect 49 | golang.org/x/crypto v0.12.0 // indirect 50 | golang.org/x/net v0.14.0 // indirect 51 | golang.org/x/sys v0.12.0 // indirect 52 | golang.org/x/text v0.12.0 // indirect 53 | google.golang.org/appengine v1.6.7 // indirect 54 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 55 | google.golang.org/grpc v1.56.3 // indirect 56 | google.golang.org/protobuf v1.31.0 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM= 2 | github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= 3 | github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 4 | github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= 5 | github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= 6 | github.com/ProtonMail/gopenpgp/v2 v2.7.3 h1:AJu1OI/1UWVYZl6QcCLKGu9OTngS2r52618uGlje84I= 7 | github.com/ProtonMail/gopenpgp/v2 v2.7.3/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g= 8 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 9 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 10 | github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y= 11 | github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM= 12 | github.com/aws/aws-sdk-go v1.44.114 h1:plIkWc/RsHr3DXBj4MEw9sEW4CcL/e2ryokc+CKyq1I= 13 | github.com/aws/aws-sdk-go v1.44.114/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= 14 | github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc= 15 | github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= 16 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 h1:OPLEkmhXf6xFPiz0bLeDArZIDx1NNS4oJyG4nv3Gct0= 17 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13/go.mod h1:gpAbvyDGQFozTEmlTFO8XcQKHzubdq0LzRyJpG6MiXM= 18 | github.com/aws/aws-sdk-go-v2/config v1.18.39 h1:oPVyh6fuu/u4OiW4qcuQyEtk7U7uuNBmHmJSLg1AJsQ= 19 | github.com/aws/aws-sdk-go-v2/config v1.18.39/go.mod h1:+NH/ZigdPckFpgB1TRcRuWCB/Kbbvkxc/iNAKTq5RhE= 20 | github.com/aws/aws-sdk-go-v2/credentials v1.13.37 h1:BvEdm09+ZEh2XtN+PVHPcYwKY3wIeB6pw7vPRM4M9/U= 21 | github.com/aws/aws-sdk-go-v2/credentials v1.13.37/go.mod h1:ACLrdkd4CLZyXOghZ8IYumQbcooAcp2jo/s2xsFH8IM= 22 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.39 h1:DX/r3aNL7pIVn0K5a+ESL0Fw9ti7Rj05pblEiIJtPmQ= 23 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.39/go.mod h1:oTk09orqXlwSKnKf+UQhy+4Ci7aCo9x8hn0ZvPCLrns= 24 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg= 25 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8= 26 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g= 27 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas= 28 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 h1:SijA0mgjV8E+8G45ltVHs0fvKpTj8xmZJ3VwhGKtUSI= 29 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw= 30 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 h1:GPUcE/Yq7Ur8YSUk6lVkoIMWnJNO0HT18GUzCWCgCI0= 31 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42/go.mod h1:rzfdUlfA+jdgLDmPKjd3Chq9V7LVLYo1Nz++Wb91aRo= 32 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.21.5 h1:EeNQ3bDA6hlx3vifHf7LT/l9dh9w7D2XgCdaD11TRU4= 33 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.21.5/go.mod h1:X3ThW5RPV19hi7bnQ0RMAiBjZbzxj4rZlj+qdctbMWY= 34 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.15.5 h1:xoalM/e1YsT6jkLKl6KA9HUiJANwn2ypJsM9lhW2WP0= 35 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.15.5/go.mod h1:7QtKdGj66zM4g5hPgxHRQgFGLGal4EgwggTw5OZH56c= 36 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14 h1:m0QTSI6pZYJTk5WSKx3fm5cNW/DCicVzULBgU/6IyD0= 37 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14/go.mod h1:dDilntgHy9WnHXsh7dDtUPgHKEfTJIBUTHM8OWm0f/0= 38 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.35 h1:UKjpIDLVF90RfV88XurdduMoTxPqtGHZMIDYZQM7RO4= 39 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.35/go.mod h1:B3dUg0V6eJesUTi+m27NUkj7n8hdDKYUpxj8f4+TqaQ= 40 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI= 41 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o= 42 | github.com/aws/aws-sdk-go-v2/service/lambda v1.39.5 h1:uMvxJFS92hNW6BRX0Ou+5zb9DskgrJQHZ+5yT8FXK5Y= 43 | github.com/aws/aws-sdk-go-v2/service/lambda v1.39.5/go.mod h1:ByLHcf0zbHpyLTOy1iPVRPJWmAUPCiJv5k81dt52ID8= 44 | github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.3 h1:H6ZipEknzu7RkJW3w2PP75zd8XOdR35AEY5D57YrJtA= 45 | github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.3/go.mod h1:5W2cYXDPabUmwULErlC92ffLhtTuyv4ai+5HhdbhfNo= 46 | github.com/aws/aws-sdk-go-v2/service/sso v1.13.6 h1:2PylFCfKCEDv6PeSN09pC/VUiRd10wi1VfHG5FrW0/g= 47 | github.com/aws/aws-sdk-go-v2/service/sso v1.13.6/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4= 48 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.6 h1:pSB560BbVj9ZlJZF4WYj5zsytWHWKxg+NgyGV4B2L58= 49 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.6/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4= 50 | github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 h1:CQBFElb0LS8RojMJlxRSo/HXipvTZW2S44Lt9Mk2aYQ= 51 | github.com/aws/aws-sdk-go-v2/service/sts v1.21.5/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU= 52 | github.com/aws/aws-xray-sdk-go v1.8.1 h1:O4pXV+hnCskaamGsZnFpzHyAmgPGusBMN6i7nnsy0Fo= 53 | github.com/aws/aws-xray-sdk-go v1.8.1/go.mod h1:wMmVYzej3sykAttNBkXQHK/+clAPWTOrPiajEk7Cp3A= 54 | github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ= 55 | github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= 56 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 57 | github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= 58 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 59 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 60 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 61 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 62 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 63 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 64 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 65 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 66 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 67 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 68 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 69 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 70 | github.com/google/go-github/v54 v54.0.0 h1:OZdXwow4EAD5jEo5qg+dGFH2DpkyZvVsAehjvJuUL/c= 71 | github.com/google/go-github/v54 v54.0.0/go.mod h1:Sw1LXWHhXRZtzJ9LI5fyJg9wbQzYvFhW8W5P2yaAQ7s= 72 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 73 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 74 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= 75 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 76 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 77 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 78 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 79 | github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= 80 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 81 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 82 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 83 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 84 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 85 | github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278 h1:kdEGVAV4sO46DPtb8k793jiecUEhaX9ixoIBt41HEGU= 86 | github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= 87 | github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= 88 | github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= 89 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 90 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 91 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 92 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 93 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 94 | github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= 95 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 96 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 97 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 100 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 101 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 102 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 103 | golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= 104 | golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 105 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 106 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 107 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 108 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 109 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 110 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 111 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 112 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 113 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 114 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 115 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 116 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 117 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 118 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 119 | golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= 120 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 121 | golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= 122 | golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= 123 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 124 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 125 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 126 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 127 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 132 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 133 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 134 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 135 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 136 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 137 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 139 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 141 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 142 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 143 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 144 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 145 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 146 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 147 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 148 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 149 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 150 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 151 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 152 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 153 | golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= 154 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 155 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 156 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 157 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 158 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 159 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 160 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 161 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 162 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 163 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= 164 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= 165 | google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= 166 | google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= 167 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 168 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 169 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 170 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 171 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 172 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 173 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 174 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 175 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 176 | -------------------------------------------------------------------------------- /src/internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | awsconfig "github.com/aws/aws-sdk-go-v2/config" 11 | "github.com/aws/aws-sdk-go-v2/service/lambda" 12 | "github.com/aws/aws-xray-sdk-go/xray" 13 | gogithub "github.com/google/go-github/v54/github" 14 | "github.com/opentofu/registry/internal/github" 15 | "github.com/opentofu/registry/internal/providers/providercache" 16 | "github.com/opentofu/registry/internal/secrets" 17 | "github.com/shurcooL/githubv4" 18 | ) 19 | 20 | type Builder struct { 21 | IncludeProviderRedirects bool 22 | } 23 | 24 | func NewBuilder(options ...func(*Builder)) *Builder { 25 | configBuilder := &Builder{} 26 | for _, option := range options { 27 | option(configBuilder) 28 | } 29 | return configBuilder 30 | } 31 | 32 | func WithProviderRedirects() func(*Builder) { 33 | return func(builder *Builder) { 34 | builder.IncludeProviderRedirects = true 35 | } 36 | } 37 | 38 | type Config struct { 39 | ManagedGithubClient *gogithub.Client 40 | RawGithubv4Client *githubv4.Client 41 | 42 | LambdaClient *lambda.Client 43 | ProviderVersionCache *providercache.Handler 44 | SecretsHandler *secrets.Handler 45 | 46 | ProviderRedirects map[string]string 47 | } 48 | 49 | // BuildConfig will build a configuration object for the application. This 50 | // includes loading secrets from AWS Secrets Manager, and configuring the 51 | // AWS SDK. 52 | func (c Builder) BuildConfig(ctx context.Context, xraySegmentName string) (config *Config, err error) { 53 | if err = xray.Configure(xray.Config{ServiceVersion: "1.2.3"}); err != nil { 54 | err = fmt.Errorf("could not configure X-Ray: %w", err) 55 | return nil, err 56 | } 57 | 58 | // At this point we're not part of a Lambda request execution, so let's 59 | // explicitly create a segment to represent the configuration process. 60 | ctx, segment := xray.BeginSegment(ctx, xraySegmentName) 61 | defer func() { segment.Close(err) }() 62 | 63 | var awsConfig aws.Config 64 | awsConfig, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(os.Getenv("AWS_REGION"))) 65 | if err != nil { 66 | err = fmt.Errorf("could not load AWS configuration: %w", err) 67 | return nil, err 68 | } 69 | 70 | secretsHandler := secrets.NewHandler(awsConfig) 71 | 72 | githubAPIToken, err := secretsHandler.GetSecretValueFromEnvReference(ctx, "GITHUB_TOKEN_SECRET_ASM_NAME") 73 | if err != nil { 74 | err = fmt.Errorf("could not get GitHub API token: %w", err) 75 | return nil, err 76 | } 77 | 78 | tableName := os.Getenv("PROVIDER_VERSIONS_TABLE_NAME") 79 | if tableName == "" { 80 | err = fmt.Errorf("PROVIDER_VERSIONS_TABLE_NAME environment variable not set") 81 | return nil, err 82 | } 83 | 84 | providerRedirects := make(map[string]string) 85 | if c.IncludeProviderRedirects { 86 | if redirectsJSON, ok := os.LookupEnv("PROVIDER_NAMESPACE_REDIRECTS"); ok { 87 | if err := json.Unmarshal([]byte(redirectsJSON), &providerRedirects); err != nil { 88 | panic(fmt.Errorf("could not parse PROVIDER_NAMESPACE_REDIRECTS: %w", err)) 89 | } 90 | } 91 | } 92 | 93 | config = &Config{ 94 | ManagedGithubClient: github.NewManagedGithubClient(githubAPIToken), 95 | RawGithubv4Client: github.NewRawGithubv4Client(githubAPIToken), 96 | 97 | SecretsHandler: secretsHandler, 98 | ProviderVersionCache: providercache.NewHandler(awsConfig, tableName), 99 | LambdaClient: lambda.NewFromConfig(awsConfig), 100 | 101 | ProviderRedirects: providerRedirects, 102 | } 103 | return config, nil 104 | } 105 | 106 | // EffectiveProviderNamespace will map namespaces for providers in situations 107 | // where the author (owner of the namespace) does not release artifacts as 108 | // GitHub Releases. 109 | func (c Config) EffectiveProviderNamespace(namespace string) string { 110 | if redirect, ok := c.ProviderRedirects[namespace]; ok { 111 | return redirect 112 | } 113 | 114 | return namespace 115 | } 116 | -------------------------------------------------------------------------------- /src/internal/github/client.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | 10 | "github.com/aws/aws-xray-sdk-go/xray" 11 | "github.com/google/go-github/v54/github" 12 | "github.com/shurcooL/githubv4" 13 | "golang.org/x/oauth2" 14 | ) 15 | 16 | func getGithubOauth2Client(token string) *http.Client { 17 | return xray.Client(oauth2.NewClient(context.Background(), oauth2.StaticTokenSource( 18 | &oauth2.Token{AccessToken: token}, 19 | ))) 20 | } 21 | 22 | func NewManagedGithubClient(token string) *github.Client { 23 | client := github.NewClient(getGithubOauth2Client(token)) 24 | client.BaseURL, _ = url.Parse(fmt.Sprintf("https://%s/github/rest/", os.Getenv("GITHUB_API_GW_URL"))) 25 | return client 26 | } 27 | 28 | func NewRawGithubv4Client(token string) *githubv4.Client { 29 | return githubv4.NewEnterpriseClient(fmt.Sprintf("https://%s/github/graphql/", os.Getenv("GITHUB_API_GW_URL")), getGithubOauth2Client(token)) 30 | } 31 | -------------------------------------------------------------------------------- /src/internal/github/github.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/aws/aws-xray-sdk-go/xray" 12 | "github.com/google/go-github/v54/github" 13 | "github.com/shurcooL/githubv4" 14 | "golang.org/x/exp/slog" 15 | ) 16 | 17 | // GHRepository encapsulates GitHub repository details with a focus on its releases. 18 | // This is structured to align with the expected response format from GitHub's GraphQL API. 19 | type GHRepository struct { 20 | Repository struct { 21 | Releases struct { 22 | PageInfo struct { 23 | HasNextPage bool // Indicates if there are more pages of releases. 24 | EndCursor string // The cursor for pagination. 25 | } 26 | Nodes []GHRelease // A list of GitHub releases. 27 | } `graphql:"releases(first: $perPage, orderBy: {field: CREATED_AT, direction: DESC}, after: $endCursor)"` 28 | } `graphql:"repository(owner: $owner, name: $name)"` 29 | } 30 | 31 | // GHRelease represents a release on GitHub. 32 | // This provides details about the release, including its tag name, release assets, and its release status (draft, prerelease, etc.). 33 | type GHRelease struct { 34 | ID string // The ID of the release. 35 | TagName string // The tag name associated with the release. 36 | ReleaseAssets struct { 37 | Nodes []ReleaseAsset // A list of assets for the release. 38 | } `graphql:"releaseAssets(first:100)"` 39 | IsDraft bool // Indicates if the release is a draft. 40 | IsLatest bool // Indicates if the release is the latest. 41 | IsPrerelease bool // Indicates if the release is a prerelease. 42 | TagCommit struct { // The commit associated with the release tag. 43 | //nolint: revive, stylecheck // This is a struct provided by the GitHub GraphQL API. 44 | TarballUrl string // The URL to download the release tarball. 45 | } 46 | CreatedAt time.Time // The time the release was created. 47 | } 48 | 49 | // ReleaseAsset represents a single asset within a GitHub release. 50 | // This includes details such as the download URL and the name of the asset. 51 | type ReleaseAsset struct { 52 | ID string // The ID of the asset. 53 | DownloadURL string // The URL to download the asset. 54 | Name string // The name of the asset. 55 | } 56 | 57 | func RepositoryExists(ctx context.Context, managedGhClient *github.Client, namespace, name string) (exists bool, err error) { 58 | err = xray.Capture(ctx, "github.repository.exists", func(tracedCtx context.Context) error { 59 | xray.AddAnnotation(tracedCtx, "namespace", namespace) 60 | xray.AddAnnotation(tracedCtx, "name", name) 61 | 62 | slog.Info("Checking if repository exists") 63 | 64 | _, response, getErr := managedGhClient.Repositories.Get(tracedCtx, namespace, name) 65 | if getErr != nil { 66 | if response.StatusCode == http.StatusNotFound { 67 | slog.Info("Repository does not exist") 68 | return nil 69 | } 70 | slog.Error("Failed to get repository", "error", getErr) 71 | return fmt.Errorf("failed to get repository: %w", getErr) 72 | } 73 | 74 | slog.Info("Repository exists") 75 | exists = true 76 | return nil 77 | }) 78 | 79 | return exists, err 80 | } 81 | 82 | func FindRelease(ctx context.Context, ghClient *githubv4.Client, namespace, name, versionNumber string) (release *GHRelease, err error) { 83 | err = xray.Capture(ctx, "github.release.find", func(tracedCtx context.Context) error { 84 | xray.AddAnnotation(tracedCtx, "namespace", namespace) 85 | xray.AddAnnotation(tracedCtx, "name", name) 86 | xray.AddAnnotation(tracedCtx, "versionNumber", versionNumber) 87 | 88 | variables := initVariables(namespace, name) 89 | 90 | slog.Info("Finding release") 91 | 92 | for { 93 | nodes, endCursor, fetchErr := fetchReleaseNodes(tracedCtx, ghClient, variables) 94 | if fetchErr != nil { 95 | slog.Error("Failed to fetch release nodes", "error", fetchErr) 96 | return fmt.Errorf("failed to fetch release nodes: %w", fetchErr) 97 | } 98 | 99 | for _, r := range nodes { 100 | if r.IsDraft { 101 | continue 102 | } 103 | 104 | if r.TagName == fmt.Sprintf("v%s", versionNumber) { 105 | rCopy := r 106 | release = &rCopy 107 | return nil 108 | } 109 | } 110 | 111 | if endCursor == nil { 112 | break 113 | } 114 | variables["endCursor"] = githubv4.String(*endCursor) 115 | } 116 | 117 | return nil 118 | }) 119 | 120 | if release == nil { 121 | slog.Info("Release not found") 122 | return nil, err 123 | } 124 | 125 | slog.Info("Release found", "release", release) 126 | return release, err 127 | } 128 | 129 | const sincePadding = 2 * time.Minute 130 | 131 | func FetchReleases(ctx context.Context, ghClient *githubv4.Client, namespace, name string, since *time.Time) (releases []GHRelease, err error) { 132 | err = xray.Capture(ctx, "github.releases.fetch", func(tracedCtx context.Context) error { 133 | xray.AddAnnotation(tracedCtx, "namespace", namespace) 134 | xray.AddAnnotation(tracedCtx, "name", name) 135 | 136 | variables := initVariables(namespace, name) 137 | 138 | slog.Info("Fetching new releases", "since", since) 139 | 140 | for { 141 | nodes, endCursor, fetchErr := fetchReleaseNodes(tracedCtx, ghClient, variables) 142 | if fetchErr != nil { 143 | slog.Error("Failed to fetch release nodes", "error", fetchErr) 144 | return fmt.Errorf("failed to fetch release nodes: %w", fetchErr) 145 | } 146 | 147 | slog.Info("Checking for possible new releases", "count", len(nodes)) 148 | 149 | for _, r := range nodes { 150 | if r.IsDraft { 151 | continue 152 | } 153 | 154 | // if we have been provided a "since" time, we should only fetch releases created after that time 155 | // if the release was created before the given time, we can stop fetching 156 | // this is because all releases are ordered by creation date 157 | if since != nil && r.CreatedAt.Before(since.Add(-sincePadding)) { 158 | slog.Info("New release was created before given time, stopping reading releases", "release", r.TagName, "created_at", r.CreatedAt, "since", since) 159 | break 160 | } 161 | 162 | slog.Info("New release fetched", "release", r.TagName, "created_at", r.CreatedAt) 163 | releases = append(releases, r) 164 | } 165 | 166 | if endCursor == nil { 167 | slog.Info("No more releases to fetch") 168 | break 169 | } 170 | 171 | variables["endCursor"] = githubv4.String(*endCursor) 172 | } 173 | 174 | return nil 175 | }) 176 | 177 | slog.Info("New releases fetched", "count", len(releases)) 178 | return releases, err 179 | } 180 | 181 | func initVariables(namespace, name string) map[string]interface{} { 182 | perPage := 100 // TODO: make this configurable 183 | return map[string]interface{}{ 184 | "owner": githubv4.String(namespace), 185 | "name": githubv4.String(name), 186 | "perPage": githubv4.Int(perPage), 187 | "endCursor": (*githubv4.String)(nil), 188 | } 189 | } 190 | 191 | // fetchReleaseNodes will fetch a page of releases from the github api and return the nodes, endCursor, and an error 192 | // endCursor will be nil if there are no more pages 193 | func fetchReleaseNodes(ctx context.Context, ghClient *githubv4.Client, variables map[string]interface{}) (releases []GHRelease, endCursor *string, err error) { 194 | err = xray.Capture(ctx, "github.releases.nodes", func(tracedCtx context.Context) error { 195 | var query GHRepository 196 | 197 | if queryErr := ghClient.Query(tracedCtx, &query, variables); queryErr != nil { 198 | return fmt.Errorf("failed to query for releases: %w", queryErr) 199 | } 200 | 201 | if query.Repository.Releases.PageInfo.HasNextPage { 202 | endCursor = &query.Repository.Releases.PageInfo.EndCursor 203 | } 204 | 205 | releases = query.Repository.Releases.Nodes 206 | 207 | return nil 208 | }) 209 | 210 | return releases, endCursor, err 211 | } 212 | 213 | func FindAssetBySuffix(assets []ReleaseAsset, suffix string) *ReleaseAsset { 214 | slog.Info("Finding asset by suffix", "suffix", suffix) 215 | for _, asset := range assets { 216 | if strings.HasSuffix(asset.Name, suffix) { 217 | slog.Info("Asset found", "asset", asset) 218 | return &asset 219 | } 220 | } 221 | slog.Info("Asset not found") 222 | return nil 223 | } 224 | 225 | const githubAssetDownloadTimeout = 60 * time.Second 226 | 227 | func DownloadAssetContents(ctx context.Context, downloadURL string) (body io.ReadCloser, err error) { 228 | httpClient := xray.Client(&http.Client{Timeout: githubAssetDownloadTimeout}) 229 | 230 | err = xray.Capture(ctx, "github.asset.download", func(tracedCtx context.Context) error { 231 | slog.Info("Downloading asset", "url", downloadURL) 232 | req, reqErr := http.NewRequestWithContext(tracedCtx, http.MethodGet, downloadURL, nil) 233 | if reqErr != nil { 234 | slog.Error("Failed to create request", "error", reqErr) 235 | return fmt.Errorf("failed to create request: %w", reqErr) 236 | } 237 | 238 | resp, respErr := httpClient.Do(req) 239 | if respErr != nil { 240 | slog.Error("Error downloading asset", "error", respErr) 241 | return fmt.Errorf("error downloading asset: %w", respErr) 242 | } 243 | 244 | if resp.StatusCode != http.StatusOK { 245 | resp.Body.Close() 246 | slog.Error("Unexpected status code when downloading asset", "status_code", resp.StatusCode) 247 | return fmt.Errorf("unexpected status code when downloading asset: %d", resp.StatusCode) 248 | } 249 | 250 | body = resp.Body 251 | 252 | return nil 253 | }) 254 | 255 | slog.Info("Asset downloaded successfully") 256 | return body, err 257 | } 258 | -------------------------------------------------------------------------------- /src/internal/modules/repo.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import "fmt" 4 | 5 | // GetRepoName returns the repo name for a module 6 | // The repo name should match the format `terraform--` 7 | func GetRepoName(system, name string) string { 8 | return fmt.Sprintf("terraform-%s-%s", system, name) 9 | } 10 | -------------------------------------------------------------------------------- /src/internal/modules/types.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | type Version struct { 4 | Version string `json:"version"` 5 | } 6 | 7 | // VersionDetails provides comprehensive details about a specific provider version. 8 | // This includes the OS, architecture, download URLs, SHA sums, and the signing keys used for the version. 9 | // This is made to match the registry v1 API response format for the download details. 10 | type VersionDetails struct { 11 | Protocols []string `json:"protocols"` // The protocol versions the provider supports. 12 | Filename string `json:"filename"` // The filename of the provider binary. 13 | DownloadURL string `json:"download_url"` // The direct URL to download the provider binary. 14 | } 15 | -------------------------------------------------------------------------------- /src/internal/modules/versions.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/aws/aws-xray-sdk-go/xray" 10 | "github.com/shurcooL/githubv4" 11 | "golang.org/x/exp/slog" 12 | 13 | "github.com/opentofu/registry/internal/github" 14 | ) 15 | 16 | // GetVersions fetches a list of versions for a GitHub repository identified by its namespace and name. 17 | func GetVersions(ctx context.Context, ghClient *githubv4.Client, namespace string, name string, since *time.Time) (versions []Version, err error) { 18 | err = xray.Capture(ctx, "module.versions", func(tracedCtx context.Context) error { 19 | xray.AddAnnotation(tracedCtx, "namespace", namespace) 20 | xray.AddAnnotation(tracedCtx, "name", name) 21 | 22 | slog.Info("Fetching releases") 23 | 24 | releases, fetchErr := github.FetchReleases(tracedCtx, ghClient, namespace, name, since) 25 | if err != nil { 26 | return fmt.Errorf("failed to fetch releases: %w", fetchErr) 27 | } 28 | 29 | for _, release := range releases { 30 | versions = append(versions, Version{ 31 | // Normalize the version string to remove the leading "v" if it exists. 32 | Version: strings.TrimPrefix(release.TagName, "v"), 33 | }) 34 | } 35 | 36 | return nil 37 | }) 38 | 39 | return versions, err 40 | } 41 | -------------------------------------------------------------------------------- /src/internal/platform/platform.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import "regexp" 4 | 5 | type Platform struct { 6 | OS string `json:"os"` 7 | Arch string `json:"arch"` 8 | } 9 | 10 | var platformPattern = regexp.MustCompile(`.*_(?P[a-zA-Z0-9]+)_(?P[a-zA-Z0-9]+)`) 11 | 12 | func ExtractPlatformFromArtifact(releaseArtifact string) *Platform { 13 | matches := platformPattern.FindStringSubmatch(releaseArtifact) 14 | 15 | if matches == nil { 16 | return nil 17 | } 18 | 19 | platform := Platform{ 20 | OS: matches[platformPattern.SubexpIndex("Os")], 21 | Arch: matches[platformPattern.SubexpIndex("Arch")], 22 | } 23 | 24 | return &platform 25 | } 26 | -------------------------------------------------------------------------------- /src/internal/platform/platform_test.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import "testing" 4 | 5 | func TestExtractPlatformFromArtifact(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | releaseArtifact string 9 | expectedPlatform *Platform 10 | }{ 11 | { 12 | name: "should return platform for valid artifact", 13 | releaseArtifact: "my-provider_0.0.1_darwin_amd64.zip", 14 | expectedPlatform: &Platform{ 15 | OS: "darwin", 16 | Arch: "amd64", 17 | }, 18 | }, 19 | { 20 | name: "should return nil for invalid artifact", 21 | releaseArtifact: "no-thankyou", 22 | expectedPlatform: nil, 23 | }, 24 | { 25 | name: "should return nil for empty artifact", 26 | releaseArtifact: "", 27 | expectedPlatform: nil, 28 | }, 29 | } 30 | 31 | for _, test := range tests { 32 | t.Run(test.name, func(t *testing.T) { 33 | platform := ExtractPlatformFromArtifact(test.releaseArtifact) 34 | if platform == nil && test.expectedPlatform != nil { 35 | t.Fatalf("expected platform to not be nil") 36 | } 37 | if platform != nil && test.expectedPlatform == nil { 38 | t.Fatalf("expected platform to be nil") 39 | } 40 | if platform != nil && test.expectedPlatform != nil { 41 | if platform.OS != test.expectedPlatform.OS { 42 | t.Fatalf("expected platform OS to be %s, got %s", test.expectedPlatform.OS, platform.OS) 43 | } 44 | if platform.Arch != test.expectedPlatform.Arch { 45 | t.Fatalf("expected platform Arch to be %s, got %s", test.expectedPlatform.Arch, platform.Arch) 46 | } 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/internal/providers/RiskIdent/5180E94C4E6D9709.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQMuBGS+aOsRCADuB/PG+dBBJTPJXriuQwx6Dyi3HBO9SyIdW0kODz+5DCEI+fqr 4 | 0vC1LNUi/0qWrQVAfNol/holWFFFHof32DtmwK9Be/J9hNAjdyCjqT6rhndAKsbP 5 | RfI0M5OoW4GJjhFcPFQE7CfNLuT0pr/SlA0SwkULwwOLl1rpy98GpNpvczroXJVP 6 | kkw3TSxOPl0RjYPXlWPljFTrIJi6b+d+FcQ0tSEIN79rpTC920aKvzGLZA6xybyJ 7 | 8u5DSes7V1/IA/Z3Nmks8xQgqdvYnq7EZhtoNfQKrqixC/wFAp9Pr2+AZVaEMlyF 8 | jQX6wsBvx10sQoMJJOpTq7ispqDQb+5gbgx/AQCT94484bBQ2R7JRovwtX1d0Y3Q 9 | Fm2d2si9mBFQ7U86jwgAvkiMyH2SFsZq+2femSmdp++mZ28Uf4brYhaKz9iFgcW6 10 | Kv6wmw2ZqSQ4DzoIOJG2tgRbu0bKbbe3D+SMG7729McND2HQfkdsuevbw8EJrla4 11 | wt/t5L3yP77SrN3QZ1qZ0ExwB8qS//iIuAUWbNoRSv7HR/qe1eK0244Ycp6i9Ki2 12 | h3fe1oDFM2+WO2+Fi0uWdRjgkrHc+13sRG9vTTgmFHi5mHvKdAYHIeh79lmyRmyr 13 | xND9DSslkW454Mg3tLzhEIkAucoVf618R/N15R+ueerOTNZdKtO91IYIsYxv/ehb 14 | OcwOFBm5ftcy5bVL4uP3vCONW6b2tGZVBNTEIBzl+QgA0wX18bVeRY4F2CRifV/i 15 | NqJw6ooYZWxbUQsxp7VFMvw1siMD6WKJ/RfKh0G5UCa33317ajHvJGswdz3kDntt 16 | 4jPVhMcFLWovcAXlpbeOOAKJHpJyvJrKXp6WZNWcjZQt1uLACqZBMPVutpx4jwdx 17 | /XaP2AFQSZiO/1B3KTeKKDnrbrE+S79lO7C0XIqCcgT1VzgM+KxuZY5un6uxa5KY 18 | jchEbDosEavOi/iYMq5cXo1H4nNzlv7nmAcGicHnKJfUU8KQsgy7RPOUs28oOk2j 19 | 9MRTWO9jpwCJzt23x2xe+wIJCJv9ocqsuktJjQ+rPnZVj9OesilS6Cev8rra9Sxh 20 | +7RRUGxhdGZvcm0gdGVhbSAoVGVycmFmb3JtIHByb3ZpZGVyIHNpZ25pbmcga2V5 21 | KSA8cGxhdGZvcm0rdGVycmFmb3JtQHJpc2tpZGVudC5jb20+iJMEExEIADsWIQT8 22 | XTRYODy7+Njcr/tRgOlMTm2XCQUCZL5o6wIbAwULCQgHAgIiAgYVCgkICwIEFgID 23 | AQIeBwIXgAAKCRBRgOlMTm2XCYHGAP4/4qHffk6tOAkpdbnutHkDkPh3uC+QjCyk 24 | MMmxES3gEwD/eLPtasHaBC6am2yGk40BcvhNdx53ZkI6MvmiSAOPgRk= 25 | =oFEG 26 | -----END PGP PUBLIC KEY BLOCK----- 27 | -------------------------------------------------------------------------------- /src/internal/providers/errors.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import "fmt" 4 | 5 | type FetchErrorCode int 6 | 7 | const ( 8 | ErrCodeReleaseNotFound FetchErrorCode = 1 9 | ErrCodeAssetNotFound FetchErrorCode = 2 10 | ErrCodeSHASumsNotFound FetchErrorCode = 3 11 | ErrCodeManifestNotFound FetchErrorCode = 4 12 | ErrCodeCouldNotGetPublicKeys FetchErrorCode = 5 13 | ) 14 | 15 | type FetchError struct { 16 | Inner error 17 | Message string 18 | Code FetchErrorCode 19 | } 20 | 21 | func (p *FetchError) Error() string { 22 | return fmt.Sprintf("%d, %s", p.Code, p.Message) 23 | } 24 | 25 | func (p *FetchError) Unwrap() error { 26 | return p.Inner 27 | } 28 | 29 | func newFetchError(message string, code FetchErrorCode, err error) error { 30 | return &FetchError{ 31 | Message: message, 32 | Code: code, 33 | Inner: err, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/internal/providers/keys.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "embed" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/ProtonMail/gopenpgp/v2/crypto" 13 | "github.com/opentofu/registry/internal/providers/types" 14 | ) 15 | 16 | //go:embed keys/* 17 | var keys embed.FS 18 | 19 | // KeysForNamespace returns the GPG public keys for the given namespace. 20 | func KeysForNamespace(namespace string) ([]types.GPGPublicKey, error) { 21 | dirName := filepath.Join("keys", namespace) 22 | 23 | entries, err := keys.ReadDir(dirName) 24 | 25 | if err != nil { 26 | // This is fine, it just means that the namespace doesn't have any keys yet. 27 | if os.IsNotExist(err) { 28 | return []types.GPGPublicKey{}, nil 29 | } 30 | 31 | // This is not fine, it means that we failed to read the directory for some 32 | // other reason. 33 | return nil, fmt.Errorf("failed to read key directory: %w", err) 34 | } 35 | 36 | publicKeys := make([]types.GPGPublicKey, 0, len(entries)) 37 | var buildErrors []error 38 | 39 | for _, entry := range entries { 40 | path := filepath.Join(dirName, entry.Name()) 41 | 42 | publicKey, err := buildKey(path) 43 | if err != nil { 44 | buildErrors = append(buildErrors, fmt.Errorf("could not build public key at %s: %w", path, err)) 45 | } else { 46 | publicKeys = append(publicKeys, *publicKey) 47 | } 48 | } 49 | 50 | return publicKeys, errors.Join(buildErrors...) 51 | } 52 | 53 | // NamespacesWithKeys returns the namespaces that have keys. 54 | func NamespacesWithKeys() ([]string, error) { 55 | entries, err := keys.ReadDir("keys") 56 | if err != nil { 57 | return nil, fmt.Errorf("failed to read key directory: %w", err) 58 | } 59 | 60 | var namespaces []string 61 | 62 | for _, entry := range entries { 63 | namespaces = append(namespaces, entry.Name()) 64 | } 65 | 66 | return namespaces, nil 67 | } 68 | 69 | func buildKey(path string) (*types.GPGPublicKey, error) { 70 | file, err := keys.Open(path) 71 | if err != nil { 72 | return nil, fmt.Errorf("failed to open key file: %w", err) 73 | } 74 | defer file.Close() 75 | 76 | data, err := io.ReadAll(file) 77 | if err != nil { 78 | return nil, fmt.Errorf("could not read key file: %w", err) 79 | } 80 | 81 | asciiArmor := string(data) 82 | 83 | key, err := crypto.NewKeyFromArmored(asciiArmor) 84 | if err != nil { 85 | return nil, fmt.Errorf("could not build public key from ascii armor: %w", err) 86 | } 87 | 88 | return &types.GPGPublicKey{ 89 | ASCIIArmor: asciiArmor, 90 | KeyID: strings.ToUpper(key.GetHexKeyID()), 91 | }, nil 92 | } 93 | -------------------------------------------------------------------------------- /src/internal/providers/keys/Ferlab-Ste-Justine/20211111.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 50 | 5Be54g21 51 | =ak4W 52 | -----END PGP PUBLIC KEY BLOCK----- -------------------------------------------------------------------------------- /src/internal/providers/keys/claranet/20230515-argocd.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 50 | =CE1o 51 | -----END PGP PUBLIC KEY BLOCK----- 52 | -------------------------------------------------------------------------------- /src/internal/providers/keys/claranet/20230515-rke.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 50 | =B5UN 51 | -----END PGP PUBLIC KEY BLOCK----- 52 | -------------------------------------------------------------------------------- /src/internal/providers/keys/gitlabhq/20220911.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBGNrurUBEADXE1KKOg3VUNmfdVVhDcX3eKGneXqKU8Z09i3AUa5O00889ZOv 4 | ji8ffNGM6NGgcEKmGIjRBmKykIHxzGNOM0giKH3ru0wAYepFGyNsBvuwQzaqo1cW 5 | GSOELGyCfptnAQDom2PLzrl3ponf2qFpcRb+N/O7Xq/2qTqsyHUOmPQCnH04RPS6 6 | KI4fJp2qXRdoXIuAOBtNkz2tfAc80V+EYPA1mkukB8wUckIEkio46HKd+18dCYZ0 7 | NMlkZTRjY+VJ32Di0WURnM7ZUJDzOQR3kbH0xa7Lv6vp7unWQeClTJPyruglgeYE 8 | byM1RNveq41vm34TlhaSLOkfK/86Upx8y8zTOiBAO9CIKMUGmRvrzN//TQx82R4k 9 | zF7kyAVOZvt9eGei9yunYPhKknSKOf/yTSILQNLGyNocFTftqN9sX3/79M2WwjdA 10 | Y5Jo49cfRg/xvUOo8ImecI/7qBc+pgoIoo5CK4UVfgrHK+o1GEe8egiZhcJtDOGb 11 | 82Yfvdgi4VNf4bj+EVhcE1tGigx0mBdKnSEiSMZx2+UFR5h1oaqAomEKsw6Mz6Tk 12 | OX2bkAisYQ1MS0Dcw7AVjYHqLrQg9Uph6FBACRcWE3tSfdaQbkbduomVLKvm11m/ 13 | oKk9htP5ES7I2qkJ+MuPe5SUniP88h3RJiHG1I7XU9+cJi7F6E7W4GE5rQARAQAB 14 | tPhUaW1vIEZ1cnJlciBvbiBiZWhhbGYgb2YgdGh0ZSBHaXRMYWIgVGVycmFmb3Jt 15 | IFByb3ZpZGVyIChTaWduaW5nIEtleSBmb3IgdGhlIEdpdExhYiBUZXJyYWZvcm0g 16 | UHJvdmlkZXIgcmVsZWFzZXMuIENyZWF0ZWQgYnkgVGltbyBGdXJyZXIgPHRmdXJy 17 | ZXJAZ2l0bGFiLmNvbT4pIDxjb250YWN0LXByb2plY3QrZ2l0bGFiLW9yZy10ZXJy 18 | YWZvcm0tcHJvdmlkZXItZ2l0bGFiLTQwOTE2Nzc2LWlzc3VlLUBpbmNvbWluZy5n 19 | aXRsYWIuY29tPokCUQQTAQgAOxYhBPE9Bl0hYbg4wiu7Mw1Ht6uF9j9lBQJja7q1 20 | AhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEA1Ht6uF9j9lk+4QAJRx 21 | 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 54 | oMblH2QxUwpKh+zj0D+5edmKil0TGSI= 55 | =ds82 56 | -----END PGP PUBLIC KEY BLOCK----- -------------------------------------------------------------------------------- /src/internal/providers/keys/opentofu/20230928.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBGUVT+EBEADtxhSVWgcv/pwABx+0bw3RKWZr5jOvM66DqchzyYwnPrR8mor0 4 | 2JaG2xHojhirMr/ElfQyOgBh77psqTUC7JRuB0N0TwzEh+9y7cjqUgL7cuZ16oCc 5 | Co36nAERNkcUwsn+eKqGBvA0MTnvjhQm7quANhrR+oDz4eGdFqK04JLm5F/8tBhx 6 | atNeLnL5XGpmx1nRPgqlindLWVKX8+O2+1TYg0R5rQzRsTSwsq3a0CL2mDHcyovQ 7 | Ri4t6gJAlpza63HEGp2FmI/7ncE+ss6uL5PxQaD9ZInKCt8h4hF4HiRbq+gOAwKn 8 | kAqdMvnh/vriqLInx54rKebZaRtMCKim3jIQ1Ohnq87LjDWySw/Ghk3h1DJHFjuU 9 | F/iN95CoYIw3bfMi+xR+7t+nH/LD67l74uApBrMW02jGhNxwlOm/w/1b/f6c4tY8 10 | xlAixd7q6Ve1MQe/KZSUXH7Y2G+eh2IHYlKBf9E9rAUIqxLYmXWAm4hKwCesBj2p 11 | 2po7rAE0IUa6nHo5dL4YTO8hG7A7d3w0kBOTv7HAgRF3/qHV45rp5eowu2awxs5y 12 | 1C7Tr4tF7Tab+gtbVcUQjkK8CRq1NlDejDtUa16Z8UTgOQ8WnZymxlKmopgGgKye 13 | qsNecAQHQOOXy4/TN33Ny9y/6sCprHlyxNkPXJH9FVWaUqH9BB1R2AAltQARAQAB 14 | tCtPcGVuVG9mdSBBbHBoYSBUZXN0IDxvcGVudG9mdUBvcGVudG9mdS5vcmc+iQJM 15 | BBMBCgA2FiEEu+OD0i6AaY0sOVyJDcZO0JOz6f8FAmUVT+ECGwMECwkIBwQVCgkI 16 | BRYCAwEAAh4FAheAAAoJEA3GTtCTs+n/oz8P/RFvYjMl2L3xp740Ch8284T+udzN 17 | JAbbbaNrGRGr+MpJKvF0I0+ac81PZD09tWVKehjce65tO3maVI9+15og7s6LFeld 18 | b09gDoAKErlAESDn574crgIgoPoMMGzI88kwbHFqXpCCR4kVQ+eT5XGpd5gP5iu+ 19 | 4q52qumFxZiJki4ZNrO86+6nJBdbr1yOqLEzotC2SitvvxLmCI48cl32U5gMKjnQ 20 | oZB8skQZ523wu/vIs1LffdldTPCn+wuPCYFZK1vGcCYvi6YVA1MJGcRNJ74r4KJB 21 | rvr4rOKDOXkQditbP0XoYKGmU4CCnDNeydM3rwH5KH3VUj+YqAdZ4wE8e5H2nW6l 22 | Oi0WrxrkjpEXbHanOSRfrp6DqIquNdF5oU08BfIKGPOfr55DOwz2Swr5o82QAaZU 23 | BbeK+eK4MOvZeBHyosE4c2aIuOqFb1Peb/QtfSroWI0GrZwkCpnjNffG/FfBQ28r 24 | REVIMiVaiqvFRsjCktDSTjZMczLQkg9mMcCB/paEdGCF2kKxvLjERkWq5zCySbom 25 | R0EacF2mT0+RismErdKrLe1ZSNtuINyZBu7c+Msixbyxy93mPMXHQjstN/mrE8eD 26 | D/k/xnbIiWmIw+vhmeqRLi4qThHFuN688IwdJDrd7J95KNb6xMxsXdxn6uXRxb28 27 | xQ2Kg3aigP/4Iy5g 28 | =lHm2 29 | -----END PGP PUBLIC KEY BLOCK----- 30 | -------------------------------------------------------------------------------- /src/internal/providers/keys/opentofu/20231115-providers.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 51 | =VhyH 52 | -----END PGP PUBLIC KEY BLOCK----- 53 | -------------------------------------------------------------------------------- /src/internal/providers/keys/oracle/20230921.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 40 | =BetV 41 | -----END PGP PUBLIC KEY BLOCK----- -------------------------------------------------------------------------------- /src/internal/providers/keys/scalr/20210514.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 50 | oGH3vqE8dmM= 51 | =pGBz 52 | -----END PGP PUBLIC KEY BLOCK----- -------------------------------------------------------------------------------- /src/internal/providers/keys/spacelift-io/20230908.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GopenPGP 2.5.0 3 | Comment: https://gopenpgp.org 4 | 5 | xsFNBF+hjzUBEADpyDoEFxRwEocHBaP8zU2RaSko7l+v1YsanbdEL+WV5Jb9vlTk 6 | 0+AwFkpTCFe1EZH6n6P+3m2cHeLuBzICXatXtqwiSyCv86H8ZjycTyAnQ7wnGoGO 7 | 8GZcfNY1iLsLU0NOWLfmguuKPiDXHiCcjc/fxN8k0k3mFZlHbtzLyiZ29p46KQua 8 | M9jhtshD01b1lgEj8cs02OZaomgKqli8sfqGzfLkTGsxOuw9cck5SpbLTRt6WwoW 9 | gnOWl9z30EN1gCJwhe+Eq7uLWx5/XB19k6tVzjQvCX8vBUk8laI6I0vxgFbPVmVE 10 | UCoSmLtekhJZnepWH5d6oKBiqnOOCZV2NX5Ye+Aiy7uHiN5q/CZ1dvMxpCdta1la 11 | rrhYUjdn6dJeGjWpliVo7y9SV6u1aQLTRfUF2v9NxkFUIaZejbi6NGabnqsO22Z6 12 | 0H3CO4lSiJAEAp7iXUmlk2BbL3RrksQO5dYJwrpew4BnjnMRjZSniEtxnEc8HRkl 13 | n6hAvY58G6ljkaVTWq0CJ5hUdMnzVzHDrdkSUpidGXTuhVuHDCRht32PcMimRzTK 14 | eUBjNzWJHqh4EGEWII4Lxp0owS/nOW58PLzFyuLVbtau+qXkxpp9TVapKwuW1NL6 15 | Ho6E/XE8Gqph7ZLroVME/sl2rkylLvkES9jQPX4JS1OiTxKlrLkdRK9+iQARAQAB 16 | zSNzcGFjZWxpZnQuaW8gPGNvbnRhY3RAc3BhY2VsaWZ0LmlvPsLBlAQTAQgAPhYh 17 | BBdf2XrSNY7+AoMpeOMC+1qinYj3BQJfoY81AhsDBQkHhh+ABQsJCAcCBhUKCQgL 18 | AgQWAgMBAh4BAheAAAoJEOMC+1qinYj31LUQAJWHt9dIsOHl3j2IKa8MacOiyKwp 19 | hvLpbH3Ja8MXMRdIWIuxkv848AH2g22C2rnw0nPA8dUPuzp26imfnriN18ENBayx 20 | MgtqzvLEzO3QLx3cD7zcZ4xzoEKIVE8h4UEL8qNvKqdc1B81kQHdmdBxzKuWwAS8 21 | VLcmHFA2XFWaiGoPn2XNGvkOns1XNxEN2udmqQkJJ5y2QktKW/RLifvltiUXc2Cc 22 | CANG1bzkJdkveJd4e0LgTOLrB3jZ1bX2NEkOHM3hiJK3hzefGDDmLScLGCz23ecc 23 | +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 52 | /M+xZ7MXUVg1vQs= 53 | =mlxJ 54 | -----END PGP PUBLIC KEY BLOCK----- 55 | -------------------------------------------------------------------------------- /src/internal/providers/keys/umich-vci/20201014.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBF+HYaYBEADA1rNYgD3nO7w64a7EbG3/iNx6UiNGTTXA9k/1BelV51yXkQ/9 4 | 0o/Y28yXpbAnxaZx8gGyE+GBJPwmoLzWngrvb/YKrbxZkvOc1GfikO6Tf/sS1kdo 5 | 3VwbNibFRawfy+NryaXHbmdDUg+PmUrlgHaq8nfAnmgNpOypnbG6M3AZnJx3K7vC 6 | pPUmsyJAwBR+pjJozO4o+ryql0OOYqvkGNY1qjl4wOUBdPCaxvSWpB/GlXyQE9av 7 | WLjQ/jCIa9avv0MaYiWpPA2Kdte5UaiJiqfkgIk1k/0BLqYw7J1SQP4n6X9NBvJ5 8 | +a124ya1BeaKzcdbBnFLYC6iYjn03UuQqf9pBJ/YSrQx9R2LrslaMJUc87FdM4wO 9 | C5yk7J0sS+WfsK0CYAFKCSG4vc+BJk36rBe6+Daqu2gnQwoXVBY5SNnILr6WVzSa 10 | faqyIBTCaEwgLbYR8I7aD7KZ+2X9vjA7hDLjpfilvGuCCEQ4gVQgz4iFZT6q8XH1 11 | 7NZ7dbhWbuqWz0FGHcJgnbXVrzTI6rmheeI3dQ8yBZhTnFO6nIl19uqWweImLowB 12 | HOcvmlpANLpp2QedCEIg363xNqlfJX68kzHm7v8kdTk13dzyRUdKB3HMSxJYjZy2 13 | 17btNOCQPnhyWKS5Re+Lo50q4dWgAgmTMl78aDkem8+QylOAc8TZsWM7iQARAQAB 14 | tEZVLU0gSVRTIFZpcnR1YWxpemF0aW9uIGFuZCBDbG91ZCBJbmZyYXN0cnVjdHVy 15 | ZSA8aXRzLmlzLnZjaUB1bWljaC5lZHU+iQJOBBMBCAA4FiEEHwVmSFD4Ei9RdJsC 16 | WWQV7i5uOlUFAl+HYaYCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQWWQV 17 | 7i5uOlWckg/+PkGPyf7mckTTHIr4kNtfUD5RU9AJQvBiczINc6AQXDjXXmpC/yda 18 | YALIVOwMbiUzctVLqSKBS8OcNTmxZYmHGhj9nICOhyI+8IVzvrE/NUw8Prfvtfsl 19 | VJ8uF6gI79yb/KqFrA9p+k72Qa2DC6CCCA5yH0Zp111wOupbH/PfB7JhKONG7vnd 20 | y4O4Q+AQBLpzJIvaTGIh4WNPQ5S+WeTM74zs8pJqslz47Af6zCw6reHqOfJmSj26 21 | KnzsQYoTCFWpxYxEsJpaXGBe5u5JrwZ+RnUH1Pi/YS+XIiZpRw564/DRp68YyT+E 22 | KI25eacttNIIlwqmM1jHYwjaTQZvJjtKd1gyKuWzBlPkoA7uXDme/82jgNuq32WD 23 | qhstRReG9ikwLCSPSMi8SpYEvoGzF7pTky7E41KD2/bzM1Z+quUJt/06JVWJMtdS 24 | IjCn/DuTALNyOTbbZO7tTZ55/XKRhD5C3Q1dCrTf/P4RaVqtXvFlFqPqcfiWJLEe 25 | B4zPqGDuZfV9xTeMU0xXU7xMpH72d1e0QMqWu2Hx0GKV8+KebHrDncDEXMMPnJuv 26 | WCyxsPS0pmhyQFW63SL46o5O3gBQt+9mnywYhkswt08aZLFg5JPsRLT3HHwYHC8s 27 | kBbd+OFHjYDp6JEtzq3/ttX25AhtuD+Nj8xL2XQo079sYeZAUZXY2pO5Ag0EX4dh 28 | pgEQANWCb1RJJpbzdctfMk+mpVpGmTA/jP9n8V9sXYEEIUP6KBZ5lFaMizH3l+/4 29 | X6T8Htep9UMD/Vihz1gjSeqzy7R5UVhr27a//220iEsx0jUbw8/UFvVAEUxKRCMR 30 | 5j3RoWATVDKA75NAS6Uj+G0E7Ar0IeVr//S2bsSHdVv/oJTigbDn05tUuGnP63qj 31 | QE5JaNQeUDJNTrgIUeQK7kdCCE5IfP5cIMz/5qG/s5Fs4HUPKWqxzDzU5Ov1XiHd 32 | 0u2ZZPd0eP0rqrkEKTLm558qJkbdZ10x9Q7DEabEoSuCN8ueOGw2Qcwm7MxMPuIt 33 | xUIS3HxXyWFKaR42SAMGkVtsOO+8Ipm446tUx7vscnxY28zLbACUMojPNVMo+AdS 34 | BjhV1OQHlI90ZjHBgXlO7vzZdDbKi1depMDl9dQV7HXtlG/JBHyWSOBTdElJBJcC 35 | CXysGlxoj57WhupAbk3SBU/TyghE4ndka36YzU3ooAHCrxee7IOFANSXofSNQi/k 36 | v+QzNwyO1q6bJtuRBc+5MaEDbMDdL1rGguBxNXtnspB6mzFBRqABNtq6NALyh9Eg 37 | lk8kL+ryt9Pi8K3L4/5sBioCwhRs9Vua/HT5S6ftly8VOlXXnrEnNAiRd1BBLuBm 38 | f9cjLL3ywRQs/hUQPrgaGDAstP0ij0r+pJCHB0Bnfxx6fvi/ABEBAAGJAjYEGAEI 39 | ACAWIQQfBWZIUPgSL1F0mwJZZBXuLm46VQUCX4dhpgIbDAAKCRBZZBXuLm46Vavd 40 | D/9DWfKREKdclHhq3AxWggKFu0AlNvuqBgdCEsIlr7CfieA8jY4Jl+1jGHecSIPq 41 | nrfKvwC0OGNF9yYtiaF4hkOfHoNtJsfuzdbeOTrqPu8mOltjtWwrzB0Tezs7tO74 42 | drwq6Tc9F0uYOVsT/wGByWYjwa+TGv4/iefY3xOVmnyMldICHj9nqrcnlQrQEf3t 43 | 0RqKLZqcNBFgPX8Be3o82gWn/Lr4KKqXb5LmyrXVd9NipqmjVIWnj3swrJ5kh2H+ 44 | Z3AKH1GjYvmaPRSy1YWUhQHyxW8yIq5Si6GJr9ni+CWAOaHrhkaqJ2nvx/IKql7k 45 | iI+QxHDOo3qy/9Ja0cZV3wXtHkZYbuMCsEFqrebj5N8F400ku4O3k8EHSFEdDBvo 46 | /qucy6iBBuygWeTwRbwqTZPkQ3vxBO4/KybK0Mohzr9ZlqXlTXcT5X1EKkWE597D 47 | pjuwPSbzMbB3S+JHKF7yfAW4Qq6CiNgQDarQ29EoGCXk1v5Ol+s724hPCfY0e2SX 48 | RbKBL6lCIRaEKZ8UzL00livxRhglfGo191C/wXknGwlPORaLLf/dUr8rw116jxEA 49 | qKdgwo/jSa3xDmcVHS5gzZglAwrFCxg7G6nO0V30sca7Z54HkGSjRv1+dI+I1b92 50 | XTyFOa/mAIQvI2c/vEzlqKUf61xy6xd37AMCZ3Ov27CvBg== 51 | =SlWD 52 | -----END PGP PUBLIC KEY BLOCK----- 53 | -------------------------------------------------------------------------------- /src/internal/providers/keys_test.go: -------------------------------------------------------------------------------- 1 | package providers_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/opentofu/registry/internal/providers" 9 | ) 10 | 11 | func TestKeysForNamespace(t *testing.T) { 12 | t.Run("for an existing organization", func(t *testing.T) { 13 | keys, err := providers.KeysForNamespace("spacelift-io") 14 | 15 | if err != nil { 16 | t.Fatalf("expected no error, got %v", err) 17 | } 18 | 19 | if len(keys) != 1 { 20 | t.Fatalf("expected 1 key, got %d", len(keys)) 21 | } 22 | 23 | if keys[0].KeyID != "E302FB5AA29D88F7" { 24 | t.Fatalf("expected key ID to be E302FB5AA29D88F7, got %s", keys[0].KeyID) 25 | } 26 | 27 | if !strings.HasPrefix(keys[0].ASCIIArmor, "-----BEGIN PGP PUBLIC KEY BLOCK-----") { 28 | t.Fatalf("expected key to have ascii armor, got empty string") 29 | } 30 | }) 31 | 32 | t.Run("for a non-existing organization", func(t *testing.T) { 33 | keys, err := providers.KeysForNamespace("baconsoft") 34 | 35 | if err != nil { 36 | t.Fatalf("expected no error, got %v", err) 37 | } 38 | 39 | if len(keys) != 0 { 40 | t.Fatalf("expected no keys, got %v", keys) 41 | } 42 | 43 | if keys == nil { 44 | t.Fatalf("expected keys to be an empty slice, got nil") 45 | } 46 | }) 47 | } 48 | 49 | func TestAllNamespaces(t *testing.T) { 50 | namespaces, err := providers.NamespacesWithKeys() 51 | if err != nil { 52 | t.Fatalf("expected no error, got %v", err) 53 | } 54 | 55 | for _, namespace := range namespaces { 56 | t.Run(fmt.Sprintf("keys for %s", namespace), func(t *testing.T) { 57 | if _, err := providers.KeysForNamespace(namespace); err != nil { 58 | t.Fatalf("expected no error, got %v", err) 59 | } 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/internal/providers/manifest.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | 8 | "github.com/opentofu/registry/internal/github" 9 | "golang.org/x/exp/slog" 10 | ) 11 | 12 | type Manifest struct { 13 | Version float64 `json:"version"` 14 | Metadata ManifestMetadata `json:"metadata"` 15 | } 16 | type ManifestMetadata struct { 17 | ProtocolVersions []string `json:"protocol_versions"` 18 | } 19 | 20 | func findAndParseManifest(ctx context.Context, assets []github.ReleaseAsset) (*Manifest, error) { 21 | manifestAsset := github.FindAssetBySuffix(assets, "_manifest.json") 22 | if manifestAsset == nil { 23 | slog.Warn("No manifest found in release assets") 24 | return nil, nil //nolint:nilnil // This is not an error, it just means there is no manifest. 25 | } 26 | 27 | assetContents, err := github.DownloadAssetContents(ctx, manifestAsset.DownloadURL) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | manifest, err := parseManifestContents(assetContents) 33 | assetContents.Close() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | slog.Info("Found manifest") 39 | 40 | return manifest, nil 41 | } 42 | 43 | func parseManifestContents(assetContents io.ReadCloser) (*Manifest, error) { 44 | contents, err := io.ReadAll(assetContents) 45 | if err != nil { 46 | slog.Error("Failed to read manifest contents") 47 | return nil, err 48 | } 49 | 50 | var manifest *Manifest 51 | err = json.Unmarshal(contents, &manifest) 52 | if err != nil { 53 | slog.Error("Failed to parse manifest contents") 54 | return nil, err 55 | } 56 | 57 | return manifest, nil 58 | } 59 | -------------------------------------------------------------------------------- /src/internal/providers/providercache/fetch.go: -------------------------------------------------------------------------------- 1 | package providercache 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "encoding/base64" 8 | "encoding/json" 9 | "io" 10 | 11 | "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" 12 | "github.com/aws/aws-sdk-go-v2/service/dynamodb" 13 | "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" 14 | providerTypes "github.com/opentofu/registry/internal/providers/types" 15 | "golang.org/x/exp/slog" 16 | ) 17 | 18 | func decompress(data string) ([]byte, error) { 19 | decodedData, err := base64.StdEncoding.DecodeString(data) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | rdata := bytes.NewReader(decodedData) 25 | r, err := gzip.NewReader(rdata) 26 | if err != nil { 27 | return nil, err 28 | } 29 | defer r.Close() 30 | 31 | return io.ReadAll(r) 32 | } 33 | 34 | func (p *Handler) GetItem(ctx context.Context, key string) (*providerTypes.CacheItem, error) { 35 | slog.Info("Getting item from cache", "key", key) 36 | 37 | result, err := p.Client.GetItem(ctx, &dynamodb.GetItemInput{ 38 | TableName: p.TableName, 39 | Key: map[string]types.AttributeValue{ 40 | "provider": &types.AttributeValueMemberS{Value: key}, 41 | }, 42 | }) 43 | if err != nil { 44 | slog.Error("Failed to get item from cache", "key", key, "error", err) 45 | return nil, err 46 | } 47 | 48 | // check if the item is empty, if so return nil, this makes it easier to consume in other places 49 | if len(result.Item) == 0 { 50 | slog.Info("Item not found in cache", "key", key) 51 | return nil, nil //nolint:nilnil // This is not an error, it just means there is no manifest. 52 | } 53 | 54 | var compressedItem CompressedCacheItem 55 | err = attributevalue.UnmarshalMap(result.Item, &compressedItem) 56 | if err != nil { 57 | slog.Error("Failed to unmarshal compressed item from cache", "key", key, "error", err) 58 | return nil, err 59 | } 60 | 61 | decompressedData, err := decompress(compressedItem.Data) 62 | if err != nil { 63 | slog.Error("Failed to decompress item data", "key", key, "error", err) 64 | return nil, err 65 | } 66 | 67 | var item providerTypes.CacheItem 68 | err = json.Unmarshal(decompressedData, &item.Versions) 69 | if err != nil { 70 | slog.Error("Failed to unmarshal decompressed item to CacheItem", "key", key, "error", err) 71 | return nil, err 72 | } 73 | 74 | item.Provider = compressedItem.Provider 75 | item.LastUpdated = compressedItem.LastUpdated 76 | 77 | slog.Info("Successfully decompressed and unmarshalled item from cache", "key", key) 78 | return &item, nil 79 | } 80 | -------------------------------------------------------------------------------- /src/internal/providers/providercache/handler.go: -------------------------------------------------------------------------------- 1 | package providercache 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go-v2/aws" 5 | "github.com/aws/aws-sdk-go-v2/service/dynamodb" 6 | ) 7 | 8 | type Handler struct { 9 | TableName *string 10 | Client *dynamodb.Client 11 | } 12 | 13 | func NewHandler(awsConfig aws.Config, tableName string) *Handler { 14 | ddbClient := dynamodb.NewFromConfig(awsConfig) 15 | 16 | return &Handler{ 17 | TableName: aws.String(tableName), 18 | Client: ddbClient, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/internal/providers/providercache/store.go: -------------------------------------------------------------------------------- 1 | package providercache 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" 13 | "github.com/aws/aws-sdk-go-v2/service/dynamodb" 14 | "github.com/opentofu/registry/internal/providers/types" 15 | "golang.org/x/exp/slog" 16 | ) 17 | 18 | type CompressedCacheItem struct { 19 | Provider string `dynamodbav:"provider"` 20 | Data string `dynamodbav:"data"` 21 | LastUpdated time.Time `dynamodbav:"last_updated"` 22 | } 23 | 24 | func compress(data []byte) (string, error) { 25 | var b bytes.Buffer 26 | gz := gzip.NewWriter(&b) 27 | _, err := gz.Write(data) 28 | if err != nil { 29 | return "", err 30 | } 31 | err = gz.Close() 32 | if err != nil { 33 | return "", err 34 | } 35 | return base64.StdEncoding.EncodeToString(b.Bytes()), nil 36 | } 37 | 38 | func (p *Handler) Store(ctx context.Context, key string, versions types.VersionList) error { 39 | jsonData, err := json.Marshal(versions) 40 | if err != nil { 41 | slog.Error("got error marshalling item to JSON", "error", err) 42 | return fmt.Errorf("got error marshalling item to JSON: %w", err) 43 | } 44 | 45 | compressedData, err := compress(jsonData) 46 | if err != nil { 47 | slog.Error("got error compressing JSON data", "error", err) 48 | return fmt.Errorf("got error compressing JSON data: %w", err) 49 | } 50 | 51 | // make an anonymous type to satisfy the MarshalMap function 52 | toCache := CompressedCacheItem{ 53 | Provider: key, 54 | Data: compressedData, 55 | LastUpdated: time.Now(), 56 | } 57 | 58 | marshalledItem, err := attributevalue.MarshalMap(toCache) 59 | if err != nil { 60 | slog.Error("got error marshalling dynamodb item", "error", err) 61 | return fmt.Errorf("got error marshalling dynamodb item: %w", err) 62 | } 63 | 64 | putItemInput := &dynamodb.PutItemInput{ 65 | Item: marshalledItem, 66 | TableName: p.TableName, 67 | } 68 | 69 | slog.Info("Storing provider versions", "key", key, "versions", len(versions)) 70 | _, err = p.Client.PutItem(ctx, putItemInput) 71 | if err != nil { 72 | slog.Error("got error calling PutItem", "error", err) 73 | return fmt.Errorf("got error calling PutItem: %w", err) 74 | } 75 | 76 | slog.Info("Successfully stored provider versions", "key", key, "versions", len(versions)) 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /src/internal/providers/repo.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import "fmt" 4 | 5 | // GetRepoName returns the repo name for a provider 6 | // The repo name should match the format `terraform-provider-` 7 | func GetRepoName(name string) string { 8 | return fmt.Sprintf("terraform-provider-%s", name) 9 | } 10 | -------------------------------------------------------------------------------- /src/internal/providers/types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/opentofu/registry/internal/platform" 7 | ) 8 | 9 | // Version represents an individual provider version. 10 | // It provides details such as the version number, supported Terraform protocol versions, and platforms the provider is available for. 11 | // This is made to match the registry v1 API response format for listing provider versions. 12 | type Version struct { 13 | Version string `json:"version"` // The version number of the provider. 14 | Protocols []string `json:"protocols"` // The protocol versions the provider supports. 15 | Platforms []platform.Platform `json:"platforms"` // A list of platforms for which this provider version is available. 16 | } 17 | 18 | // VersionDetails provides comprehensive details about a specific provider version. 19 | // This includes the OS, architecture, download URLs, SHA sums, and the signing keys used for the version. 20 | // This is made to match the registry v1 API response format for the download details. 21 | type VersionDetails struct { 22 | Protocols []string `json:"protocols"` // The protocol versions the provider supports. 23 | OS string `json:"os"` // The operating system for which the provider is built. 24 | Arch string `json:"arch"` // The architecture for which the provider is built. 25 | Filename string `json:"filename"` // The filename of the provider binary. 26 | DownloadURL string `json:"download_url"` // The direct URL to download the provider binary. 27 | SHASumsURL string `json:"shasums_url"` // The URL to the SHA checksums file. 28 | SHASumsSignatureURL string `json:"shasums_signature_url"` // The URL to the GPG signature of the SHA checksums file. 29 | SHASum string `json:"shasum"` // The SHA checksum of the provider binary. 30 | SigningKeys SigningKeys `json:"signing_keys"` // The signing keys used for this provider version. 31 | } 32 | 33 | // SigningKeys represents the GPG public keys used to sign a provider version. 34 | type SigningKeys struct { 35 | GPGPublicKeys []GPGPublicKey `json:"gpg_public_keys"` // A list of GPG public keys. 36 | } 37 | 38 | // GPGPublicKey represents an individual GPG public key. 39 | type GPGPublicKey struct { 40 | KeyID string `json:"key_id"` // The ID of the GPG key. 41 | ASCIIArmor string `json:"ascii_armor"` // The ASCII armored representation of the GPG public key. 42 | } 43 | 44 | // CacheItem represents a single item in the cache. This single item corresponds to a single provider and will store all of the versions for that provider. 45 | // and the data required to serve the provider download and version listing endpoints. 46 | type CacheItem struct { 47 | Provider string `dynamodbav:"provider"` 48 | Versions VersionList `dynamodbav:"versions"` 49 | LastUpdated time.Time `dynamodbav:"last_updated"` 50 | } 51 | 52 | const allowedAge = (1 * time.Hour) - (5 * time.Minute) //nolint:gomnd // 55 minutes 53 | 54 | // IsStale returns true if the cache item is stale. 55 | func (i *CacheItem) IsStale() bool { 56 | return time.Since(i.LastUpdated) > allowedAge 57 | } 58 | 59 | type VersionList []CacheVersion 60 | 61 | func (l VersionList) ToVersions() []Version { 62 | var versionsToReturn []Version 63 | for _, version := range l { 64 | versionsToReturn = append(versionsToReturn, version.ToVersion()) 65 | } 66 | return versionsToReturn 67 | } 68 | 69 | func (l VersionList) Deduplicate() VersionList { 70 | if len(l) == 0 { 71 | return l 72 | } 73 | versions := make(map[string]CacheVersion) 74 | for _, v := range l { 75 | if _, ok := versions[v.Version]; !ok { 76 | versions[v.Version] = v 77 | } 78 | } 79 | 80 | // Convert the map back into a list 81 | var versionsToReturn VersionList 82 | for _, v := range versions { 83 | versionsToReturn = append(versionsToReturn, v) 84 | } 85 | return versionsToReturn 86 | } 87 | 88 | func (i *CacheItem) GetVersionDetails(version string, os string, arch string) (*VersionDetails, bool) { 89 | for _, v := range i.Versions { 90 | if v.Version == version { 91 | versionDetails := v.GetVersionDetails(os, arch) 92 | if versionDetails == nil { 93 | return nil, false 94 | } 95 | return versionDetails, true 96 | } 97 | } 98 | return nil, false 99 | } 100 | 101 | // CacheVersion provides comprehensive details about a specific provider version. 102 | // This includes the OS, architecture, download URLs, SHA sums, and the signing keys used for the version. 103 | // This is made to store data in our cache for both provider version listing and provider download endpoints 104 | type CacheVersion struct { 105 | Version string `json:"version"` // The version number of the provider. 106 | DownloadDetails []CacheVersionDownloadDetails `json:"download_details"` 107 | Protocols []string `json:"protocols"` // The protocol versions the provider supports. 108 | } 109 | 110 | // ToVersion converts a CacheVersion to a Version to be used in the provider version listing endpoint. 111 | func (v *CacheVersion) ToVersion() Version { 112 | platforms := make([]platform.Platform, len(v.DownloadDetails)) 113 | for i, d := range v.DownloadDetails { 114 | platforms[i] = d.Platform 115 | } 116 | 117 | return Version{ 118 | Version: v.Version, 119 | Protocols: v.Protocols, 120 | Platforms: platforms, 121 | } 122 | } 123 | 124 | // GetVersionDetails gets the VersionDetails for a specific OS and architecture. 125 | // Note: The result of this function will be missing the SigningKeys field. 126 | func (v *CacheVersion) GetVersionDetails(os, arch string) *VersionDetails { 127 | for _, d := range v.DownloadDetails { 128 | if d.Platform.OS == os && d.Platform.Arch == arch { 129 | return &VersionDetails{ 130 | Protocols: v.Protocols, 131 | OS: d.Platform.OS, 132 | Arch: d.Platform.Arch, 133 | Filename: d.Filename, 134 | DownloadURL: d.DownloadURL, 135 | SHASumsURL: d.SHASumsURL, 136 | SHASumsSignatureURL: d.SHASumsSignatureURL, 137 | SHASum: d.SHASum, 138 | SigningKeys: SigningKeys{}, 139 | } 140 | } 141 | } 142 | 143 | return nil 144 | } 145 | 146 | // CacheVersionDownloadDetails provides comprehensive details about a specific provider version. 147 | type CacheVersionDownloadDetails struct { 148 | Platform platform.Platform `json:"platform"` // The platform 149 | Filename string `json:"filename"` // The filename of the provider binary. 150 | DownloadURL string `json:"download_url"` // The direct URL to download the provider binary. 151 | SHASumsURL string `json:"shasums_url"` // The URL to the SHA checksums file. 152 | SHASumsSignatureURL string `json:"shasums_signature_url"` // The URL to the GPG signature of the SHA checksums file. 153 | SHASum string `json:"shasum"` // The SHA checksum of the provider binary. 154 | } 155 | -------------------------------------------------------------------------------- /src/internal/providers/types/types_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestDeduplicate(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | input VersionList 12 | expected VersionList 13 | }{ 14 | { 15 | name: "empty", 16 | input: VersionList{}, 17 | expected: VersionList{}, 18 | }, 19 | { 20 | name: "no duplicates", 21 | input: VersionList{ 22 | {Version: "1.0"}, 23 | {Version: "1.1"}, 24 | }, 25 | expected: VersionList{ 26 | {Version: "1.0"}, 27 | {Version: "1.1"}, 28 | }, 29 | }, 30 | { 31 | name: "with duplicates", 32 | input: VersionList{ 33 | {Version: "1.0"}, 34 | {Version: "1.1"}, 35 | {Version: "1.0"}, 36 | }, 37 | expected: VersionList{ 38 | {Version: "1.0"}, 39 | {Version: "1.1"}, 40 | }, 41 | }, 42 | } 43 | 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | got := tt.input.Deduplicate() 47 | if !reflect.DeepEqual(got, tt.expected) { 48 | t.Errorf("Deduplicate() = %v, want %v", got, tt.expected) 49 | } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/internal/providers/utils.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/aws/aws-xray-sdk-go/xray" 10 | "github.com/opentofu/registry/internal/github" 11 | "github.com/opentofu/registry/internal/platform" 12 | "golang.org/x/exp/slog" 13 | ) 14 | 15 | func getShaSum(ctx context.Context, downloadURL string, filename string) (shaSum string, err error) { 16 | err = xray.Capture(ctx, "filename.shasum", func(tracedCtx context.Context) error { 17 | xray.AddAnnotation(tracedCtx, "filename", filename) 18 | 19 | assetContents, assetErr := github.DownloadAssetContents(tracedCtx, downloadURL) 20 | if assetErr != nil { 21 | return fmt.Errorf("failed to download asset contents: %w", assetErr) 22 | } 23 | 24 | contents, contentsErr := io.ReadAll(assetContents) 25 | if err != nil { 26 | return fmt.Errorf("failed to read asset contents: %w", contentsErr) 27 | } 28 | 29 | shaSum = findShaSum(contents, filename, shaSum) 30 | 31 | return nil 32 | }) 33 | 34 | return shaSum, err 35 | } 36 | 37 | func findShaSum(contents []byte, filename string, shaSum string) string { 38 | lines := strings.Split(string(contents), "\n") 39 | 40 | for _, line := range lines { 41 | if strings.HasSuffix(line, filename) { 42 | shaSum = strings.Split(line, " ")[0] 43 | break 44 | } 45 | } 46 | return shaSum 47 | } 48 | 49 | func getSupportedArchAndOS(assets []github.ReleaseAsset) []platform.Platform { 50 | var platforms []platform.Platform 51 | slog.Info("Finding supported platforms", "assets", len(assets)) 52 | for _, asset := range assets { 53 | slog.Info("Extracting platform from asset", "asset", asset) 54 | platform := platform.ExtractPlatformFromArtifact(asset.Name) 55 | if platform == nil { 56 | continue 57 | } 58 | slog.Info("Platform identified", "platform", platform) 59 | platforms = append(platforms, *platform) 60 | } 61 | slog.Info("Supported platforms found", "platforms", len(platforms)) 62 | return platforms 63 | } 64 | -------------------------------------------------------------------------------- /src/internal/providers/utils_test.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFindShaSum(t *testing.T) { 8 | contents := `0002dd4c79453da5bf1bb9c52172a25d042a571f6df131b7c9ced3d1f8f3eb44 terraform-provider-random_3.5.1_linux_386.zip 9 | 49b0f8c2bd5632799aa6113e0e46acaa7d008f927665a41a1f8e8559fe6d8165 terraform-provider-random_3.5.1_darwin_amd64.zip 10 | 56df70fca236caa06d0e636c41ab71dd1ced05375f4ddcb905b0ed2105737048 terraform-provider-random_3.5.1_windows_386.zip 11 | 58e4de40540c86b9e2e2595dac1318ba057718961a467fa9727866f747693eb2 terraform-provider-random_3.5.1_windows_arm64.zip 12 | 5992f11c738812ccd7476d4c607cb8b76dea5aa612be491150c89957ec395ddd terraform-provider-random_3.5.1_darwin_arm64.zip 13 | 7ff4f0b7707b51737f684e96d85a47f0dd8be0f72a3c27b0798755d3faad15e2 terraform-provider-random_3.5.1_linux_arm.zip 14 | 8e4b0972e216c9773ab525accfa36eb27c44c751b06b125ecc53f4226c91cea8 terraform-provider-random_3.5.1_linux_arm64.zip 15 | d8956cc5abcd5d1173b6cc25d5d8ed2c5cc456edab2fddb774a17d45e84820cb terraform-provider-random_3.5.1_linux_amd64.zip 16 | df7f9eb93a832e66bc20cc41c57d38954f87671ec60be09fa866273adb8d9353 terraform-provider-random_3.5.1_windows_amd64.zip 17 | eb583d8f03b11f0b6c535375d8ed0d29e5f7f537b5c78943856d2e8ce76482d9 terraform-provider-random_3.5.1_windows_arm.zip 18 | ` 19 | 20 | filename := "terraform-provider-random_3.5.1_linux_amd64.zip" 21 | shaSum := findShaSum([]byte(contents), filename, "") 22 | if shaSum != "d8956cc5abcd5d1173b6cc25d5d8ed2c5cc456edab2fddb774a17d45e84820cb" { 23 | t.Fatal("shaSum not found") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/internal/providers/versions.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/aws/aws-xray-sdk-go/xray" 12 | "github.com/opentofu/registry/internal/github" 13 | "github.com/opentofu/registry/internal/platform" 14 | "github.com/opentofu/registry/internal/providers/types" 15 | "github.com/shurcooL/githubv4" 16 | "golang.org/x/exp/slog" 17 | ) 18 | 19 | type versionResult struct { 20 | Version types.CacheVersion 21 | Err error 22 | } 23 | 24 | // GetVersions fetches and returns a list of available versions of a given provider hosted on GitHub. 25 | // The returned versions also include information about supported platforms and the Terraform protocol versions they are compatible with. 26 | // 27 | // Parameters: 28 | // - ctx: The context used to control cancellations and timeouts. 29 | // - ghClient: The GitHub GraphQL client to interact with the GitHub GraphQL API. 30 | // - namespace: The GitHub namespace (typically, the organization or user) under which the provider repository is hosted. 31 | // - name: The name of the provider repository. 32 | // - since: The time after which to fetch versions. If nil, it fetches all versions. 33 | // 34 | // Returns a slice of Version structures detailing each available version. If an error occurs during fetching or processing, it returns an error. 35 | func GetVersions(ctx context.Context, ghClient *githubv4.Client, namespace string, name string, since *time.Time) (versions types.VersionList, err error) { 36 | err = xray.Capture(ctx, "provider.versions", func(tracedCtx context.Context) error { 37 | xray.AddAnnotation(tracedCtx, "namespace", namespace) 38 | xray.AddAnnotation(tracedCtx, "name", name) 39 | 40 | slog.Info("Fetching versions") 41 | 42 | releases, releasesErr := github.FetchReleases(tracedCtx, ghClient, namespace, name, since) 43 | if releasesErr != nil { 44 | return fmt.Errorf("failed to fetch releases: %w", releasesErr) 45 | } 46 | 47 | // if the releases slice is empty, we can't do anything 48 | // so, we should just return an empty slice 49 | if len(releases) == 0 { 50 | slog.Info("No releases found") 51 | return nil 52 | } 53 | 54 | versionCh := make(chan versionResult, len(releases)) 55 | 56 | var wg sync.WaitGroup 57 | 58 | for _, release := range releases { 59 | wg.Add(1) 60 | go func(r github.GHRelease) { 61 | defer wg.Done() 62 | getVersionFromGithubRelease(tracedCtx, r, versionCh) 63 | }(release) 64 | } 65 | 66 | // Close the channel when all goroutines are done. 67 | wg.Wait() 68 | close(versionCh) 69 | 70 | for vr := range versionCh { 71 | if vr.Err != nil { 72 | slog.Error("Failed to process some releases", "error", vr.Err) 73 | // we should not fail the entire operation if we can't process a single release 74 | // this is because some GitHub releases may not have the correct assets attached, 75 | // and therefore we should just log and skip them 76 | xrayErr := xray.AddError(tracedCtx, fmt.Errorf("failed to process some releases: %w", vr.Err)) 77 | if xrayErr != nil { 78 | return fmt.Errorf("failed to add error to trace: %w", err) 79 | } 80 | } else if vr.Version.Version != "" && len(vr.Version.DownloadDetails) > 0 { 81 | // only add the final list of versions if it's populated and has platforms attached 82 | versions = append(versions, vr.Version) 83 | } 84 | } 85 | return nil 86 | }) 87 | 88 | slog.Info("Successfully found versions", "versions", len(versions)) 89 | return versions, nil 90 | } 91 | 92 | // getVersionFromGithubRelease fetches and returns detailed information about a specific version of a provider hosted on GitHub. 93 | // all results are passed back to the versionCh channel. 94 | func getVersionFromGithubRelease(ctx context.Context, r github.GHRelease, versionCh chan versionResult) { 95 | result := versionResult{} 96 | 97 | logger := slog.Default().With("version", r.TagName) 98 | 99 | logger.Info("Processing release") 100 | 101 | assets := r.ReleaseAssets.Nodes 102 | platforms := getSupportedArchAndOS(assets) 103 | 104 | // if there are no platforms, we can't do anything with this release 105 | // so, we should just skip 106 | if len(platforms) == 0 { 107 | return 108 | } 109 | 110 | protocols := []string{"5.0"} 111 | 112 | logger.Info("Fetching manifest") 113 | // Read the manifest so that we can get the protocol versions. 114 | manifest, manifestErr := findAndParseManifest(ctx, assets) 115 | if manifestErr != nil { 116 | logger.Error("Failed to find and parse manifest", "error", manifestErr) 117 | result.Err = fmt.Errorf("failed to find and parse manifest: %w", manifestErr) 118 | versionCh <- result 119 | return 120 | } 121 | 122 | // attach the protocol versions to the version result 123 | if manifest != nil { 124 | slog.Info("Found manifest", "protocols", manifest.Metadata.ProtocolVersions) 125 | protocols = manifest.Metadata.ProtocolVersions 126 | } 127 | 128 | slog.Info("Fetching shasums") 129 | // download the shasums file so that we can get the checksum for each platform 130 | shaSums, err := downloadShaSums(ctx, assets) 131 | if err != nil { 132 | slog.Error("Failed to download shasums", "error", err) 133 | result.Err = fmt.Errorf("failed to download shasums: %w", err) 134 | versionCh <- result 135 | return 136 | } 137 | 138 | slog.Info("Found shasums", "shasums", len(shaSums)) 139 | 140 | shaSumsURL := github.FindAssetBySuffix(assets, "_SHA256SUMS") 141 | shaSumsSignatureURL := github.FindAssetBySuffix(assets, "_SHA256SUMS.sig") 142 | 143 | if shaSumsSignatureURL == nil { 144 | // make an empty one 145 | shaSumsSignatureURL = &github.ReleaseAsset{ 146 | DownloadURL: "", 147 | } 148 | } 149 | 150 | downloadDetails := make([]types.CacheVersionDownloadDetails, 0, len(platforms)) 151 | // for each of the supported platforms, we need to find the appropriate assets 152 | // and add them to the version result 153 | for _, platform := range platforms { 154 | slog.Info("Fetching download details", "platform", fmt.Sprintf("%s_%s", platform.OS, platform.Arch)) 155 | details := getVersionDownloadDetails(platform, assets, shaSums) 156 | if details != nil { 157 | details.SHASumsURL = shaSumsURL.DownloadURL 158 | details.SHASumsSignatureURL = shaSumsSignatureURL.DownloadURL 159 | downloadDetails = append(downloadDetails, *details) 160 | } 161 | } 162 | 163 | // only populate the version if we have all download details 164 | result.Version = types.CacheVersion{ 165 | Version: strings.TrimPrefix(r.TagName, "v"), 166 | Protocols: protocols, 167 | DownloadDetails: downloadDetails, 168 | } 169 | 170 | versionCh <- result 171 | } 172 | 173 | func getVersionDownloadDetails(platform platform.Platform, assets []github.ReleaseAsset, shaSums map[string]string) *types.CacheVersionDownloadDetails { 174 | // find the asset for the given platform 175 | asset := github.FindAssetBySuffix(assets, fmt.Sprintf("_%s_%s.zip", platform.OS, platform.Arch)) 176 | if asset == nil { 177 | slog.Warn("Could not find asset for platform", "platform", platform) 178 | return nil 179 | } 180 | 181 | // get the shasum for the asset 182 | shasum, ok := shaSums[asset.Name] 183 | if !ok { 184 | slog.Warn("Could not find shasum for asset", "asset", asset.Name) 185 | return nil 186 | } 187 | 188 | return &types.CacheVersionDownloadDetails{ 189 | Platform: platform, 190 | Filename: asset.Name, 191 | DownloadURL: asset.DownloadURL, 192 | SHASumsURL: "", 193 | SHASumsSignatureURL: "", 194 | SHASum: shasum, 195 | } 196 | } 197 | 198 | func downloadShaSums(ctx context.Context, assets []github.ReleaseAsset) (map[string]string, error) { 199 | asset := github.FindAssetBySuffix(assets, "_SHA256SUMS") 200 | if asset == nil { 201 | return nil, fmt.Errorf("could not find shasums asset") 202 | } 203 | 204 | // download the asset 205 | sumsContent, assetErr := github.DownloadAssetContents(ctx, asset.DownloadURL) 206 | if assetErr != nil { 207 | return nil, fmt.Errorf("failed to download asset: %w", assetErr) 208 | } 209 | defer sumsContent.Close() 210 | 211 | sums := make(map[string]string) 212 | 213 | // read the contents of the shasums file 214 | scanner := bufio.NewScanner(sumsContent) 215 | for scanner.Scan() { 216 | // read the line 217 | parts := strings.Fields(scanner.Text()) 218 | if len(parts) != 2 { //nolint:gomnd // we expect 2 parts 219 | continue 220 | } 221 | 222 | // the first part is the shasum, the second part is the filename 223 | // we want to return a map of filename -> shasum 224 | // so we can easily look up the shasum for a given filename 225 | // when we are processing the release assets 226 | if len(parts[1]) > 0 { 227 | sums[parts[1]] = parts[0] 228 | } 229 | } 230 | if err := scanner.Err(); err != nil { 231 | return nil, fmt.Errorf("failed to read asset contents: %w", err) 232 | } 233 | return sums, nil 234 | } 235 | 236 | // GetVersion fetches and returns detailed information about a specific version of a provider hosted on GitHub. 237 | // The returned information includes the download URL, the filename, SHA sums, and more details pertinent to the specific version, OS, and architecture. 238 | // 239 | // Parameters: 240 | // - ctx: The context used to control cancellations and timeouts. 241 | // - ghClient: The GitHub GraphQL client to interact with the GitHub GraphQL API. 242 | // - namespace: The GitHub namespace (typically, the organization or user) under which the provider repository is hosted. 243 | // - name: The name of the provider without the "terraform-provider-" prefix. 244 | // - version: The specific version of the Terraform provider to fetch details for. 245 | // - os: The operating system for which the provider binary is intended. 246 | // - arch: The architecture for which the provider binary is intended. 247 | // 248 | // Returns a VersionDetails structure with detailed information about the specified version. If an error occurs during fetching or processing, it returns an error. 249 | 250 | func GetVersion(ctx context.Context, ghClient *githubv4.Client, namespace string, name string, version string, os string, arch string) (versionDetails *types.VersionDetails, err error) { 251 | err = xray.Capture(ctx, "provider.versiondetails", func(tracedCtx context.Context) error { 252 | xray.AddAnnotation(tracedCtx, "namespace", namespace) 253 | xray.AddAnnotation(tracedCtx, "name", name) 254 | xray.AddAnnotation(tracedCtx, "version", version) 255 | xray.AddAnnotation(tracedCtx, "OS", os) 256 | xray.AddAnnotation(tracedCtx, "arch", arch) 257 | 258 | slog.Info("Fetching version") 259 | 260 | // TODO: Replace this with a GetRelease, iterating all the releases is not efficient at all! 261 | // Fetch the specific release for the given version. 262 | release, releaseErr := github.FindRelease(tracedCtx, ghClient, namespace, name, version) 263 | if releaseErr != nil { 264 | return fmt.Errorf("failed to find release: %w", releaseErr) 265 | } 266 | 267 | if release == nil { 268 | return newFetchError("failed to find release", ErrCodeReleaseNotFound, nil) 269 | } 270 | 271 | // Initialize the VersionDetails struct. 272 | versionDetails = &types.VersionDetails{ 273 | OS: os, 274 | Arch: arch, 275 | } 276 | 277 | // Find and parse the manifest from the release assets. 278 | manifest, manifestErr := findAndParseManifest(tracedCtx, release.ReleaseAssets.Nodes) 279 | if manifestErr != nil { 280 | return newFetchError("failed to find and parse manifest", ErrCodeManifestNotFound, manifestErr) 281 | } 282 | 283 | if manifest != nil { 284 | versionDetails.Protocols = manifest.Metadata.ProtocolVersions 285 | } else { 286 | versionDetails.Protocols = []string{"5.0"} 287 | } 288 | 289 | // Identify the appropriate asset for download based on OS and architecture. 290 | assetToDownload := github.FindAssetBySuffix(release.ReleaseAssets.Nodes, fmt.Sprintf("_%s_%s.zip", os, arch)) 291 | if assetToDownload == nil { 292 | return newFetchError("failed to find asset to download", ErrCodeAssetNotFound, nil) 293 | } 294 | versionDetails.Filename = assetToDownload.Name 295 | versionDetails.DownloadURL = assetToDownload.DownloadURL 296 | 297 | // Locate the SHA256 checksums and its signature from the release assets. 298 | shaSumsAsset := github.FindAssetBySuffix(release.ReleaseAssets.Nodes, "_SHA256SUMS") 299 | shasumsSigAsset := github.FindAssetBySuffix(release.ReleaseAssets.Nodes, "_SHA256SUMS.sig") 300 | 301 | if shaSumsAsset == nil || shasumsSigAsset == nil { 302 | slog.Error("Could not find shasums or its signature asset") 303 | return newFetchError("failed to find shasums or its signature asset", ErrCodeSHASumsNotFound, nil) 304 | } 305 | 306 | versionDetails.SHASumsURL = shaSumsAsset.DownloadURL 307 | versionDetails.SHASumsSignatureURL = shasumsSigAsset.DownloadURL 308 | 309 | // Extract the SHA256 checksum for the asset to download. 310 | shaSum, shaSumErr := getShaSum(tracedCtx, shaSumsAsset.DownloadURL, versionDetails.Filename) 311 | if shaSumErr != nil { 312 | slog.Error("Could not get shasum", "error", shaSumErr) 313 | return newFetchError("failed to get shasum: %w", ErrCodeSHASumsNotFound, shaSumErr) 314 | } 315 | versionDetails.SHASum = shaSum 316 | 317 | publicKeys, keysErr := KeysForNamespace(namespace) 318 | if keysErr != nil { 319 | slog.Error("Could not get public keys", "error", keysErr) 320 | return newFetchError("failed to get public keys", ErrCodeCouldNotGetPublicKeys, keysErr) 321 | } 322 | 323 | versionDetails.SigningKeys = types.SigningKeys{ 324 | GPGPublicKeys: publicKeys, 325 | } 326 | 327 | return nil 328 | }) 329 | 330 | slog.Info("Successfully found version details") 331 | return versionDetails, err 332 | } 333 | -------------------------------------------------------------------------------- /src/internal/secrets/secretsmanager.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/secretsmanager" 10 | ) 11 | 12 | type Handler struct { 13 | client *secretsmanager.Client 14 | } 15 | 16 | func NewHandler(awsConfig aws.Config) *Handler { 17 | client := secretsmanager.NewFromConfig(awsConfig) 18 | return &Handler{client: client} 19 | } 20 | 21 | func (s *Handler) GetValue(ctx context.Context, secretName string) (string, error) { 22 | value, err := s.client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ 23 | SecretId: aws.String(secretName), 24 | }) 25 | if err != nil { 26 | return "", err 27 | } 28 | return *value.SecretString, nil 29 | } 30 | 31 | func (s *Handler) GetSecretValueFromEnvReference(ctx context.Context, envVarName string) (string, error) { 32 | envVarValue := os.Getenv(envVarName) 33 | if envVarValue == "" { 34 | return "", fmt.Errorf("%s environment variable not set", envVarName) 35 | } 36 | 37 | var value string 38 | value, err := s.GetValue(ctx, envVarValue) 39 | if err != nil { 40 | return "", fmt.Errorf("could not get secret: %w", err) 41 | } 42 | 43 | if value == "" { 44 | return "", fmt.Errorf("empty value fetched from secrets manager") 45 | } 46 | return value, nil 47 | } 48 | -------------------------------------------------------------------------------- /src/internal/warnings/warnings.go: -------------------------------------------------------------------------------- 1 | // Package warnings defines the warnings associated with the provider 2 | package warnings 3 | 4 | // ProviderWarnings return the list of warnings for a given provider identified by its namespace and type 5 | // 6 | // Example: registry.terraform.io/hashicorp/terraform 7 | // 8 | // warn := ProviderWarnings("hashicorp", "terraform") 9 | // fmt.Println(warn) 10 | // >> [This provider is archived and no longer needed. The terraform_remote_state data source is built into the latest OpenTofu release.] 11 | func ProviderWarnings(providerNamespace, providerType string) []string { 12 | switch providerNamespace { //nolint:gocritic // Switch is more appropriate than 'if' for the use case 13 | case "hashicorp": 14 | switch providerType { //nolint:gocritic // Switch is more appropriate than 'if' for the use case 15 | case "terraform": 16 | return []string{`This provider is archived and no longer needed. The terraform_remote_state data source is built into the latest OpenTofu release.`} 17 | } 18 | } 19 | 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /src/internal/warnings/warnings_test.go: -------------------------------------------------------------------------------- 1 | package warnings 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestProviderWarnings(t *testing.T) { 9 | type args struct { 10 | providerNamespace string 11 | providerType string 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want []string 17 | }{ 18 | { 19 | name: "shall return no warnings", 20 | args: args{ 21 | providerNamespace: "foo", 22 | providerType: "bar", 23 | }, 24 | want: nil, 25 | }, 26 | { 27 | name: "shall return warnings as in https://github.com/opentofu/registry/issues/108", 28 | args: args{ 29 | providerNamespace: "hashicorp", 30 | providerType: "terraform", 31 | }, 32 | want: []string{`This provider is archived and no longer needed. The terraform_remote_state data source is built into the latest OpenTofu release.`}, 33 | }, 34 | } 35 | for _, tt := range tests { 36 | t.Run( 37 | tt.name, func(t *testing.T) { 38 | if got := ProviderWarnings(tt.args.providerNamespace, tt.args.providerType); !reflect.DeepEqual(got, tt.want) { 39 | t.Errorf("ProviderWarnings() = %v, want %v", got, tt.want) 40 | } 41 | }, 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lambda/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-lambda-go/events" 7 | "github.com/aws/aws-lambda-go/lambda" 8 | "github.com/opentofu/registry/internal/config" 9 | ) 10 | 11 | type LambdaFunc func(context.Context, events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) 12 | 13 | func main() { 14 | configBuilder := config.NewBuilder(config.WithProviderRedirects()) 15 | 16 | config, err := configBuilder.BuildConfig(context.Background(), "registry.buildconfig") 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | lambda.Start(Router(*config)) 22 | } 23 | -------------------------------------------------------------------------------- /src/lambda/api/moduleDownload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/opentofu/registry/internal/config" 9 | "golang.org/x/exp/slog" 10 | 11 | "github.com/aws/aws-lambda-go/events" 12 | 13 | "github.com/opentofu/registry/internal/github" 14 | "github.com/opentofu/registry/internal/modules" 15 | ) 16 | 17 | type DownloadModuleHandlerPathParams struct { 18 | Namespace string `json:"namespace"` 19 | Name string `json:"name"` 20 | System string `json:"system"` 21 | Version string `json:"version"` 22 | } 23 | 24 | func (p DownloadModuleHandlerPathParams) AnnotateLogger() { 25 | logger := slog.Default() 26 | logger = logger. 27 | With("namespace", p.Namespace). 28 | With("name", p.Name). 29 | With("system", p.System). 30 | With("version", p.Version) 31 | slog.SetDefault(logger) 32 | } 33 | 34 | func downloadModuleVersion(config config.Config) LambdaFunc { 35 | return func(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 36 | params := getDownloadModuleHandlerPathParams(req) 37 | params.AnnotateLogger() 38 | repoName := modules.GetRepoName(params.System, params.Name) 39 | 40 | // check if the repo exists 41 | exists, err := github.RepositoryExists(ctx, config.ManagedGithubClient, params.Namespace, repoName) 42 | if err != nil { 43 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 44 | } 45 | 46 | if !exists { 47 | return NotFoundResponse, nil 48 | } 49 | 50 | releaseTag, err := getReleaseTag(ctx, config, params.Namespace, repoName, params.Version) 51 | if err != nil { 52 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 53 | } 54 | 55 | return events.APIGatewayProxyResponse{StatusCode: http.StatusNoContent, Body: "", Headers: map[string]string{ 56 | "X-Terraform-Get": fmt.Sprintf("git::https://github.com/%s/%s?ref=%s", params.Namespace, repoName, releaseTag), 57 | }}, nil 58 | } 59 | } 60 | 61 | func getDownloadModuleHandlerPathParams(req events.APIGatewayProxyRequest) DownloadModuleHandlerPathParams { 62 | return DownloadModuleHandlerPathParams{ 63 | Namespace: req.PathParameters["namespace"], 64 | Name: req.PathParameters["name"], 65 | System: req.PathParameters["system"], 66 | Version: req.PathParameters["version"], 67 | } 68 | } 69 | 70 | func getReleaseTag(ctx context.Context, config config.Config, namespace string, repoName string, version string) (string, error) { 71 | // TODO: Create a modulecache, similar to the providercache, and use it here to avoid unnecessary API calls to GitHub 72 | // First we check if a tag with "v" prefix exists in GitHub 73 | release, err := github.FindRelease(ctx, config.RawGithubv4Client, namespace, repoName, version) 74 | if err != nil { 75 | return "", err 76 | } 77 | 78 | // If the release exists, then the tag does have the "v" prefix 79 | // If it does not, then we assume the tag exists without the "v" prefix 80 | if release != nil { 81 | return fmt.Sprintf("v%s", version), nil 82 | } 83 | 84 | return version, nil 85 | } 86 | -------------------------------------------------------------------------------- /src/lambda/api/moduleVersions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/opentofu/registry/internal/config" 9 | "golang.org/x/exp/slog" 10 | 11 | "github.com/aws/aws-lambda-go/events" 12 | 13 | "github.com/opentofu/registry/internal/github" 14 | "github.com/opentofu/registry/internal/modules" 15 | ) 16 | 17 | type ListModuleVersionsPathParams struct { 18 | Namespace string `json:"namespace"` 19 | Name string `json:"name"` 20 | System string `json:"system"` 21 | } 22 | 23 | func (p ListModuleVersionsPathParams) AnnotateLogger() { 24 | logger := slog.Default() 25 | logger = logger. 26 | With("namespace", p.Namespace). 27 | With("name", p.Name). 28 | With("system", p.System) 29 | slog.SetDefault(logger) 30 | } 31 | 32 | func getListModuleVersionsPathParams(req events.APIGatewayProxyRequest) ListModuleVersionsPathParams { 33 | return ListModuleVersionsPathParams{ 34 | Namespace: req.PathParameters["namespace"], 35 | Name: req.PathParameters["name"], 36 | System: req.PathParameters["system"], 37 | } 38 | } 39 | 40 | type ListModuleVersionsResponse struct { 41 | Modules []ModulesResponse `json:"modules"` 42 | } 43 | 44 | type ModulesResponse struct { 45 | Versions []modules.Version `json:"versions"` 46 | } 47 | 48 | func listModuleVersions(config config.Config) LambdaFunc { 49 | return func(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 50 | params := getListModuleVersionsPathParams(req) 51 | params.AnnotateLogger() 52 | repoName := modules.GetRepoName(params.System, params.Name) 53 | 54 | // check the repo exists 55 | exists, err := github.RepositoryExists(ctx, config.ManagedGithubClient, params.Namespace, repoName) 56 | if err != nil { 57 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 58 | } 59 | if !exists { 60 | return NotFoundResponse, nil 61 | } 62 | 63 | // TODO: Implement ddb caching similar to provider versions, but for modules 64 | // this will also allow us to populate the `since` parameter in the module.GetVersions call below 65 | 66 | // fetch all the versions 67 | versions, err := modules.GetVersions(ctx, config.RawGithubv4Client, params.Namespace, repoName, nil) 68 | if err != nil { 69 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 70 | } 71 | 72 | response := ListModuleVersionsResponse{ 73 | Modules: []ModulesResponse{ 74 | { 75 | Versions: versions, 76 | }, 77 | }, 78 | } 79 | 80 | resBody, err := json.Marshal(response) 81 | if err != nil { 82 | slog.Error("Error marshalling response", "error", err) 83 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 84 | } 85 | return events.APIGatewayProxyResponse{StatusCode: http.StatusOK, Body: string(resBody)}, nil 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/lambda/api/providerDownload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/opentofu/registry/internal/config" 11 | "github.com/opentofu/registry/internal/providers/types" 12 | "golang.org/x/exp/slog" 13 | 14 | "github.com/aws/aws-lambda-go/events" 15 | 16 | "github.com/opentofu/registry/internal/github" 17 | "github.com/opentofu/registry/internal/providers" 18 | ) 19 | 20 | type DownloadHandlerPathParams struct { 21 | Architecture string `json:"arch"` 22 | OS string `json:"os"` 23 | Namespace string `json:"namespace"` 24 | Type string `json:"type"` 25 | Version string `json:"version"` 26 | } 27 | 28 | func (p DownloadHandlerPathParams) AnnotateLogger() { 29 | logger := slog.Default() 30 | logger = logger. 31 | With("namespace", p.Namespace). 32 | With("type", p.Type). 33 | With("version", p.Version). 34 | With("os", p.OS). 35 | With("arch", p.Architecture) 36 | slog.SetDefault(logger) 37 | } 38 | 39 | func getDownloadPathParams(req events.APIGatewayProxyRequest) DownloadHandlerPathParams { 40 | return DownloadHandlerPathParams{ 41 | Architecture: req.PathParameters["arch"], 42 | OS: req.PathParameters["os"], 43 | Namespace: req.PathParameters["namespace"], 44 | Type: req.PathParameters["type"], 45 | Version: req.PathParameters["version"], 46 | } 47 | } 48 | 49 | func downloadProviderVersion(config config.Config) LambdaFunc { 50 | return func(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 51 | params := getDownloadPathParams(req) 52 | params.AnnotateLogger() 53 | effectiveNamespace := config.EffectiveProviderNamespace(params.Namespace) 54 | 55 | // Construct the repo name. 56 | repoName := providers.GetRepoName(params.Type) 57 | 58 | // For now, we will ignore errors from the cache and just fetch from GH instead 59 | document, _ := config.ProviderVersionCache.GetItem(ctx, fmt.Sprintf("%s/%s", effectiveNamespace, params.Type)) 60 | if document != nil { 61 | return processDocumentForProviderDownload(document, effectiveNamespace, params) 62 | } 63 | 64 | // check the repo exists 65 | exists, err := github.RepositoryExists(ctx, config.ManagedGithubClient, effectiveNamespace, repoName) 66 | if err != nil { 67 | slog.Error("Error checking if repo exists", "error", err) 68 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 69 | } 70 | if !exists { 71 | slog.Info("Repo does not exist") 72 | return NotFoundResponse, nil 73 | } 74 | 75 | // if the document didn't exist in the cache, trigger the lambda to populate it and return the current results from GH 76 | if triggerErr := triggerPopulateProviderVersions(ctx, config, effectiveNamespace, params.Type); triggerErr != nil { 77 | slog.Error("Error triggering lambda", "error", triggerErr) 78 | } 79 | 80 | return fetchVersionFromGithub(ctx, config, effectiveNamespace, repoName, params) 81 | } 82 | } 83 | 84 | func fetchVersionFromGithub(ctx context.Context, config config.Config, effectiveNamespace string, repoName string, params DownloadHandlerPathParams) (events.APIGatewayProxyResponse, error) { 85 | versionDownloadResponse, err := providers.GetVersion(ctx, config.RawGithubv4Client, effectiveNamespace, repoName, params.Version, params.OS, params.Architecture) 86 | if err != nil { 87 | var fetchErr *providers.FetchError 88 | // if it's a providers.FetchError 89 | if errors.As(err, &fetchErr) { 90 | return handleFetchFromGithubErr(fetchErr) 91 | } 92 | 93 | slog.Error("Error getting version", "error", err) 94 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 95 | } 96 | 97 | resBody, err := json.Marshal(versionDownloadResponse) 98 | if err != nil { 99 | slog.Error("Error marshalling response", "error", err) 100 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 101 | } 102 | return events.APIGatewayProxyResponse{StatusCode: http.StatusOK, Body: string(resBody)}, nil 103 | } 104 | 105 | func handleFetchFromGithubErr(err *providers.FetchError) (events.APIGatewayProxyResponse, error) { 106 | if err.Code == providers.ErrCodeReleaseNotFound { 107 | slog.Info("Release not found in repo") 108 | return NotFoundResponse, nil 109 | } 110 | if err.Code == providers.ErrCodeAssetNotFound { 111 | slog.Info("Asset for download not found in release") 112 | return NotFoundResponse, nil 113 | } 114 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 115 | } 116 | 117 | func processDocumentForProviderDownload(document *types.CacheItem, effectiveNamespace string, params DownloadHandlerPathParams) (events.APIGatewayProxyResponse, error) { 118 | slog.Info("Found document in cache", "last_updated", document.LastUpdated, "versions", len(document.Versions)) 119 | 120 | // try and find the version in the document 121 | versionDetails, ok := document.GetVersionDetails(params.Version, params.OS, params.Architecture) 122 | if !ok { 123 | slog.Info("Version not found in document, returning 404", "version", params.Version) 124 | return NotFoundResponse, nil 125 | } 126 | 127 | // attach the signing keys 128 | publicKeys, keysErr := providers.KeysForNamespace(effectiveNamespace) 129 | if keysErr != nil { 130 | slog.Error("Could not get public keys", "error", keysErr) 131 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, keysErr 132 | } 133 | 134 | keys := types.SigningKeys{} 135 | keys.GPGPublicKeys = publicKeys 136 | 137 | versionDetails.SigningKeys = keys 138 | 139 | slog.Info("Found version in document", "version", params.Version) 140 | resBody, err := json.Marshal(versionDetails) 141 | if err != nil { 142 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 143 | } 144 | return events.APIGatewayProxyResponse{StatusCode: http.StatusOK, Body: string(resBody)}, nil 145 | } 146 | -------------------------------------------------------------------------------- /src/lambda/api/providerVersions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/aws/aws-lambda-go/events" 11 | "github.com/aws/aws-sdk-go-v2/aws" 12 | "github.com/aws/aws-sdk-go-v2/service/lambda" 13 | "github.com/opentofu/registry/internal/config" 14 | "github.com/opentofu/registry/internal/github" 15 | "github.com/opentofu/registry/internal/providers" 16 | "github.com/opentofu/registry/internal/providers/types" 17 | "github.com/opentofu/registry/internal/warnings" 18 | "golang.org/x/exp/slog" 19 | ) 20 | 21 | type ListProvidersPathParams struct { 22 | Namespace string `json:"namespace"` 23 | Type string `json:"name"` 24 | } 25 | 26 | func (p ListProvidersPathParams) AnnotateLogger() { 27 | logger := slog.Default() 28 | logger = logger. 29 | With("namespace", p.Namespace). 30 | With("type", p.Type) 31 | slog.SetDefault(logger) 32 | } 33 | 34 | func getListProvidersPathParams(req events.APIGatewayProxyRequest) ListProvidersPathParams { 35 | return ListProvidersPathParams{ 36 | Namespace: req.PathParameters["namespace"], 37 | Type: req.PathParameters["type"], 38 | } 39 | } 40 | 41 | type ListProviderVersionsResponse struct { 42 | Versions []types.Version `json:"versions"` 43 | Warnings []string `json:"warnings,omitempty"` 44 | } 45 | 46 | func listProviderVersions(config config.Config) LambdaFunc { 47 | return func(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 48 | params := getListProvidersPathParams(req) 49 | params.AnnotateLogger() 50 | 51 | effectiveNamespace := config.EffectiveProviderNamespace(params.Namespace) 52 | 53 | // Warnings lookup: https://github.com/opentofu/registry/issues/108 54 | warn := warnings.ProviderWarnings(params.Namespace, params.Type) 55 | 56 | // For now, we will ignore errors from the cache and just fetch from GH instead 57 | versionList, _ := listVersionsFromCache(ctx, config, effectiveNamespace, params.Type) 58 | if len(versionList) > 0 { 59 | return versionsResponse(versionList, warn) 60 | } 61 | 62 | versionList, repoExists, err := listVersionsFromRepository(ctx, config, effectiveNamespace, params.Type) 63 | if !repoExists { 64 | if err != nil { 65 | slog.Error("Error checking if repo exists", "error", err) 66 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 67 | } 68 | slog.Info("Repo does not exist") 69 | // if the repo doesn't exist, there's no point in trying to fetch versions 70 | return NotFoundResponse, nil 71 | } 72 | if err != nil { 73 | slog.Error("Error fetching versions from github", "error", err) 74 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 75 | } 76 | 77 | // if the document didn't exist in the cache, trigger the lambda to populate it 78 | if err := triggerPopulateProviderVersions(ctx, config, effectiveNamespace, params.Type); err != nil { 79 | slog.Error("Error triggering lambda", "error", err) 80 | } 81 | 82 | return versionsResponse(versionList, warn) 83 | } 84 | } 85 | 86 | // listVersionsFromCache retrieves version details for a given effective namespace and provider type from the cache. 87 | // - If the cached document is not present or there's an error during retrieval, the function returns an error. 88 | // - If the cached document is present and is not stale, the cached versions are returned directly. 89 | // - If the cached document is present and is detected as stale: 90 | // - An asynchronous update via a lambda function is triggered. 91 | // - The stale version details are returned. 92 | func listVersionsFromCache(ctx context.Context, config config.Config, effectiveNamespace, providerType string) ([]types.Version, error) { 93 | document, err := config.ProviderVersionCache.GetItem(ctx, fmt.Sprintf("%s/%s", effectiveNamespace, providerType)) 94 | if err != nil || document == nil { 95 | return nil, err 96 | } 97 | 98 | slog.Info("Found document in cache", "last_updated", document.LastUpdated, "versions", len(document.Versions)) 99 | 100 | if document.IsStale() { 101 | // if it's stale, trigger the lambda to update, and still return the stale document 102 | slog.Info("Document is stale, returning cached versions and triggering lambda", "last_updated", document.LastUpdated) 103 | if triggerErr := triggerPopulateProviderVersions(ctx, config, effectiveNamespace, providerType); triggerErr != nil { 104 | slog.Error("Error triggering lambda", "error", triggerErr) 105 | } 106 | } 107 | 108 | // if it's stale or not, we still return the cached versions 109 | return document.Versions.ToVersions(), nil 110 | } 111 | 112 | func listVersionsFromRepository(ctx context.Context, config config.Config, effectiveNamespace, providerType string) ([]types.Version, bool, error) { 113 | repoName := providers.GetRepoName(providerType) 114 | exists, err := github.RepositoryExists(ctx, config.ManagedGithubClient, effectiveNamespace, repoName) 115 | if err != nil { 116 | return nil, exists, err 117 | } 118 | 119 | slog.Info("Fetching versions from github\n") 120 | versionList, err := providers.GetVersions(ctx, config.RawGithubv4Client, effectiveNamespace, repoName, nil) 121 | return versionList.ToVersions(), exists, err 122 | } 123 | 124 | func triggerPopulateProviderVersions(ctx context.Context, config config.Config, effectiveNamespace string, effectiveType string) error { 125 | slog.Info("Invoking populate provider versions lambda asynchronously to update dynamodb document\n") 126 | // invoke the async lambda to update the dynamodb document 127 | _, err := config.LambdaClient.Invoke(ctx, &lambda.InvokeInput{ 128 | FunctionName: aws.String(os.Getenv("POPULATE_PROVIDER_VERSIONS_FUNCTION_NAME")), 129 | InvocationType: "Event", // Event == async 130 | Payload: []byte(fmt.Sprintf("{\"namespace\": \"%s\", \"type\": \"%s\"}", effectiveNamespace, effectiveType)), 131 | }) 132 | if err != nil { 133 | slog.Error("Error invoking lambda", "error", err) 134 | return err 135 | } 136 | return nil 137 | } 138 | 139 | func versionsResponse(versions []types.Version, warnings []string) (events.APIGatewayProxyResponse, error) { 140 | response := ListProviderVersionsResponse{ 141 | Versions: versions, 142 | } 143 | 144 | if len(warnings) > 0 { 145 | response.Warnings = warnings 146 | } 147 | 148 | resBody, err := json.Marshal(response) 149 | if err != nil { 150 | return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err 151 | } 152 | 153 | return events.APIGatewayProxyResponse{StatusCode: http.StatusOK, Body: string(resBody)}, nil 154 | } 155 | -------------------------------------------------------------------------------- /src/lambda/api/responses.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/aws/aws-lambda-go/events" 7 | ) 8 | 9 | //nolint:gochecknoglobals // This should be treated as a constant. 10 | var NotFoundResponse = events.APIGatewayProxyResponse{StatusCode: http.StatusNotFound, Body: `{"errors":["not found"]}`} 11 | -------------------------------------------------------------------------------- /src/lambda/api/router.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "regexp" 9 | 10 | "github.com/aws/aws-xray-sdk-go/xray" 11 | "github.com/opentofu/registry/internal/config" 12 | "golang.org/x/exp/slog" 13 | 14 | "github.com/aws/aws-lambda-go/events" 15 | ) 16 | 17 | func RouteHandlers(config config.Config) map[string]LambdaFunc { 18 | return map[string]LambdaFunc{ 19 | // Download provider version 20 | // `/v1/providers/{namespace}/{type}/{version}/download/{os}/{arch}` 21 | "^/v1/providers/[^/]+/[^/]+/[^/]+/download/[^/]+/[^/]+$": downloadProviderVersion(config), 22 | 23 | // List provider versions 24 | // `/v1/providers/{namespace}/{type}/versions` 25 | "^/v1/providers/[^/]+/[^/]+/versions$": listProviderVersions(config), 26 | 27 | // List module versions 28 | // `/v1/modules/{namespace}/{name}/{system}/versions` 29 | "^/v1/modules/[^/]+/[^/]+/[^/]+/versions$": listModuleVersions(config), 30 | 31 | // Download module version 32 | // `/v1/modules/{namespace}/{name}/{system}/{version}/download` 33 | "^/v1/modules/[^/]+/[^/]+/[^/]+/[^/]+/download$": downloadModuleVersion(config), 34 | 35 | // .well-known/terraform.json 36 | "^/.well-known/terraform.json$": terraformWellKnownMetadataHandler(config), 37 | } 38 | } 39 | 40 | func getRouteHandler(config config.Config, path string) LambdaFunc { 41 | // We will replace this with some sort of actual router (chi, gorilla, etc) 42 | // for now regex is fine 43 | for route, handler := range RouteHandlers(config) { 44 | if match, _ := regexp.MatchString(route, path); match { 45 | return handler 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func Router(config config.Config) LambdaFunc { 52 | return func(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 53 | ctx, segment := xray.BeginSubsegment(ctx, "registry.handle") 54 | 55 | logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 56 | logger = logger. 57 | With("request_id", req.RequestContext.RequestID). 58 | With("path", req.Path) 59 | slog.SetDefault(logger) 60 | 61 | handler := getRouteHandler(config, req.Path) 62 | if handler == nil { 63 | slog.Error("No route handler found for path") 64 | return events.APIGatewayProxyResponse{StatusCode: http.StatusNotFound, Body: fmt.Sprintf("No route handler found for path %s", req.Path)}, nil 65 | } 66 | 67 | response, err := handler(ctx, req) 68 | segment.Close(err) 69 | 70 | slog.Info("Returning response", "status_code", response.StatusCode) 71 | return response, err 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/lambda/api/terraformWellKnown.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/opentofu/registry/internal/config" 9 | ) 10 | 11 | const wellKnownMetadataResponse = `{ 12 | "modules.v1": "/v1/modules/", 13 | "providers.v1": "/v1/providers/" 14 | }` 15 | 16 | func terraformWellKnownMetadataHandler(_ config.Config) LambdaFunc { 17 | return func(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 18 | return events.APIGatewayProxyResponse{ 19 | StatusCode: http.StatusOK, 20 | Body: wellKnownMetadataResponse, 21 | }, nil 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lambda/populate_provider_versions/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/aws/aws-xray-sdk-go/xray" 10 | "github.com/opentofu/registry/internal/config" 11 | "github.com/opentofu/registry/internal/github" 12 | "github.com/opentofu/registry/internal/providers" 13 | "github.com/opentofu/registry/internal/providers/types" 14 | "golang.org/x/exp/slog" 15 | ) 16 | 17 | type PopulateProviderVersionsEvent struct { 18 | Namespace string `json:"namespace"` 19 | Type string `json:"type"` 20 | } 21 | 22 | func (p PopulateProviderVersionsEvent) Validate() error { 23 | if p.Namespace == "" { 24 | return fmt.Errorf("namespace is required") 25 | } 26 | if p.Type == "" { 27 | return fmt.Errorf("type is required") 28 | } 29 | return nil 30 | } 31 | 32 | type LambdaFunc func(ctx context.Context, e PopulateProviderVersionsEvent) (string, error) 33 | 34 | func setupLogging(e PopulateProviderVersionsEvent) { 35 | logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 36 | logger = logger. 37 | With("namespace", e.Namespace). 38 | With("type", e.Type) 39 | slog.SetDefault(logger) 40 | } 41 | 42 | func HandleRequest(config *config.Config) LambdaFunc { 43 | return func(ctx context.Context, e PopulateProviderVersionsEvent) (string, error) { 44 | setupLogging(e) 45 | 46 | var versions types.VersionList 47 | 48 | slog.Info("Populating provider versions") 49 | err := xray.Capture(ctx, "populate_provider_versions.handle", func(tracedCtx context.Context) error { 50 | xray.AddAnnotation(tracedCtx, "namespace", e.Namespace) 51 | xray.AddAnnotation(tracedCtx, "type", e.Type) 52 | 53 | err := e.Validate() 54 | if err != nil { 55 | slog.Error("invalid event", "error", err) 56 | return fmt.Errorf("invalid event: %w", err) 57 | } 58 | 59 | var since *time.Time 60 | 61 | // check if the document exists in dynamodb, if it does, and it's newer than the allowed max age, 62 | // we should treat it as a noop and just return 63 | document, err := config.ProviderVersionCache.GetItem(tracedCtx, fmt.Sprintf("%s/%s", e.Namespace, e.Type)) 64 | if err != nil { 65 | // if there was an error getting the document, that's fine. we'll just log it and carry on 66 | slog.Error("Error getting document from cache", "error", err) 67 | } 68 | if document != nil { 69 | if !document.IsStale() { 70 | slog.Info("Document is up to date, not updating") 71 | return nil 72 | } 73 | slog.Info("Document is stale, fetching versions", "last_updated", document.LastUpdated) 74 | since = &document.LastUpdated 75 | } 76 | 77 | fetchedVersions, err := fetchFromGithub(tracedCtx, e, config, since) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | // if we have a document, we should combine the fetched versions with the existing versions 83 | // this is so that we don't lose any versions that were added since the last time we fetched 84 | // but also so we don't add duplicates 85 | if since != nil && document != nil { 86 | fetchedVersions = append(document.Versions, fetchedVersions...) 87 | slog.Info("Combined versions", "versions", len(fetchedVersions)) 88 | 89 | // deduplicate the versions 90 | fetchedVersions = fetchedVersions.Deduplicate() 91 | slog.Info("Deduplicated versions", "versions", len(fetchedVersions)) 92 | } 93 | 94 | versions = fetchedVersions 95 | return nil 96 | }) 97 | 98 | if err != nil { 99 | slog.Error("Error fetching versions", "error", err) 100 | return "", err 101 | } 102 | 103 | err = storeVersions(ctx, e, versions, config) 104 | if err != nil { 105 | return "", err 106 | } 107 | 108 | return "", nil 109 | } 110 | } 111 | 112 | func storeVersions(ctx context.Context, e PopulateProviderVersionsEvent, versions types.VersionList, config *config.Config) error { 113 | if len(versions) == 0 { 114 | slog.Error("No versions found, skipping storage") 115 | return nil 116 | } 117 | 118 | key := fmt.Sprintf("%s/%s", e.Namespace, e.Type) 119 | 120 | err := config.ProviderVersionCache.Store(ctx, key, versions) 121 | if err != nil { 122 | return fmt.Errorf("failed to store provider listing: %w", err) 123 | } 124 | return nil 125 | } 126 | 127 | func fetchFromGithub(ctx context.Context, e PopulateProviderVersionsEvent, config *config.Config, since *time.Time) (types.VersionList, error) { 128 | // Construct the repo name. 129 | repoName := providers.GetRepoName(e.Type) 130 | 131 | // if we've been provided with a "since" we don't have to check if the repo exists 132 | // we can assume that it does because we've already fetched versions from it before 133 | 134 | if since == nil { 135 | // check the repo exists 136 | exists, err := github.RepositoryExists(ctx, config.ManagedGithubClient, e.Namespace, repoName) 137 | if err != nil { 138 | return nil, fmt.Errorf("failed to check if repo exists: %w", err) 139 | } 140 | if !exists { 141 | return nil, fmt.Errorf("repo %s/%s does not exist", e.Namespace, repoName) 142 | } 143 | } else { 144 | slog.Info("Skipping repo existence check because we already have a document in dynamodb") 145 | } 146 | 147 | slog.Info("Fetching versions") 148 | 149 | v, err := providers.GetVersions(ctx, config.RawGithubv4Client, e.Namespace, repoName, since) 150 | if err != nil { 151 | return nil, fmt.Errorf("failed to get versions: %w", err) 152 | } 153 | 154 | return v, nil 155 | } 156 | -------------------------------------------------------------------------------- /src/lambda/populate_provider_versions/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/aws/aws-lambda-go/lambda" 8 | "github.com/opentofu/registry/internal/config" 9 | ) 10 | 11 | func main() { 12 | configBuilder := config.NewBuilder() 13 | config, err := configBuilder.BuildConfig(context.Background(), "populate_provider_versions.buildconfig") 14 | if err != nil { 15 | panic(fmt.Errorf("could not build config: %w", err)) 16 | } 17 | 18 | lambda.Start(HandleRequest(config)) 19 | } 20 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | default = "eu-west-1" 3 | } 4 | 5 | variable "github_api_token" { 6 | type = string 7 | sensitive = true 8 | } 9 | 10 | variable "route53_zone_id" { 11 | type = string 12 | } 13 | 14 | variable "domain_name" { 15 | type = string 16 | } 17 | 18 | variable "provider_namespace_redirects" { 19 | type = map(any) 20 | default = { 21 | "hashicorp" : "opentofu" 22 | } 23 | } 24 | --------------------------------------------------------------------------------