├── .github ├── ISSUE_TEMPLATE │ ├── backend.yml │ ├── beta-feedback.yml │ ├── beta-inclusion.yml │ ├── config.yml │ └── frontend.yml ├── pull_request_template.md └── workflows │ ├── deploy-search-worker.yml │ ├── index-manual.yml │ ├── index.yml │ ├── remove-provider.yml │ └── verify.yml ├── .gitignore ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── backend ├── .gitignore ├── cmd │ ├── generate │ │ ├── main.go │ │ ├── rlimit_nonwindows.go │ │ └── rlimit_windows.go │ └── remove │ │ └── main.go ├── go.mod ├── go.sum └── internal │ ├── backend.go │ ├── blocklist │ └── blocklist.go │ ├── defaults │ └── defaults.go │ ├── e2e_test.go │ ├── factory │ └── factory.go │ ├── indexstorage │ ├── api.go │ ├── bufferedstorage │ │ ├── buffered.go │ │ ├── buffered_test.go │ │ └── index.go │ ├── filesystemstorage │ │ └── filesystem.go │ ├── s3storage │ │ ├── config.go │ │ ├── direct.go │ │ └── direct_test.go │ └── transactional.go │ ├── license │ ├── .gitignore │ ├── license_detector.go │ ├── license_detector_test.go │ ├── list.go │ └── vcslinkfetcher │ │ └── fetcher.go │ ├── moduleindex │ ├── err_blocked.md.tpl │ ├── err_incompatible_license.md │ ├── err_no_readme.md │ ├── force_options.go │ ├── generator.go │ ├── module.go │ ├── module_list.go │ ├── module_version.go │ ├── module_version_descriptor.go │ ├── moduleschema │ │ ├── .gitignore │ │ ├── errors.go │ │ ├── extractor.go │ │ ├── extractor_test.go │ │ ├── extractor_unix.go │ │ ├── extractor_windows.go │ │ ├── generate.go │ │ ├── schema.go │ │ ├── testdata │ │ │ ├── modulecall │ │ │ │ ├── module │ │ │ │ │ └── variables.tf │ │ │ │ ├── modulecall.tf │ │ │ │ └── versions.tf │ │ │ └── variables │ │ │ │ └── variables.tf │ │ ├── tofu.go │ │ └── tools │ │ │ └── build-tofu-binary │ │ │ ├── README.md │ │ │ └── main.go │ └── search.go │ ├── providerindex │ ├── force_options.go │ ├── generator.go │ ├── providerdocsource │ │ ├── api.go │ │ ├── err_blocked.md.tpl │ │ ├── err_file_too_large.md.tpl │ │ ├── err_incompatible_license.md │ │ ├── errors.go │ │ ├── scrape.go │ │ ├── scrape_test.go │ │ ├── type_doc_item.go │ │ ├── type_documentation.go │ │ └── type_provider_doc.go │ ├── providerindexstorage │ │ ├── api_provider.go │ │ ├── api_provider_cdktf_doc.go │ │ ├── api_provider_cdktf_doc_item.go │ │ ├── api_provider_doc.go │ │ ├── api_provider_doc_item.go │ │ ├── api_provider_list.go │ │ ├── api_provider_version.go │ │ ├── error_base.go │ │ ├── error_provider_list_not_found.go │ │ ├── error_provider_list_read_failed.go │ │ ├── error_provider_list_store_failed.go │ │ ├── error_provider_not_found.go │ │ ├── error_provider_read_failed.go │ │ ├── error_provider_store_failed.go │ │ ├── error_provider_version_not_found.go │ │ ├── error_provider_version_read_failed.go │ │ └── error_provider_version_store_failed.go │ ├── providertypes │ │ ├── cdktf_language.go │ │ ├── doc_item_kind.go │ │ ├── doc_item_name.go │ │ ├── provider.go │ │ ├── provider_addr.go │ │ ├── provider_list.go │ │ ├── provider_version.go │ │ └── provider_version_descriptor.go │ └── search.go │ ├── registrycloner │ ├── api.go │ └── git.go │ ├── search │ ├── api.go │ ├── ndjson.go │ ├── searchstorage │ │ ├── api.go │ │ └── indexstoragesearch │ │ │ └── new.go │ └── searchtypes │ │ ├── generated_index.go │ │ ├── index_id.go │ │ ├── index_item.go │ │ ├── index_type.go │ │ └── metaindex.go │ └── server │ ├── convert.sh │ ├── index.html │ ├── openapi.go │ ├── openapi.yml │ └── swagger.yml ├── blocklist.json ├── docker-compose.yml ├── documentation ├── documentation-sources.md ├── license-detection.md └── search-worker.md ├── frontend ├── .env.example ├── .gitignore ├── .gitkeep ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── NOTICE ├── announcement.md ├── docs │ ├── index.md │ ├── modules │ │ ├── adding.md │ │ ├── creating.md │ │ ├── index.md │ │ └── publishing.md │ ├── providers │ │ ├── adding.md │ │ ├── creating.md │ │ ├── docs.md │ │ ├── index.md │ │ └── publishing.md │ ├── sidebar.json │ └── users │ │ ├── index.md │ │ ├── modules.md │ │ └── providers.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public │ ├── apple-touch-icon-180x180.png │ ├── favicon.ico │ ├── favicon.svg │ ├── fonts │ │ ├── LICENSE │ │ ├── dmsans-bold-latin-ext.woff2 │ │ ├── dmsans-bold-latin.woff2 │ │ ├── dmsans-latin-ext.woff2 │ │ └── dmsans-latin.woff2 │ ├── maskable-icon-512x512.png │ ├── open-graph.png │ ├── pwa-192x192.png │ ├── pwa-512x512.png │ ├── pwa-64x64.png │ └── site.webmanifest ├── pwa-assets.config.ts ├── src │ ├── api.d.ts │ ├── components │ │ ├── AnnouncementBar │ │ │ ├── content.ts │ │ │ └── index.tsx │ │ ├── Breadcrumbs │ │ │ └── index.tsx │ │ ├── Button │ │ │ └── index.tsx │ │ ├── CardItem │ │ │ ├── Footer.tsx │ │ │ ├── Title.tsx │ │ │ └── index.tsx │ │ ├── Code │ │ │ ├── SyntaxHighlighter.tsx │ │ │ ├── index.tsx │ │ │ └── theme.ts │ │ ├── DateTime │ │ │ └── index.tsx │ │ ├── EditLink │ │ │ └── index.tsx │ │ ├── EmptyState │ │ │ └── index.tsx │ │ ├── Footer │ │ │ └── index.tsx │ │ ├── Header │ │ │ ├── Link.tsx │ │ │ ├── Logo.tsx │ │ │ ├── ThemeSwitcher.tsx │ │ │ └── index.tsx │ │ ├── HeadingLink │ │ │ └── index.tsx │ │ ├── Icon │ │ │ └── index.tsx │ │ ├── InfoSection │ │ │ └── index.tsx │ │ ├── LanguagePicker │ │ │ └── index.tsx │ │ ├── LicenseSidebarBlock │ │ │ └── index.tsx │ │ ├── Markdown │ │ │ ├── A.tsx │ │ │ ├── Code.tsx │ │ │ ├── H1.tsx │ │ │ ├── H2.tsx │ │ │ ├── H3.tsx │ │ │ ├── Hr.tsx │ │ │ ├── Img.tsx │ │ │ ├── Li.tsx │ │ │ ├── Ol.tsx │ │ │ ├── P.tsx │ │ │ ├── Pre.tsx │ │ │ ├── Table.tsx │ │ │ ├── Td.tsx │ │ │ ├── Th.tsx │ │ │ ├── Ul.tsx │ │ │ ├── index.tsx │ │ │ └── processor.ts │ │ ├── MetaTags │ │ │ └── index.tsx │ │ ├── ModuleDependencies │ │ │ └── index.tsx │ │ ├── ModuleInput │ │ │ └── index.tsx │ │ ├── ModuleInputs │ │ │ └── index.tsx │ │ ├── ModuleOutput │ │ │ └── index.tsx │ │ ├── ModuleOutputs │ │ │ └── index.tsx │ │ ├── ModuleResources │ │ │ └── index.tsx │ │ ├── OldVersionBanner │ │ │ └── index.tsx │ │ ├── PageSkeleton │ │ │ └── index.tsx │ │ ├── PageTitle │ │ │ └── index.tsx │ │ ├── Paragraph │ │ │ └── index.tsx │ │ ├── PatternBg │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── RepoSidebarBlock │ │ │ └── index.tsx │ │ ├── Search │ │ │ ├── ModuleResult.tsx │ │ │ ├── OtherResult.tsx │ │ │ ├── ProviderResult.tsx │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── SidebarBlock │ │ │ └── index.tsx │ │ ├── SidebarLayout │ │ │ └── index.tsx │ │ ├── SidebarPanel │ │ │ └── index.tsx │ │ ├── SimpleLayout │ │ │ └── index.tsx │ │ ├── TreeView │ │ │ └── index.tsx │ │ ├── VersionInfo │ │ │ └── index.tsx │ │ └── VersionsSidebarBlock │ │ │ ├── index.tsx │ │ │ └── utils.ts │ ├── crumbs.ts │ ├── hooks │ │ └── useDebouncedValue.ts │ ├── icons │ │ ├── arrow.ts │ │ ├── chevron.ts │ │ ├── contributors.ts │ │ ├── copy.ts │ │ ├── cross.tsx │ │ ├── document.ts │ │ ├── empty.ts │ │ ├── expand.ts │ │ ├── github.ts │ │ ├── home.ts │ │ ├── info.ts │ │ ├── issues.ts │ │ ├── lock.ts │ │ ├── moon.ts │ │ ├── prs.ts │ │ ├── search.ts │ │ ├── slack.ts │ │ ├── spinner.ts │ │ ├── sun.ts │ │ ├── tick.ts │ │ ├── warning.ts │ │ └── x.ts │ ├── index.css │ ├── main.tsx │ ├── prism.d.ts │ ├── q.ts │ ├── query.ts │ ├── remove-me.ts │ ├── router.tsx │ ├── routes │ │ ├── Docs │ │ │ ├── components │ │ │ │ └── SidebarMenu.tsx │ │ │ ├── index.tsx │ │ │ ├── loader.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── Error │ │ │ └── index.tsx │ │ ├── Home │ │ │ └── index.tsx │ │ ├── Module │ │ │ ├── Dependencies │ │ │ │ └── index.tsx │ │ │ ├── Inputs │ │ │ │ └── index.tsx │ │ │ ├── Outputs │ │ │ │ └── index.tsx │ │ │ ├── Readme │ │ │ │ ├── index.tsx │ │ │ │ └── loader.ts │ │ │ ├── Resources │ │ │ │ └── index.tsx │ │ │ ├── TabLink.tsx │ │ │ ├── components │ │ │ │ ├── ExamplesSidebarBlock.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── MetaTags.tsx │ │ │ │ ├── MetadataSidebarBlock.tsx │ │ │ │ ├── ProvisionInstructionsSidebarBlock.tsx │ │ │ │ ├── SchemaError.tsx │ │ │ │ ├── SideMenu.tsx │ │ │ │ ├── SubmodulesSidebarBlock.tsx │ │ │ │ ├── VersionInfo.tsx │ │ │ │ └── VersionsSidebarBlock.tsx │ │ │ ├── hooks │ │ │ │ └── useModuleParams.ts │ │ │ ├── index.tsx │ │ │ ├── loader.ts │ │ │ ├── middleware.ts │ │ │ ├── query.ts │ │ │ └── types.ts │ │ ├── ModuleExample │ │ │ ├── Inputs │ │ │ │ └── index.tsx │ │ │ ├── Outputs │ │ │ │ └── index.tsx │ │ │ ├── Readme │ │ │ │ ├── index.tsx │ │ │ │ └── loader.ts │ │ │ ├── components │ │ │ │ ├── Header.tsx │ │ │ │ ├── MetaTags.tsx │ │ │ │ ├── ProvisionInstructionsSidebarBlock.tsx │ │ │ │ ├── SideMenu.tsx │ │ │ │ └── TabLink.tsx │ │ │ ├── hooks │ │ │ │ └── useModuleExampleParams.ts │ │ │ ├── index.tsx │ │ │ ├── loader.ts │ │ │ ├── middleware.ts │ │ │ ├── query.ts │ │ │ └── types.ts │ │ ├── ModuleSubmodule │ │ │ ├── Dependencies │ │ │ │ └── index.tsx │ │ │ ├── Inputs │ │ │ │ └── index.tsx │ │ │ ├── Outputs │ │ │ │ └── index.tsx │ │ │ ├── Readme │ │ │ │ ├── index.tsx │ │ │ │ └── loader.ts │ │ │ ├── Resources │ │ │ │ └── index.tsx │ │ │ ├── components │ │ │ │ ├── Header.tsx │ │ │ │ ├── MetaTags.tsx │ │ │ │ ├── ProvisionInstructionsSidebarBlock.tsx │ │ │ │ ├── SideMenu.tsx │ │ │ │ └── TabLink.tsx │ │ │ ├── hooks │ │ │ │ └── useModuleSubmoduleParams.ts │ │ │ ├── index.tsx │ │ │ ├── loader.ts │ │ │ ├── middleware.ts │ │ │ ├── query.ts │ │ │ └── types.ts │ │ ├── Modules │ │ │ ├── components │ │ │ │ ├── CardItem.tsx │ │ │ │ └── List.tsx │ │ │ ├── index.tsx │ │ │ ├── loader.ts │ │ │ └── query.ts │ │ ├── Provider │ │ │ ├── Docs │ │ │ │ ├── index.tsx │ │ │ │ └── loader.ts │ │ │ ├── Overview │ │ │ │ ├── index.tsx │ │ │ │ └── loader.ts │ │ │ ├── components │ │ │ │ ├── DocsContent.tsx │ │ │ │ ├── DocsMenu.tsx │ │ │ │ ├── Error.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── InstructionSidebarBlock.tsx │ │ │ │ ├── MetaTags.tsx │ │ │ │ ├── MetadataSidebarBlock.tsx │ │ │ │ ├── VersionInfo.tsx │ │ │ │ ├── VersionsSidebarBlock.tsx │ │ │ │ ├── docsProcessor.test.ts │ │ │ │ └── docsProcessor.ts │ │ │ ├── docsSidebar.test.ts │ │ │ ├── docsSidebar.ts │ │ │ ├── hooks │ │ │ │ └── useProviderParams.ts │ │ │ ├── index.tsx │ │ │ ├── loader.ts │ │ │ ├── middleware.ts │ │ │ ├── query.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ ├── getProviderDoc.ts │ │ │ │ ├── isValidCDKTFLang.ts │ │ │ │ └── isValidDocsType.ts │ │ └── Providers │ │ │ ├── components │ │ │ ├── CardItem.tsx │ │ │ └── List.tsx │ │ │ ├── index.tsx │ │ │ ├── loader.ts │ │ │ └── query.ts │ ├── utils │ │ └── errors.tsx │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── licenses.json └── search ├── pg-indexer ├── go.mod ├── go.sum ├── main.go ├── schema.sql └── types.go └── worker ├── .gitignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── NOTICE ├── README.md ├── example.dev.vars ├── package-lock.json ├── package.json ├── scripts └── feed-data-r2.js ├── src ├── client.ts ├── index.ts ├── query.ts ├── types.ts └── validation.ts ├── test ├── index.spec.ts └── tsconfig.json ├── tsconfig.json ├── vitest.config.mts ├── worker-configuration.d.ts └── wrangler.toml /.github/ISSUE_TEMPLATE/backend.yml: -------------------------------------------------------------------------------- 1 | name: "Backend bug" 2 | description: "Report a problem with the indexed dataset" 3 | labels: ["bug","Backend"] 4 | body: 5 | - type: textarea 6 | id: text 7 | attributes: 8 | label: Please explain the bug 9 | value: 10 | validations: 11 | required: true 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/beta-feedback.yml: -------------------------------------------------------------------------------- 1 | name: "Beta: Feedback" 2 | description: "General feedback for the beta." 3 | labels: ["feedback"] 4 | body: 5 | - type: textarea 6 | id: text 7 | attributes: 8 | label: Feedback 9 | value: 10 | validations: 11 | required: true 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/beta-inclusion.yml: -------------------------------------------------------------------------------- 1 | name: "Beta: Add a provider/module" 2 | description: "The beta does not include the full registry dataset. Use this issue to request the inclusion of a provider." 3 | labels: ["feedback"] 4 | body: 5 | - type: textarea 6 | id: repository 7 | attributes: 8 | label: Repository URL 9 | placeholder: https://github.com/... 10 | value: 11 | validations: 12 | required: true 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/frontend.yml: -------------------------------------------------------------------------------- 1 | name: "Frontend bug" 2 | description: "Report a rendering error or bug in the frontend" 3 | labels: ["bug","frontend"] 4 | body: 5 | - type: textarea 6 | id: url 7 | attributes: 8 | label: URL of the page that's broken 9 | placeholder: https://search.opentofu.org/... 10 | value: 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshot 15 | attributes: 16 | label: Screenshot 17 | value: 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: browser 22 | attributes: 23 | label: OS, browser version, installed extensions 24 | value: 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: extra 29 | attributes: 30 | label: Additional information 31 | value: 32 | validations: 33 | required: false 34 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ## Checklist 3 | 4 | - [ ] I have read the [contribution guide](https://github.com/opentofu/opentofu/blob/main/CONTRIBUTING.md). 5 | - [ ] I have not used an AI coding assistant to create this PR. 6 | - [ ] My contribution is compatible with the MPL-2.0 license and I have provided a DCO sign-off on all my commits. 7 | - [ ] I have written all code in this PR myself OR I have marked all code I have not written myself (including modified code, e.g. copied from other places and then modified) with a comment indicating where it came from. 8 | -------------------------------------------------------------------------------- /.github/workflows/deploy-search-worker.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Search Worker 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'search/worker/**' 9 | 10 | jobs: 11 | deploy: 12 | name: Run deploy 13 | runs-on: ubuntu-latest 14 | environment: cloudflare-worker 15 | defaults: 16 | run: 17 | working-directory: search/worker 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Setup Node 22 | uses: actions/setup-node@v4 23 | with: 24 | # if we need to change node version, we need to change the Dockerfile on this folder as well 25 | node-version: '18.x' 26 | - name: Install dependencies 27 | run: npm ci 28 | - name: Deploy Wrangler 29 | env: 30 | CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 31 | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 32 | run: | 33 | npm run deploy 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /work 2 | /docs 3 | /registry 4 | .idea 5 | .DS_Store 6 | .tool-versions 7 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | * @opentofu/core-engineers 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # generate-registry runs `go generate` to build the registry files and saves them to /tmp/registry 2 | # since it's a high number of files, we are limiting to opentofu namespace. 3 | # If more providers are needed, tweak the arguments on this command 4 | generate-registry: 5 | @cd "$(CURDIR)/backend" && go generate ./... && go run ./cmd/generate/ --licenses-file ../licenses.json --destination-dir /tmp/registry --namespace opentofu --name ad 6 | 7 | # remove-provider removes a provider from the registry 8 | # Example: make remove-provider PROVIDER=hashicorp/aws 9 | # Example with flags: make remove-provider PROVIDER=hashicorp/aws REMOVE_FLAGS="--dry-run --version v1.0.0" 10 | remove-provider: 11 | @cd "$(CURDIR)/backend" && go run ./cmd/remove/ $(REMOVE_FLAGS) provider $(PROVIDER) 12 | 13 | # load-registry feed the data from /tmp/registry into the local R2 bucket (search/worker/.wrangler/state/r2) folder 14 | load-registry: 15 | @cd "$(CURDIR)/search/worker" && npm run feed-data 16 | 17 | # index-search downloads search data from api.opentofu.org and feeds that data into the postgres database used for searching 18 | index-search: 19 | @cd "$(CURDIR)/search/pg-indexer" && PG_CONNECTION_STRING=postgres://postgres:secret@localhost:5432/postgres?sslmode=disable go run . 20 | 21 | # after docker-compose us running, run this command to feed data into the application 22 | feed-data: 23 | make generate-registry 24 | make load-registry 25 | make index-search 26 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Go build artifacts 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Go test and benchmark results 9 | *.test 10 | *.out 11 | 12 | # Go coverage profile 13 | *.cover 14 | 15 | # Go dependency directories (generated by go mod) 16 | /vendor/ 17 | 18 | .idea 19 | 20 | # TODO: what is generating this? 21 | {{ -------------------------------------------------------------------------------- /backend/cmd/generate/rlimit_nonwindows.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "syscall" 8 | 9 | "github.com/opentofu/libregistry/logger" 10 | ) 11 | 12 | func setRLimit(ctx context.Context, log logger.Logger) error { 13 | log.Info(ctx, "Setting maximum number of file descriptors to 50000...") 14 | if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &syscall.Rlimit{ 15 | Cur: 50000, 16 | Max: 50000, 17 | }); err != nil { 18 | log.Warn(ctx, "Failed to set rlimit, generation may fail on larger repositories (%v)", err) 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /backend/cmd/generate/rlimit_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "github.com/opentofu/libregistry/logger" 8 | ) 9 | 10 | func setRLimit(_ context.Context, _ logger.Logger) error { 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /backend/internal/defaults/defaults.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | const RegistryRepo = "https://github.com/opentofu/registry.git" 4 | const RegistryRef = "main" 5 | const RegistryDir = "../registry" 6 | const WorkDir = "../work" 7 | const DestinationDir = "../docs" 8 | -------------------------------------------------------------------------------- /backend/internal/indexstorage/api.go: -------------------------------------------------------------------------------- 1 | package indexstorage 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | ) 8 | 9 | type Path string 10 | 11 | const MaxPathLength = 255 12 | 13 | var pathPartRe = regexp.MustCompile("^[a-zA-Z0-9_.@-]+(/[a-zA-Z0-9_.@-]+)*$") 14 | 15 | func (p Path) Validate() error { 16 | if len(p) > MaxPathLength { 17 | return fmt.Errorf("path too long: %s", p) 18 | } 19 | if !pathPartRe.MatchString(string(p)) { 20 | return fmt.Errorf("invalid path: %s", p) 21 | } 22 | return nil 23 | } 24 | 25 | type API interface { 26 | // ReadFile reads the given path if found, otherwise returns a not found error. 27 | ReadFile(ctx context.Context, path Path) ([]byte, error) 28 | 29 | // WriteFile writes a given file at a path, creating all directories in the path. 30 | WriteFile(ctx context.Context, path Path, contents []byte) error 31 | 32 | // RemoveAll removes all files under the given path. 33 | RemoveAll(ctx context.Context, path Path) error 34 | 35 | // Subdirectory returns a storage API that is restricted to a subdirectory. 36 | Subdirectory(ctx context.Context, dir Path) (API, error) 37 | } 38 | -------------------------------------------------------------------------------- /backend/internal/indexstorage/transactional.go: -------------------------------------------------------------------------------- 1 | package indexstorage 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Committable interface { 8 | // Rollback attempts to remove all local changes and revert to the state where the backing storage is the 9 | // authoritative source of information. 10 | Rollback(ctx context.Context) error 11 | // Commit attempts to write all changes to the backing storage. If it fails, it attempts to make the remaining 12 | // state on the backing storage consistent, but it may not be able to guarantee it. It will leave the local 13 | // directory in such a state that it can continue the upload by calling Commit again. 14 | Commit(ctx context.Context) error 15 | 16 | // Recover attempts to recover a previously-aborted commit if any. If no commit was started, it rolls 17 | // back any changes. 18 | Recover(ctx context.Context) error 19 | } 20 | 21 | type TransactionalAPI interface { 22 | API 23 | Committable 24 | } 25 | -------------------------------------------------------------------------------- /backend/internal/license/.gitignore: -------------------------------------------------------------------------------- 1 | *~ -------------------------------------------------------------------------------- /backend/internal/license/vcslinkfetcher/fetcher.go: -------------------------------------------------------------------------------- 1 | package vcslinkfetcher 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/opentofu/libregistry/vcs" 8 | "github.com/opentofu/registry-ui/internal/license" 9 | ) 10 | 11 | // Fetcher creates a license fetcher from a VCS system. 12 | func Fetcher(ctx context.Context, repository vcs.RepositoryAddr, version vcs.VersionNumber, vcsClient vcs.Client) license.LinkFetcher { 13 | return func(license *license.License) error { 14 | link, err := vcsClient.GetFileViewURL(ctx, repository, version, license.File) 15 | if err != nil { 16 | var noWebAccessError *vcs.NoWebAccessError 17 | if errors.As(err, &noWebAccessError) { 18 | return nil 19 | } 20 | return err 21 | } 22 | license.Link = link 23 | return nil 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/err_blocked.md.tpl: -------------------------------------------------------------------------------- 1 | # 451 Unavailable for Legal Reasons 2 | 3 | The contents of this module are blocked due to a request by the module author or third party. 4 | 5 | > {{.}} 6 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/err_incompatible_license.md: -------------------------------------------------------------------------------- 1 | # 451 Unavailable for Legal Reasons 2 | 3 | We cannot show you the documentation for this module because it does not have a license or has one or more licenses that are compatible with this project. 4 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/err_no_readme.md: -------------------------------------------------------------------------------- 1 | # 404 Not Found 2 | 3 | This module does not have a README file. 4 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/force_options.go: -------------------------------------------------------------------------------- 1 | package moduleindex 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/opentofu/libregistry/types/module" 7 | ) 8 | 9 | // ForceRegenerate describes an interface to force regenerating a module even if it already exists in the index. 10 | type ForceRegenerate interface { 11 | // MustRegenerateModule returns true if a module addr should be regenerated regardless of freshness state. 12 | // This function purposefully doesn't implement a by-version selection because it will interfere with the 13 | // generation of the search index. 14 | MustRegenerateModule(ctx context.Context, addr module.Addr) bool 15 | } 16 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/module_version_descriptor.go: -------------------------------------------------------------------------------- 1 | package moduleindex 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/opentofu/libregistry/types/module" 7 | ) 8 | 9 | // ModuleVersionDescriptor describes a single version. 10 | type ModuleVersionDescriptor struct { 11 | ID module.VersionNumber `json:"id"` 12 | Published time.Time `json:"published"` 13 | } 14 | 15 | func (d ModuleVersionDescriptor) Validate() error { 16 | if err := d.ID.Validate(); err != nil { 17 | return err 18 | } 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/.gitignore: -------------------------------------------------------------------------------- 1 | # Directory to build tofu test directory. 2 | testtofu -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/errors.go: -------------------------------------------------------------------------------- 1 | package moduleschema 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | var stripColorRe = regexp.MustCompile("\x1b\\[(.*?)m") 8 | 9 | type SchemaExtractionFailedError struct { 10 | Output []byte 11 | Cause error 12 | } 13 | 14 | func (s SchemaExtractionFailedError) Error() string { 15 | if len(s.Output) > 0 { 16 | return "Schema extraction failed: " + s.Cause.Error() + " (tofu output: " + s.OutputString() + ")" 17 | } 18 | return "Schema extraction failed: " + s.Cause.Error() 19 | } 20 | 21 | func (s SchemaExtractionFailedError) Unwrap() error { 22 | return s.Cause 23 | } 24 | 25 | func (s SchemaExtractionFailedError) OutputString() string { 26 | outputString := string(s.Output) 27 | outputString = stripColorRe.ReplaceAllString(outputString, "") 28 | return outputString 29 | } 30 | 31 | func (s SchemaExtractionFailedError) RawOutput() []byte { 32 | return s.Output 33 | } 34 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/extractor.go: -------------------------------------------------------------------------------- 1 | package moduleschema 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Extractor is a utility to extract module schemas. 8 | type Extractor interface { 9 | // Extract extracts the module schema of a module present in the given directory. 10 | Extract(ctx context.Context, moduleDirectory string) (Schema, error) 11 | } 12 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/extractor_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package moduleschema 4 | 5 | const tofuName = "tofu" 6 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/extractor_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package moduleschema 4 | 5 | const tofuName = "tofu.exe" 6 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/generate.go: -------------------------------------------------------------------------------- 1 | package moduleschema 2 | 3 | // This command generates a usable tofu binary and tofudl cache directory. This is a temporary measure until the 4 | // tofu metadata dump command makes it into a released version. 5 | 6 | //go:generate go run github.com/opentofu/registry-ui/internal/moduleindex/moduleschema/tools/build-tofu-binary 7 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/testdata/modulecall/module/variables.tf: -------------------------------------------------------------------------------- 1 | variable "test" {} -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/testdata/modulecall/modulecall.tf: -------------------------------------------------------------------------------- 1 | module "foo" { 2 | source = "./module" 3 | 4 | test = "Hello world!" 5 | } -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/testdata/modulecall/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | opentofu = { 4 | source = "opentofu/opentofu" 5 | version = "1.6.0" 6 | } 7 | 8 | ad = { 9 | source = "opentofu/ad" 10 | version = "0.5.0" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/testdata/variables/variables.tf: -------------------------------------------------------------------------------- 1 | variable "untyped" { 2 | 3 | } 4 | 5 | variable "str" { 6 | type = string 7 | } 8 | 9 | variable "int" { 10 | type = number 11 | } 12 | 13 | variable "def" { 14 | default = 42 15 | } 16 | 17 | variable "desc" { 18 | description = "This is a test" 19 | } -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/tools/build-tofu-binary/README.md: -------------------------------------------------------------------------------- 1 | # Tofu build tool 2 | 3 | This tool builds an OpenTofu binary from a hard-coded branch that contains the `metadata dump` subcommand. It also creates a cache suitable for tofudl. -------------------------------------------------------------------------------- /backend/internal/moduleindex/moduleschema/tools/build-tofu-binary/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/exec" 7 | "path" 8 | "path/filepath" 9 | "runtime" 10 | ) 11 | 12 | func main() { 13 | wd, err := os.Getwd() 14 | if err != nil { 15 | panic(err) 16 | } 17 | testTofuDir := path.Join(wd, "testtofu") 18 | repoDir := path.Join(testTofuDir, "repo") 19 | binaryName := "tofu" 20 | if runtime.GOOS == "windows" { 21 | binaryName += ".exe" 22 | } 23 | binaryPath := path.Join(testTofuDir, binaryName) 24 | 25 | if err := os.MkdirAll(testTofuDir, 0755); err != nil { 26 | panic(err) 27 | } 28 | 29 | buildTofu(repoDir, testTofuDir, binaryPath) 30 | } 31 | 32 | func buildTofu(repoDir string, testTofuDir string, binaryPath string) { 33 | if _, err := os.Stat(binaryPath); err == nil { 34 | return 35 | } 36 | if _, err := os.Stat(repoDir); err != nil { 37 | runCommand(testTofuDir, "git", "clone", "https://github.com/opentofu/opentofu.git", repoDir) 38 | } 39 | 40 | runCommand(repoDir, "git", "pull") 41 | runCommand(repoDir, "git", "checkout", "experiment/json_config_dump") 42 | runCommand(repoDir, "go", "build", "-o", filepath.ToSlash(binaryPath), "github.com/opentofu/opentofu/cmd/tofu") 43 | } 44 | 45 | func runCommand(wd string, argv ...string) { 46 | cmd := exec.Command(argv[0], argv[1:]...) 47 | cmd.Stdout = os.Stdout 48 | cmd.Stderr = os.Stderr 49 | cmd.Dir = wd 50 | if err := cmd.Run(); err != nil { 51 | var exitErr *exec.ExitError 52 | if errors.As(err, &exitErr) { 53 | if exitErr.ExitCode() != 0 { 54 | panic(err) 55 | } 56 | } else { 57 | panic(err) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/internal/providerindex/force_options.go: -------------------------------------------------------------------------------- 1 | package providerindex 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/opentofu/libregistry/types/provider" 7 | ) 8 | 9 | // ForceRegenerate describes an interface to force regenerating a module even if it already exists in the index. 10 | type ForceRegenerate interface { 11 | // MustRegenerateProvider returns true if a provider addr should be regenerated regardless of freshness state. 12 | // This function purposefully doesn't implement a by-version selection because it will interfere with the 13 | // generation of the search index. 14 | MustRegenerateProvider(ctx context.Context, addr provider.Addr) bool 15 | } 16 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerdocsource/err_blocked.md.tpl: -------------------------------------------------------------------------------- 1 | # 451 Unavailable for Legal Reasons 2 | 3 | The contents of this provider are blocked due to a request by the module author or third party. 4 | 5 | > {{.}} 6 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerdocsource/err_file_too_large.md.tpl: -------------------------------------------------------------------------------- 1 | # 502 File too large 2 | 3 | We are sorry, the documentation file you requested was too large and is not included here. You can view this file [directly in the source repository]({{.}}). 4 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerdocsource/err_incompatible_license.md: -------------------------------------------------------------------------------- 1 | # 451 Unavailable for Legal Reasons 2 | 3 | We cannot show you the documentation for this provider because it does not have a license or has one or more licenses that are not approved for this project. 4 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerdocsource/errors.go: -------------------------------------------------------------------------------- 1 | package providerdocsource 2 | 3 | import ( 4 | _ "embed" 5 | "text/template" 6 | ) 7 | 8 | //go:embed err_blocked.md.tpl 9 | var errorMessageBlocked []byte 10 | 11 | var errorMessageBlockedTemplate = template.Must(template.New("").Parse(string(errorMessageBlocked))) 12 | 13 | //go:embed err_incompatible_license.md 14 | var errorIncompatibleLicense []byte 15 | 16 | //go:embed err_file_too_large.md.tpl 17 | var errorTooLarge []byte 18 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/api_provider_cdktf_doc.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | import ( 4 | "context" 5 | "path" 6 | 7 | "github.com/opentofu/libregistry/types/provider" 8 | "github.com/opentofu/registry-ui/internal/indexstorage" 9 | "github.com/opentofu/registry-ui/internal/providerindex/providertypes" 10 | ) 11 | 12 | func (s storage) getProviderCDKTFDocPath(_ context.Context, providerAddr provider.Addr, version provider.VersionNumber, language providertypes.CDKTFLanguage) indexstorage.Path { 13 | providerAddr = providerAddr.Normalize() 14 | version = version.Normalize() 15 | return indexstorage.Path(path.Join(providerAddr.Namespace, providerAddr.Name, string(version), cdktfDirName, string(language), "index.md")) 16 | } 17 | 18 | func (s storage) GetProviderCDKTFDoc(ctx context.Context, providerAddr provider.Addr, version provider.VersionNumber, language providertypes.CDKTFLanguage) ([]byte, error) { 19 | // TODO validate provider addr 20 | if err := version.Validate(); err != nil { 21 | return nil, err 22 | } 23 | if err := language.Validate(); err != nil { 24 | return nil, err 25 | } 26 | // TODO add typed errors 27 | return s.indexStorageAPI.ReadFile(ctx, s.getProviderCDKTFDocPath(ctx, providerAddr, version, language)) 28 | } 29 | 30 | func (s storage) StoreProviderCDKTFDoc(ctx context.Context, providerAddr provider.Addr, version provider.VersionNumber, language providertypes.CDKTFLanguage, data []byte) error { 31 | // TODO validate provider addr 32 | if err := version.Validate(); err != nil { 33 | return err 34 | } 35 | if err := language.Validate(); err != nil { 36 | return err 37 | } 38 | // TODO add typed errors 39 | return s.indexStorageAPI.WriteFile(ctx, s.getProviderCDKTFDocPath(ctx, providerAddr, version, language), data) 40 | } 41 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/api_provider_doc.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | import ( 4 | "context" 5 | "path" 6 | 7 | "github.com/opentofu/libregistry/types/provider" 8 | "github.com/opentofu/registry-ui/internal/indexstorage" 9 | ) 10 | 11 | func (s storage) getProviderDocPath(_ context.Context, providerAddr provider.Addr, version provider.VersionNumber) indexstorage.Path { 12 | providerAddr = providerAddr.Normalize() 13 | version = version.Normalize() 14 | return indexstorage.Path(path.Join(providerAddr.Namespace, providerAddr.Name, string(version), "index.md")) 15 | } 16 | 17 | func (s storage) GetProviderDoc(ctx context.Context, providerAddr provider.Addr, version provider.VersionNumber) ([]byte, error) { 18 | // TODO validate provider addr 19 | if err := version.Validate(); err != nil { 20 | return nil, err 21 | } 22 | // TODO add typed errors 23 | return s.indexStorageAPI.ReadFile(ctx, s.getProviderDocPath(ctx, providerAddr, version)) 24 | } 25 | 26 | func (s storage) StoreProviderDoc(ctx context.Context, providerAddr provider.Addr, version provider.VersionNumber, data []byte) error { 27 | // TODO validate provider addr 28 | if err := version.Validate(); err != nil { 29 | return err 30 | } 31 | // TODO add typed errors 32 | return s.indexStorageAPI.WriteFile(ctx, s.getProviderDocPath(ctx, providerAddr, version), data) 33 | } 34 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/api_provider_list.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "os" 7 | 8 | "github.com/opentofu/registry-ui/internal/indexstorage" 9 | "github.com/opentofu/registry-ui/internal/providerindex/providertypes" 10 | ) 11 | 12 | func (s storage) getProviderListFile() indexstorage.Path { 13 | return "index.json" 14 | } 15 | 16 | func (s storage) GetProviderList(ctx context.Context) (providertypes.ProviderList, error) { 17 | index := providertypes.ProviderList{ 18 | Providers: []*providertypes.Provider{}, 19 | } 20 | indexContents, err := s.indexStorageAPI.ReadFile(ctx, s.getProviderListFile()) 21 | if err != nil { 22 | if os.IsNotExist(err) { 23 | return index, &ProviderListNotFoundError{BaseError: BaseError{Cause: err}} 24 | } 25 | return index, &ProviderListReadFailedError{BaseError: BaseError{Cause: err}} 26 | } 27 | 28 | if err := json.Unmarshal(indexContents, &index); err != nil { 29 | return index, &ProviderListReadFailedError{BaseError: BaseError{Cause: err}} 30 | } 31 | return index, nil 32 | } 33 | 34 | func (s storage) StoreProviderList(ctx context.Context, providerList providertypes.ProviderList) error { 35 | marshalled, err := json.Marshal(providerList) 36 | if err != nil { 37 | return &ProviderListStoreFailedError{BaseError: BaseError{Cause: err}} 38 | } 39 | if err := s.indexStorageAPI.WriteFile(ctx, s.getProviderListFile(), marshalled); err != nil { 40 | return &ProviderListStoreFailedError{BaseError: BaseError{Cause: err}} 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/error_base.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type BaseError struct { 8 | Cause error 9 | } 10 | 11 | func (b BaseError) Unwrap() error { 12 | return b.Cause 13 | } 14 | 15 | func (b BaseError) Error() string { 16 | if b.Cause != nil { 17 | return b.Cause.Error() 18 | } 19 | return fmt.Sprintf("Unspecified %T", b) 20 | } 21 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/error_provider_list_not_found.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | type ProviderListNotFoundError struct { 4 | BaseError 5 | } 6 | 7 | func (p *ProviderListNotFoundError) Error() string { 8 | if p.Cause != nil { 9 | return "Provider list not found in storage (" + p.Cause.Error() + ")." 10 | } 11 | return "Provider list not found in storage." 12 | } 13 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/error_provider_list_read_failed.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | type ProviderListReadFailedError struct { 4 | BaseError 5 | } 6 | 7 | func (p *ProviderListReadFailedError) Error() string { 8 | if p.Cause != nil { 9 | return "Provider list could not be read: " + p.Cause.Error() 10 | } 11 | return "Provider list could not be read." 12 | } 13 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/error_provider_list_store_failed.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | type ProviderListStoreFailedError struct { 4 | BaseError 5 | } 6 | 7 | func (p *ProviderListStoreFailedError) Error() string { 8 | if p.Cause != nil { 9 | return "Provider list could not be stored: " + p.Cause.Error() 10 | } 11 | return "Provider list could not be stored." 12 | } 13 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/error_provider_not_found.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | import ( 4 | "github.com/opentofu/libregistry/types/provider" 5 | ) 6 | 7 | type ProviderNotFoundError struct { 8 | BaseError 9 | 10 | ProviderAddr provider.Addr 11 | } 12 | 13 | func (p *ProviderNotFoundError) Error() string { 14 | if p.Cause != nil { 15 | return "Provider " + p.ProviderAddr.String() + " not found (" + p.Cause.Error() + ")." 16 | } 17 | return "Provider " + p.ProviderAddr.String() + " not found." 18 | } 19 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/error_provider_read_failed.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | import ( 4 | "github.com/opentofu/libregistry/types/provider" 5 | ) 6 | 7 | type ProviderReadFailedError struct { 8 | BaseError 9 | ProviderAddr provider.Addr 10 | } 11 | 12 | func (p *ProviderReadFailedError) Error() string { 13 | if p.Cause != nil { 14 | return "Provider " + p.ProviderAddr.String() + " could not be read: " + p.Cause.Error() 15 | } 16 | return "Provider " + p.ProviderAddr.String() + " could not be read." 17 | } 18 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/error_provider_store_failed.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | import ( 4 | "github.com/opentofu/libregistry/types/provider" 5 | ) 6 | 7 | type ProviderStoreFailedError struct { 8 | BaseError 9 | ProviderAddr provider.Addr 10 | } 11 | 12 | func (p *ProviderStoreFailedError) Error() string { 13 | if p.Cause != nil { 14 | return "Provider " + p.ProviderAddr.String() + " could not be stored: " + p.Cause.Error() 15 | } 16 | return "Provider " + p.ProviderAddr.String() + " could not be stored." 17 | } 18 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/error_provider_version_not_found.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | import ( 4 | "github.com/opentofu/libregistry/types/provider" 5 | ) 6 | 7 | type ProviderVersionNotFoundError struct { 8 | BaseError 9 | 10 | ProviderAddr provider.Addr 11 | Version provider.VersionNumber 12 | } 13 | 14 | func (p *ProviderVersionNotFoundError) Error() string { 15 | if p.Cause != nil { 16 | return "Provider " + p.ProviderAddr.String() + " version " + string(p.Version) + " not found (" + p.Cause.Error() + ")." 17 | } 18 | return "Provider " + p.ProviderAddr.String() + " version " + string(p.Version) + " not found." 19 | } 20 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/error_provider_version_read_failed.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | import ( 4 | "github.com/opentofu/libregistry/types/provider" 5 | ) 6 | 7 | type ProviderVersionReadFailedError struct { 8 | BaseError 9 | ProviderAddr provider.Addr 10 | Version provider.VersionNumber 11 | } 12 | 13 | func (p *ProviderVersionReadFailedError) Error() string { 14 | if p.Cause != nil { 15 | return "Provider " + p.ProviderAddr.String() + " version " + string(p.Version) + " could not be read: " + p.Cause.Error() 16 | } 17 | return "Provider " + p.ProviderAddr.String() + " version " + string(p.Version) + " could not be read." 18 | } 19 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providerindexstorage/error_provider_version_store_failed.go: -------------------------------------------------------------------------------- 1 | package providerindexstorage 2 | 3 | import ( 4 | "github.com/opentofu/libregistry/types/provider" 5 | ) 6 | 7 | type ProviderVersionStoreFailedError struct { 8 | BaseError 9 | ProviderAddr provider.Addr 10 | Version provider.VersionNumber 11 | } 12 | 13 | func (p *ProviderVersionStoreFailedError) Error() string { 14 | if p.Cause != nil { 15 | return "Provider " + p.ProviderAddr.String() + " version " + string(p.Version) + " could not be stored: " + p.Cause.Error() 16 | } 17 | return "Provider " + p.ProviderAddr.String() + " version " + string(p.Version) + " could not be stored." 18 | } 19 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providertypes/cdktf_language.go: -------------------------------------------------------------------------------- 1 | package providertypes 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type CDKTFLanguage string 8 | 9 | const ( 10 | CDKTFLanguagePython CDKTFLanguage = "python" 11 | CDKTFLanguageTypescript CDKTFLanguage = "typescript" 12 | CDKTFLanguageCSharp CDKTFLanguage = "csharp" 13 | CDKTFLanguageJava CDKTFLanguage = "java" 14 | CDKTFLanguageGo CDKTFLanguage = "go" 15 | ) 16 | 17 | func (l CDKTFLanguage) Validate() error { 18 | switch l { 19 | case CDKTFLanguagePython: 20 | case CDKTFLanguageTypescript: 21 | case CDKTFLanguageCSharp: 22 | case CDKTFLanguageJava: 23 | case CDKTFLanguageGo: 24 | default: 25 | return fmt.Errorf("invalid CDKTF language: %s", l) 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providertypes/doc_item_kind.go: -------------------------------------------------------------------------------- 1 | package providertypes 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type DocItemKind string 8 | 9 | const ( 10 | DocItemKindRoot DocItemKind = "" 11 | DocItemKindResource DocItemKind = "resource" 12 | DocItemKindDataSource DocItemKind = "datasource" 13 | DocItemKindFunction DocItemKind = "function" 14 | DocItemKindGuide DocItemKind = "guide" 15 | ) 16 | 17 | func (k DocItemKind) Validate() error { 18 | switch k { 19 | case DocItemKindRoot: 20 | case DocItemKindResource: 21 | case DocItemKindDataSource: 22 | case DocItemKindFunction: 23 | case DocItemKindGuide: 24 | default: 25 | return fmt.Errorf("invalid doc item kind: %s", k) 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providertypes/doc_item_name.go: -------------------------------------------------------------------------------- 1 | package providertypes 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | type DocItemName string 10 | 11 | const docItemNameMaxLength = 255 12 | 13 | var docItemNameRe = regexp.MustCompile("^[a-zA-Z0-9 ._@-]+$") 14 | 15 | func (n DocItemName) Validate() error { 16 | if len(n) > docItemNameMaxLength || !docItemNameRe.MatchString(string(n)) { 17 | return fmt.Errorf("invalid doc item name: %s", n) 18 | } 19 | return nil 20 | } 21 | 22 | func (n DocItemName) Normalize() DocItemName { 23 | return DocItemName(strings.ToLower(string(n))) 24 | } 25 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providertypes/provider_addr.go: -------------------------------------------------------------------------------- 1 | package providertypes 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/opentofu/libregistry/types/provider" 7 | ) 8 | 9 | func Addr(addr provider.Addr) ProviderAddr { 10 | return ProviderAddr{ 11 | Addr: addr, 12 | Display: addr.String(), 13 | Namespace: addr.Namespace, 14 | Name: addr.Name, 15 | } 16 | } 17 | 18 | // ProviderAddr is an enriched model of provider.Addr with display properties for the frontend. 19 | type ProviderAddr struct { 20 | provider.Addr 21 | 22 | // Display contains the user-readable display variant of this addr. This may be capitalized. 23 | Display string `json:"display"` 24 | // Namespace contains the lower-case namespace part of the addr. 25 | Namespace string `json:"namespace"` 26 | // Name contains the lower-case name part of the addr. 27 | Name string `json:"name"` 28 | } 29 | 30 | type marshalledProviderAddr struct { 31 | Display string `json:"display"` 32 | Namespace string `json:"namespace"` 33 | Name string `json:"name"` 34 | } 35 | 36 | func (p *ProviderAddr) UnmarshalJSON(data []byte) error { 37 | marshalled := marshalledProviderAddr{} 38 | if err := json.Unmarshal(data, &marshalled); err != nil { 39 | return err 40 | } 41 | 42 | *p = ProviderAddr{ 43 | Addr: provider.Addr{Namespace: marshalled.Namespace, Name: marshalled.Name}, 44 | Display: marshalled.Display, 45 | Namespace: marshalled.Namespace, 46 | Name: marshalled.Name, 47 | } 48 | return nil 49 | } 50 | 51 | func (p *ProviderAddr) MarshalJSON() ([]byte, error) { 52 | return json.Marshal(marshalledProviderAddr{ 53 | Display: p.Display, 54 | Namespace: p.Namespace, 55 | Name: p.Name, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providertypes/provider_version.go: -------------------------------------------------------------------------------- 1 | package providertypes 2 | 3 | import ( 4 | "github.com/opentofu/registry-ui/internal/license" 5 | ) 6 | 7 | // ProviderVersion describes a single provider version. 8 | type ProviderVersion struct { 9 | ProviderVersionDescriptor 10 | 11 | Docs ProviderDocs `json:"docs"` 12 | 13 | CDKTFDocs map[CDKTFLanguage]ProviderDocs `json:"cdktf_docs"` 14 | 15 | Licenses license.List `json:"license"` 16 | 17 | // IncompatibleLicense indicates that there are no licenses or there is one or more license that are not approved. 18 | IncompatibleLicense bool `json:"incompatible_license"` 19 | 20 | Link string `json:"link"` 21 | } 22 | 23 | // ProviderDocs describes either a provider or a CDKTF language. 24 | type ProviderDocs struct { 25 | Root *ProviderDocItem `json:"index,omitempty"` 26 | Resources []ProviderDocItem `json:"resources"` 27 | DataSources []ProviderDocItem `json:"datasources"` 28 | Functions []ProviderDocItem `json:"functions"` 29 | Guides []ProviderDocItem `json:"guides"` 30 | } 31 | 32 | // ProviderDocItem describes a single documentation item. 33 | type ProviderDocItem struct { 34 | Name DocItemName `json:"name"` 35 | EditLink string `json:"edit_link"` 36 | 37 | Title string `json:"title"` 38 | Subcategory string `json:"subcategory"` 39 | Description string `json:"description"` 40 | } 41 | -------------------------------------------------------------------------------- /backend/internal/providerindex/providertypes/provider_version_descriptor.go: -------------------------------------------------------------------------------- 1 | package providertypes 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/opentofu/libregistry/types/provider" 7 | ) 8 | 9 | // ProviderVersionDescriptor describes a provider version. 10 | type ProviderVersionDescriptor struct { 11 | ID provider.VersionNumber `json:"id"` 12 | Published time.Time `json:"published"` 13 | } 14 | -------------------------------------------------------------------------------- /backend/internal/search/api.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/opentofu/registry-ui/internal/search/searchtypes" 7 | ) 8 | 9 | type API interface { 10 | // AddItem queues the addition of an index item. 11 | AddItem(ctx context.Context, item searchtypes.IndexItem) error 12 | 13 | // RemoveVersionItems removes all items of a specific type matching a version. 14 | RemoveVersionItems(ctx context.Context, itemType searchtypes.IndexType, addr string, version string) error 15 | 16 | // RemoveItem removes an item with the specific ID and all items referencing this item as a parent. 17 | RemoveItem(ctx context.Context, id searchtypes.IndexID) error 18 | 19 | // GenerateIndex generates a search index with the items currently in the searchtypes.MetaIndex. This function 20 | // returns an opaque blob that should be passed to the frontend for use as a search index. 21 | GenerateIndex(ctx context.Context) error 22 | } 23 | -------------------------------------------------------------------------------- /backend/internal/search/searchstorage/api.go: -------------------------------------------------------------------------------- 1 | package searchstorage 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/opentofu/registry-ui/internal/search/searchtypes" 7 | ) 8 | 9 | type API interface { 10 | // GetMetaIndex returns the meta index from storage. If the meta index does not exist, it returns a preconfigured 11 | // empty searchtypes.MetaIndex and a *MetaIndexNotFoundError. 12 | GetMetaIndex(ctx context.Context) (searchtypes.MetaIndex, error) 13 | StoreMetaIndex(ctx context.Context, metaIndex searchtypes.MetaIndex) error 14 | StoreGeneratedIndex(ctx context.Context, data []byte) error 15 | } 16 | 17 | type MetaIndexNotFoundError struct { 18 | Cause error 19 | } 20 | 21 | func (m MetaIndexNotFoundError) Error() string { 22 | if m.Cause != nil { 23 | return "Meta index not found (" + m.Cause.Error() + ")" 24 | } 25 | return "Meta index not found" 26 | } 27 | 28 | func (m MetaIndexNotFoundError) Unwrap() error { 29 | return m.Cause 30 | } 31 | -------------------------------------------------------------------------------- /backend/internal/search/searchtypes/generated_index.go: -------------------------------------------------------------------------------- 1 | package searchtypes 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type GeneratedIndexItemType string 8 | 9 | const ( 10 | GeneratedIndexItemHeader GeneratedIndexItemType = "header" 11 | GeneratedIndexItemAdd GeneratedIndexItemType = "add" 12 | GeneratedIndexItemDelete GeneratedIndexItemType = "delete" 13 | ) 14 | 15 | type GeneratedIndexHeader struct { 16 | LastUpdated time.Time `json:"last_updated"` 17 | } 18 | 19 | type ItemDeletion struct { 20 | ID IndexID `json:"id"` 21 | DeletedAt time.Time `json:"deleted_at"` 22 | } 23 | 24 | type GeneratedIndexItem struct { 25 | Type GeneratedIndexItemType `json:"type"` 26 | Header *GeneratedIndexHeader `json:"header,omitempty"` 27 | Addition *IndexItem `json:"addition,omitempty"` 28 | Deletion *ItemDeletion `json:"deletion,omitempty"` 29 | } 30 | -------------------------------------------------------------------------------- /backend/internal/search/searchtypes/index_id.go: -------------------------------------------------------------------------------- 1 | package searchtypes 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type IndexID string 8 | 9 | func (i IndexID) Validate() error { 10 | if i == "" { 11 | return fmt.Errorf("the ID must not be empty") 12 | } 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /backend/internal/search/searchtypes/index_type.go: -------------------------------------------------------------------------------- 1 | package searchtypes 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type IndexType string 8 | 9 | const ( 10 | IndexTypeProvider IndexType = "provider" 11 | IndexTypeProviderResource IndexType = "provider/resource" 12 | IndexTypeProviderDatasource IndexType = "provider/datasource" 13 | IndexTypeProviderFunction IndexType = "provider/function" 14 | IndexTypeModule IndexType = "module" 15 | IndexTypeModuleSubmodule IndexType = "module/submodule" 16 | ) 17 | 18 | func (i IndexType) Validate() error { 19 | switch i { 20 | case IndexTypeProvider: 21 | case IndexTypeProviderResource: 22 | case IndexTypeProviderDatasource: 23 | case IndexTypeProviderFunction: 24 | case IndexTypeModule: 25 | case IndexTypeModuleSubmodule: 26 | default: 27 | return fmt.Errorf("invalid index type: %s", i) 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /backend/internal/server/convert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Convert OpenAPI 3.0 spec to Swagger 2.0 format 4 | # This script converts the manually maintained OpenAPI 3.0 specification 5 | # to Swagger 2.0 for backward compatibility. 6 | # This should be run every time the OpenAPI spec is updated. 7 | # It requires the `api-spec-converter` package to be installed globally. 8 | 9 | set -e 10 | 11 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 12 | OPENAPI_FILE="$SCRIPT_DIR/openapi.yml" 13 | SWAGGER_FILE="$SCRIPT_DIR/swagger.yml" 14 | 15 | echo "Converting OpenAPI 3.0 to Swagger 2.0..." 16 | if [ ! -f "$OPENAPI_FILE" ]; then 17 | echo "Error: OpenAPI 3.0 spec not found at $OPENAPI_FILE" 18 | exit 1 19 | fi 20 | if ! command -v api-spec-converter &>/dev/null; then 21 | echo "Error: api-spec-converter not found. Please install it with: npm install -g api-spec-converter" 22 | exit 1 23 | fi 24 | 25 | # Convert OpenAPI 3.0 to Swagger 2.0 26 | echo "Converting $OPENAPI_FILE to $SWAGGER_FILE..." 27 | api-spec-converter --from=openapi_3 --to=swagger_2 --syntax=yaml "$OPENAPI_FILE" >"$SWAGGER_FILE" 28 | 29 | if [ $? -eq 0 ]; then 30 | echo "✅ Successfully converted OpenAPI 3.0 to Swagger 2.0" 31 | echo " - OpenAPI 3.0: openapi.yml" 32 | echo " - Swagger 2.0: swagger.yml" 33 | else 34 | echo "❌ Conversion failed" 35 | exit 1 36 | fi 37 | -------------------------------------------------------------------------------- /backend/internal/server/openapi.go: -------------------------------------------------------------------------------- 1 | // Package server OpenTofu Registry Docs API 2 | // 3 | // @title OpenTofu Registry Docs API 4 | // @description The API to fetch documentation index and documentation files from the OpenTofu registry. 5 | // @version 1.0.0-beta 6 | // @license.name MPL-2.0 7 | // @servers.url https://api.opentofu.org 8 | // @servers.description OpenTofu Registry API server 9 | package server 10 | 11 | import ( 12 | "context" 13 | _ "embed" 14 | 15 | "github.com/opentofu/registry-ui/internal/indexstorage" 16 | ) 17 | 18 | //go:embed openapi.yml 19 | var openapiYaml []byte 20 | 21 | //go:embed swagger.yml 22 | var swaggerYaml []byte 23 | 24 | //go:embed index.html 25 | var indexHTML []byte 26 | 27 | type OpenAPIWriter interface { 28 | Write(ctx context.Context) error 29 | } 30 | 31 | func NewWriter(storage indexstorage.API) (OpenAPIWriter, error) { 32 | return &writer{ 33 | storage: storage, 34 | }, nil 35 | } 36 | 37 | type writer struct { 38 | storage indexstorage.API 39 | } 40 | 41 | func (w writer) Write(ctx context.Context) error { 42 | if err := w.storage.WriteFile(ctx, "swagger.yml", swaggerYaml); err != nil { 43 | return err 44 | } 45 | if err := w.storage.WriteFile(ctx, "openapi.yml", openapiYaml); err != nil { 46 | return err 47 | } 48 | if err := w.storage.WriteFile(ctx, "index.html", indexHTML); err != nil { 49 | return err 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /blocklist.json: -------------------------------------------------------------------------------- 1 | { 2 | "providers": { 3 | }, 4 | "modules": { 5 | } 6 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres 4 | environment: 5 | POSTGRES_USER: postgres 6 | POSTGRES_DB: postgres 7 | POSTGRES_PASSWORD: secret 8 | ports: 9 | - "5432:5432" 10 | volumes: 11 | - type: bind 12 | source: ./search/pg-indexer/schema.sql 13 | target: /docker-entrypoint-initdb.d/schema.sql 14 | frontend: 15 | build: 16 | context: frontend 17 | volumes: 18 | - source: ./frontend 19 | target: /work 20 | type: bind 21 | ports: 22 | - "3000:3000" 23 | dataapi: 24 | build: 25 | context: search/worker 26 | ports: 27 | - "8787:8787" 28 | volumes: 29 | - source: ./search/worker 30 | target: /work 31 | type: bind 32 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | VITE_DATA_API_URL= 2 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env 27 | 28 | .pnpm-store -------------------------------------------------------------------------------- /frontend/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/.gitkeep -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | package-lock.json 3 | node_modules 4 | .pnpm-store 5 | .env 6 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "plugins": ["prettier-plugin-tailwindcss"] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | 3 | WORKDIR /work 4 | RUN npm install -g pnpm 5 | VOLUME ["/work"] 6 | EXPOSE 3000 7 | 8 | CMD ["/bin/bash", "-c", "echo VITE_DATA_API_URL=http://127.0.0.1:8787 >.env && pnpm i && pnpm run dev --host 0.0.0.0 --port 3000"] 9 | -------------------------------------------------------------------------------- /frontend/announcement.md: -------------------------------------------------------------------------------- 1 | This is the **beta** preview of the OpenTofu Registry Search. The data in this interface may not be up to date. 2 | -------------------------------------------------------------------------------- /frontend/docs/index.md: -------------------------------------------------------------------------------- 1 | # The OpenTofu Registry 2 | 3 | The OpenTofu Registry provides an API for OpenTofu to locate providers and modules. This documentation will guide you through the most important steps in using the registry or publishing your modules and providers. 4 | 5 | ## For users 6 | 7 | This section contains the documentation for OpenTofu users. 8 | 9 | - [Overview](/docs/users) 10 | - [Using a provider](/docs/users/providers) 11 | - [Using a module](/docs/users/modules) 12 | 13 | ## For provider authors 14 | 15 | This section guides you through the steps of creating and publishing an OpenTofu provider. 16 | 17 | - [Overview](/docs/providers) 18 | - [Creating a provider](/docs/providers/creating) 19 | - [Writing docs for your provider](/docs/providers/docs) 20 | - [Publishing your provider](/docs/providers/publishing) 21 | - [Adding a provider to the registry](/docs/providers/adding) 22 | 23 | ## For module authors 24 | 25 | This section shows you how to create and publish a module. 26 | 27 | - [Overview](/docs/modules) 28 | - [Creating a module](/docs/modules/creating) 29 | - [Publishing a module](/docs/modules/publishing) 30 | - [Adding a module to the OpenTofu Registry](/docs/modules/adding) 31 | -------------------------------------------------------------------------------- /frontend/docs/modules/adding.md: -------------------------------------------------------------------------------- 1 | # Adding your module to the OpenTofu registry 2 | 3 | Once you have [published your module](/docs/modules/publishing), you can add your module to the OpenTofu Registry. You can do this by [creating an issue](https://github.com/opentofu/registry/issues/new/choose) on the OpenTofu Registry GitHub repository. 4 | 5 | Here you will have to provide your username/organization name and repository name, which will translate to a module name. For example, consider the following repository: 6 | 7 | ``` 8 | YOURNAME/terraform-NAME-TARGETSYSTEM 9 | ``` 10 | 11 | This will translate to a module address your users can reference as `YOURNAME/NAME/TARGETSYSTEM`. 12 | -------------------------------------------------------------------------------- /frontend/docs/modules/index.md: -------------------------------------------------------------------------------- 1 | # The OpenTofu Registry for Module Authors 2 | 3 | This section shows you how to create and publish a module. 4 | 5 | - [Creating a module](/docs/modules/creating) 6 | - [Publishing a module](/docs/modules/publishing) 7 | - [Adding a module to the OpenTofu Registry](/docs/modules/adding) 8 | -------------------------------------------------------------------------------- /frontend/docs/modules/publishing.md: -------------------------------------------------------------------------------- 1 | # Publishing an OpenTofu module 2 | 3 | You can host OpenTofu modules in any git repository. However, if you would like to publish the module in the OpenTofu Registry, you will need to host it on [GitHub](https://docs.github.com/en/get-started/start-your-journey). 4 | 5 | Once you pushed your code, make sure to [create a tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) following [semantic versioning](https://semver.org/). This tag will translate to a version in the OpenTofu registry. 6 | 7 | Once you have pushed your tag, you can now [add the module to the Registry](/docs/modules/adding). 8 | -------------------------------------------------------------------------------- /frontend/docs/providers/adding.md: -------------------------------------------------------------------------------- 1 | # Adding your provider to the OpenTofu Registry 2 | 3 | Once you have [published your provider](/docs/providers/publishing), you are ready to add your provider to the OpenTofu Registry. You only need to perform these steps once, the OpenTofu Registry will automatically discover new versions you publish. 4 | 5 | ## Adding the provider 6 | 7 | To add your provider, please go to the [OpenTofu Registry repository](https://github.com/opentofu/registry/issues/new/choose) and select `Submit new Provider`. In the `Provider Repository` field please enter `YOURNAME/terraform-provider-YOURPROVIDER` and submit the issue. An OpenTofu team member will review your submission and merge it into the Registry. Your provider should be live within an hour after merging. 8 | 9 | ## Adding the GPG key 10 | 11 | After your provider is merged, you can proceed to add your GPG key. You will need to perform the following steps: 12 | 13 | 1. Export your _public_ GPG key into a text file with ASCII-armor. 14 | 2. If your provider is located in an organization, make sure you make [your membership in the organization public](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-your-membership-in-organizations/publicizing-or-hiding-organization-membership). This is required to validate you have the rights to publish a GPG key. 15 | 3. Go to the [OpenTofu Registry repository](https://github.com/opentofu/registry/issues/new/choose) and select `Submit new Provider Signing Key`. 16 | 4. In the `Provider Namespace` field enter your username or organization name. 17 | 5. In the `Provider GPG Key` field paste your GPG key. 18 | 19 | ~> The GPG key applies to all providers under your username or organization. Don't submit a GPG key if you have providers that are not signed or are signed with a key you don't have. 20 | -------------------------------------------------------------------------------- /frontend/docs/providers/docs.md: -------------------------------------------------------------------------------- 1 | # Writing documentation for your provider 2 | 3 | In order for your provider to show up in the OpenTofu Registry Search properly, you will need to write some documentation. Tools like [terraform-plugin-docs](https://github.com/hashicorp/terraform-plugin-docs) can help you by auto-generating much of the documentation based on your provider schema. 4 | 5 | ## Documentation structure 6 | 7 | You can place your documentation in the `docs` folder in your repository. Please create the files using the following naming convention: 8 | 9 | - `/docs/guides/.md` for guides. 10 | - `/docs/resources/.md` for resources. (Note: if your resource is called `yourprovider_yourresource`, you should only include `yourresource` here.) 11 | - `/docs/data-sources/.md` for resources. (Note: same as for resources) 12 | - `/docs/functions/.md` for functions. 13 | 14 | Additionally, if you would like to support CDKTF, you can create the following documents: 15 | 16 | - `/docs/cdktf/[python|typescript|csharp|java|go]/resources/.md` 17 | - `/docs/cdktf/[python|typescript|csharp|java|go]/data-sources/.md` 18 | - `/docs/cdktf/[python|typescript|csharp|java|go]/functions/.md` 19 | 20 | You can include the following header (front matter) in your markdown files: 21 | 22 | ```yaml 23 | --- 24 | page_title: Title of the page 25 | subcategory: Subcategory to place the page in on the sidebar (optional) 26 | description: Description of the page 27 | --- 28 | ``` 29 | 30 | Once you have written your documentation, you can [proceed to publish your provider](/docs/providers/publishing). 31 | -------------------------------------------------------------------------------- /frontend/docs/providers/index.md: -------------------------------------------------------------------------------- 1 | # The OpenTofu Registry for Provider Authors 2 | 3 | This section of the documentation guides you through the basic steps of creating and publishing a provider in the OpenTofu Registry. The documentation assumes you are familiar with the Go programming language. 4 | 5 | ## In this section 6 | 7 | - [Creating a provider](/docs/providers/creating) 8 | - [Writing docs for your provider](/docs/providers/docs) 9 | - [Publishing your provider](/docs/providers/publishing) 10 | - [Adding a provider to the registry](/docs/providers/adding) 11 | -------------------------------------------------------------------------------- /frontend/docs/users/index.md: -------------------------------------------------------------------------------- 1 | # The OpenTofu Registry for Users 2 | 3 | OpenTofu is a tool that works with integrations made by the community called _providers_. In addition to that, community-made modules simplify many common tasks. The OpenTofu Registry contains an index pointing to thousands of providers and thousands of modules on GitHub you can use. You can install them automatically by adding a code snippet to your project and running `tofu init`. 4 | 5 | ## In this chapter 6 | 7 | - [Using a provider](/docs/users/providers) 8 | - [Using a module](/docs/users/modules) 9 | -------------------------------------------------------------------------------- /frontend/docs/users/modules.md: -------------------------------------------------------------------------------- 1 | # Using a module 2 | 3 | Modules provide reusable pieces of code for your OpenTofu project. The OpenTofu Registry contains references to over 20,000 modules on GitHub created by the community. You can [find a module for your use case using the OpenTofu Registry Search](https://search.opentofu.org/modules/). You can learn more about how modules work in OpenTofu from the [OpenTofu documentation](https://opentofu.org/docs/language/modules/). 4 | 5 | ~> The OpenTofu Registry does not perform security scanning on modules, and they may contain malicious code. Inspect any module you intend to use and only use modules from authors you trust. 6 | 7 | ## Integrating a module in your project 8 | 9 | Module addresses have three parts: namespaces, names, and target systems. You can include a module in your project by specifying its address and its version: 10 | 11 | ```hcl2 12 | module "my_name_for_the_module" { 13 | source = "NAMESPACE/NAME/TARGETSYSTEM" 14 | version = "v1.2.3" 15 | 16 | # Add parameters for the module here. 17 | } 18 | ``` 19 | 20 | Specifying the version tells OpenTofu to fetch the module from the registry. Once added, you can run `tofu init` to download the module. 21 | 22 | For more information about modules, see [the OpenTofu documentation](https://opentofu.org/docs/language/modules/sources/). 23 | 24 | ## Reporting provider issues 25 | 26 | If you find a bug in a module, please report the issue directly to the provider author. The OpenTofu team cannot fix module issues. 27 | 28 | -> Module namespaces, names and target systems in the OpenTofu registry translate directly to GitHub URLs in the form of `github.com/NAMESPACE/terraform-TARGETSYSTEM-NAME`. 29 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | export default tseslint.config({ 8 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 9 | files: ["**/*.{ts,tsx}"], 10 | ignores: ["dist", "src/api.d.ts"], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | }, 15 | plugins: { 16 | "react-hooks": reactHooks, 17 | "react-refresh": reactRefresh, 18 | }, 19 | rules: { 20 | ...reactHooks.configs.recommended.rules, 21 | "react-refresh/only-export-components": [ 22 | "warn", 23 | { allowConstantExport: true }, 24 | ], 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/public/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/public/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/fonts/dmsans-bold-latin-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/public/fonts/dmsans-bold-latin-ext.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/dmsans-bold-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/public/fonts/dmsans-bold-latin.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/dmsans-latin-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/public/fonts/dmsans-latin-ext.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/dmsans-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/public/fonts/dmsans-latin.woff2 -------------------------------------------------------------------------------- /frontend/public/maskable-icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/public/maskable-icon-512x512.png -------------------------------------------------------------------------------- /frontend/public/open-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/public/open-graph.png -------------------------------------------------------------------------------- /frontend/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/public/pwa-192x192.png -------------------------------------------------------------------------------- /frontend/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/public/pwa-512x512.png -------------------------------------------------------------------------------- /frontend/public/pwa-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentofu/registry-ui/322a95dc92cb1cb9ddbdde78cd7407566a65a66f/frontend/public/pwa-64x64.png -------------------------------------------------------------------------------- /frontend/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenTofu Registry", 3 | "short_name": "OpenTofu Registry", 4 | "theme_color": "#ffda18", 5 | "icons": [ 6 | { 7 | "src": "pwa-64x64.png", 8 | "sizes": "64x64", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "pwa-192x192.png", 13 | "sizes": "192x192", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "pwa-512x512.png", 18 | "sizes": "512x512", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "maskable-icon-512x512.png", 23 | "sizes": "512x512", 24 | "type": "image/png", 25 | "purpose": "maskable" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /frontend/pwa-assets.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | minimal2023Preset as preset, 4 | } from "@vite-pwa/assets-generator/config"; 5 | 6 | export default defineConfig({ 7 | headLinkOptions: { 8 | preset: "2023", 9 | }, 10 | preset, 11 | images: ["public/favicon.svg"], 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/components/AnnouncementBar/content.ts: -------------------------------------------------------------------------------- 1 | import { unified } from "unified"; 2 | import remarkParse from "remark-parse"; 3 | import remarkRehype from "remark-rehype"; 4 | import rehypeStringify from "rehype-stringify"; 5 | import announcement from "../../../announcement.md?raw"; 6 | 7 | export const content = unified() 8 | .use(remarkParse) 9 | .use(remarkRehype) 10 | .use(rehypeStringify) 11 | .processSync(announcement).value; 12 | -------------------------------------------------------------------------------- /frontend/src/components/AnnouncementBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { content } from "./content" with { type: "macro" }; 2 | 3 | export function AnnouncementBar() { 4 | return ( 5 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/components/Breadcrumbs/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link, NavLink, UIMatch, useMatches } from "react-router-dom"; 2 | import { Fragment } from "react"; 3 | import { chevron } from "../../icons/chevron"; 4 | import { Icon } from "../Icon"; 5 | import { home } from "../../icons/home"; 6 | import clsx from "clsx"; 7 | import { Crumb } from "../../crumbs"; 8 | 9 | interface BreadcrumbsProps { 10 | className?: string; 11 | } 12 | 13 | export function Breadcrumbs({ className }: BreadcrumbsProps) { 14 | const matches = useMatches() as Array< 15 | UIMatch Crumb }> 16 | >; 17 | 18 | const crumbs = matches 19 | .filter((match) => Boolean(match.handle?.crumb)) 20 | .map((match) => match.handle.crumb(match.data)) 21 | .flat(); 22 | 23 | return ( 24 | 53 | ); 54 | } 55 | 56 | export function BreadcrumbsSkeleton() { 57 | return ( 58 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /frontend/src/components/CardItem/Footer.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { ReactNode } from "react"; 3 | 4 | interface CardItemFooterProps { 5 | children: ReactNode; 6 | } 7 | 8 | export function CardItemFooter({ children }: CardItemFooterProps) { 9 | return ( 10 |
11 |
{children}
12 |
13 | ); 14 | } 15 | 16 | interface CardItemFooterDetailProps { 17 | label: string; 18 | children: ReactNode; 19 | className?: string; 20 | } 21 | 22 | export function CardItemFooterDetail({ 23 | label, 24 | children, 25 | className, 26 | }: CardItemFooterDetailProps) { 27 | return ( 28 |
29 |
{label}
30 |
{children}
31 |
32 | ); 33 | } 34 | 35 | export function CardItemFooterDetailSkeleton() { 36 | return ( 37 |
38 | 39 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/CardItem/Title.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | interface CardItemTitleProps { 5 | children: ReactNode; 6 | linkProps: { 7 | to: string; 8 | }; 9 | } 10 | 11 | export function CardItemTitle({ children, linkProps }: CardItemTitleProps) { 12 | return ( 13 |

14 | 15 | 16 | {children} 17 | 18 |

19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/components/CardItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface CardItemProps { 4 | children: ReactNode; 5 | } 6 | 7 | export function CardItem({ children }: CardItemProps) { 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/Code/SyntaxHighlighter.tsx: -------------------------------------------------------------------------------- 1 | import { Highlight, Prism } from "prism-react-renderer"; 2 | import { theme } from "./theme"; 3 | import { suspend } from "suspend-react"; 4 | 5 | globalThis.Prism = Prism; 6 | 7 | interface SyntaxHighlighterProps { 8 | value: string; 9 | language: string; 10 | } 11 | 12 | const languages: Record Promise> = { 13 | hcl: () => import("prismjs/components/prism-hcl"), 14 | }; 15 | 16 | const aliases: Record = { 17 | terraform: "hcl", 18 | tf: "hcl", 19 | }; 20 | 21 | export function SyntaxHighlighter({ value, language }: SyntaxHighlighterProps) { 22 | if (aliases[language]) { 23 | language = aliases[language]; 24 | } 25 | 26 | if (languages[language]) { 27 | suspend(languages[language], [language]); 28 | } 29 | 30 | const trimmedValue = value.replace(/^\n+|\n+$/g, ""); 31 | 32 | return ( 33 | 34 | {({ tokens, getLineProps, getTokenProps }) => ( 35 | <> 36 | {tokens.map((line, i) => ( 37 |
38 | {line.map((token, key) => ( 39 | 40 | ))} 41 |
42 | ))} 43 | 44 | )} 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/components/Code/index.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, useRef, useState } from "react"; 2 | import { SyntaxHighlighter } from "./SyntaxHighlighter"; 3 | import clsx from "clsx"; 4 | import { Icon } from "../Icon"; 5 | import { tick } from "../../icons/tick"; 6 | import { copy } from "../../icons/copy"; 7 | 8 | interface CodeProps { 9 | value: string; 10 | language: string; 11 | className?: string; 12 | } 13 | 14 | // TODO: make the button accessible 15 | export function Code({ value, language, className }: CodeProps) { 16 | const [copied, setCopied] = useState(false); 17 | const timeoutRef = useRef(null); 18 | 19 | const copyToClipboard = async () => { 20 | if (timeoutRef.current) { 21 | clearTimeout(timeoutRef.current); 22 | } 23 | 24 | try { 25 | await navigator.clipboard.writeText(value); 26 | setCopied(true); 27 | } finally { 28 | timeoutRef.current = setTimeout(() => setCopied(false), 1000); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 |
35 |         {value}}>
36 |           
37 |         
38 |       
39 | 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/components/Code/theme.ts: -------------------------------------------------------------------------------- 1 | import { PrismTheme } from "prism-react-renderer"; 2 | 3 | // TODO: finish the theme 4 | export const theme: PrismTheme = { 5 | plain: {}, 6 | styles: [ 7 | { 8 | types: ["comment"], 9 | style: { 10 | color: "var(--syntax-comment)", 11 | }, 12 | }, 13 | { 14 | types: ["keyword"], 15 | style: { 16 | color: "var(--syntax-keyword)", 17 | }, 18 | }, 19 | { 20 | types: ["boolean"], 21 | style: { 22 | color: "var(--syntax-boolean)", 23 | }, 24 | }, 25 | { 26 | types: ["property"], 27 | style: { 28 | color: "var(--syntax-property)", 29 | }, 30 | }, 31 | { 32 | types: ["punctuation"], 33 | style: { 34 | color: "var(--syntax-punctuation)", 35 | }, 36 | }, 37 | { 38 | types: ["string"], 39 | style: { 40 | color: "var(--syntax-string)", 41 | }, 42 | }, 43 | { 44 | types: ["variable"], 45 | style: { 46 | color: "var(--syntax-variable)", 47 | }, 48 | }, 49 | ], 50 | }; 51 | -------------------------------------------------------------------------------- /frontend/src/components/DateTime/index.tsx: -------------------------------------------------------------------------------- 1 | const dateFormat = new Intl.DateTimeFormat(); 2 | 3 | interface DateTimeProps { 4 | value: string; 5 | } 6 | 7 | export function DateTime({ value }: DateTimeProps) { 8 | const dateTimeObj = dateFormat.format(new Date(value)); 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/components/EditLink/index.tsx: -------------------------------------------------------------------------------- 1 | interface EditLinkProps { 2 | url: string; 3 | } 4 | 5 | export function EditLink({ url }: EditLinkProps) { 6 | return ( 7 | 13 | Edit this page 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/components/EmptyState/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@/components/Icon"; 2 | import { Paragraph } from "@/components/Paragraph"; 3 | import { empty } from "@/icons/empty"; 4 | import clsx from "clsx"; 5 | 6 | interface EmptyStateProps { 7 | text: string; 8 | className?: string; 9 | } 10 | 11 | export function EmptyState({ text, className }: EmptyStateProps) { 12 | return ( 13 | 14 | 15 | {text} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { Paragraph } from "../Paragraph"; 2 | 3 | export function Footer() { 4 | return ( 5 |
6 |
7 | 8 | Copyright © OpenTofu a Series of LF Projects, LLC and its 9 | contributors. Documentation materials are licensed under various open 10 | sources license from other authors, see the referenced license files 11 | for details. For web site terms of use, trademark policy, privacy 12 | policy and other project policies please see{" "} 13 | 19 | lfprojects.org/policies 20 | 21 | . 22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Link.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { Link, useMatches } from "react-router-dom"; 3 | 4 | interface LinkProps { 5 | label: string; 6 | to: string; 7 | isActive: (routeId: string) => boolean; 8 | } 9 | 10 | export function HeaderLink({ label, to, isActive }: LinkProps) { 11 | const matches = useMatches(); 12 | 13 | const isActiveMatch = !!matches.find((match) => isActive(match.id)); 14 | 15 | return ( 16 | 23 | {label} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/Header/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { sun } from "../../icons/sun"; 3 | import { Icon } from "../Icon"; 4 | import { moon } from "../../icons/moon"; 5 | 6 | export function ThemeSwitcher() { 7 | const [isDark, setIsDark] = useState( 8 | document.documentElement.classList.contains("dark"), 9 | ); 10 | 11 | const toggleTheme = () => { 12 | const newTheme = isDark ? "light" : "dark"; 13 | localStorage.setItem("theme", newTheme); 14 | 15 | const newValue = !isDark; 16 | setIsDark(newValue); 17 | document.documentElement.classList.toggle("dark", newValue); 18 | }; 19 | 20 | return ( 21 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/HeadingLink/index.tsx: -------------------------------------------------------------------------------- 1 | interface HeadingLinkProps { 2 | id: string; 3 | label: string; 4 | } 5 | 6 | export function HeadingLink({ id, label }: HeadingLinkProps) { 7 | return ( 8 | 13 | # 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | interface IconProps { 2 | path: string; 3 | className?: string; 4 | viewBox?: string; 5 | width?: number; 6 | height?: number; 7 | title?: string; 8 | } 9 | 10 | export function Icon({ 11 | title, 12 | path, 13 | className, 14 | width = 24, 15 | height = 24, 16 | }: IconProps) { 17 | return ( 18 | 24 | {title ? {title} : null} 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/InfoSection/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface InfoSectionProps { 4 | children: ReactNode; 5 | } 6 | 7 | export function InfoSection({ children }: InfoSectionProps) { 8 | return ( 9 |
10 |
{children}
11 |
12 | ); 13 | } 14 | 15 | interface InfoSectionItemProps { 16 | label: string; 17 | children: ReactNode; 18 | } 19 | 20 | export function InfoSectionItem({ label, children }: InfoSectionItemProps) { 21 | return ( 22 |
23 |
{label}
24 |
{children}
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/components/LanguagePicker/index.tsx: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx"; 2 | import { Link, useSearchParams } from "react-router-dom"; 3 | 4 | interface LanguageProps { 5 | name: string; 6 | code: string | null; 7 | } 8 | 9 | function Language({ name, code }: LanguageProps) { 10 | const [searchParams] = useSearchParams(); 11 | const isActive = searchParams.get("lang") === code; 12 | 13 | return ( 14 | 23 | {name} 24 | 25 | ); 26 | } 27 | 28 | interface LanguagePickerProps { 29 | languages: Array<{ name: string; code: string }>; 30 | } 31 | 32 | export function LanguagePicker({ languages }: LanguagePickerProps) { 33 | return ( 34 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/A.tsx: -------------------------------------------------------------------------------- 1 | import { AnchorHTMLAttributes } from "react"; 2 | 3 | function isExternalLink(href: string) { 4 | return href.startsWith("http"); 5 | } 6 | 7 | export function MarkdownA({ 8 | children, 9 | href, 10 | ...rest 11 | }: AnchorHTMLAttributes) { 12 | return ( 13 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/Code.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | export function MarkdownCode({ children }: HTMLAttributes) { 4 | return ( 5 | 6 | {children} 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/H1.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | import { HeadingLink } from "../HeadingLink"; 3 | 4 | export function MarkdownH1({ 5 | children, 6 | id, 7 | }: HTMLAttributes) { 8 | return ( 9 |

13 | {children} 14 | {id && } 15 |

16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/H2.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | import { HeadingLink } from "../HeadingLink"; 3 | 4 | export function MarkdownH2({ 5 | children, 6 | id, 7 | }: HTMLAttributes) { 8 | return ( 9 |

13 | {children} 14 | {id && } 15 |

16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/H3.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | import { HeadingLink } from "../HeadingLink"; 3 | 4 | export function MarkdownH3({ 5 | children, 6 | id, 7 | }: HTMLAttributes) { 8 | return ( 9 |
13 | {children} 14 | {id && } 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/Hr.tsx: -------------------------------------------------------------------------------- 1 | export function MarkdownHr() { 2 | return ( 3 |
4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/Img.tsx: -------------------------------------------------------------------------------- 1 | import { ImgHTMLAttributes } from "react"; 2 | 3 | export function MarkdownImg({ src, alt }: ImgHTMLAttributes) { 4 | return {alt}; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/Li.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | export function MarkdownLi({ children }: HTMLAttributes) { 4 | return ( 5 |
  • 6 | {children} 7 |
  • 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/Ol.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | export function MarkdownOl({ children }: HTMLAttributes) { 4 | return ( 5 |
      6 | {children} 7 |
    8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/Pre.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, ReactElement } from "react"; 2 | import { Code } from "../Code"; 3 | 4 | export function MarkdownPre({ children }: HTMLAttributes) { 5 | if (!children) { 6 | return null; 7 | } 8 | 9 | const child = children as ReactElement; 10 | 11 | if (!child.props) { 12 | return ( 13 |
    14 |         {children}
    15 |       
    16 | ); 17 | } 18 | 19 | if (!child.props.children) { 20 | return null; 21 | } 22 | 23 | const language = child.props.className?.match(/language-(\w+)/)?.[1]; 24 | 25 | return ( 26 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/Table.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | export function MarkdownTable({ children }: HTMLAttributes) { 4 | return ( 5 |
    6 | 7 | {children} 8 |
    9 |
    10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/Td.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | export function MarkdownTd({ children }: HTMLAttributes) { 4 | return ( 5 | 6 | {children} 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/Th.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | export function MarkdownTh({ children }: HTMLAttributes) { 4 | return ( 5 | 6 | {children} 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/Ul.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | export function MarkdownUl({ children }: HTMLAttributes) { 4 | return ( 5 |
      6 | {children} 7 |
    8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { processor } from "./processor"; 3 | 4 | interface MarkdownProps { 5 | text: string; 6 | } 7 | 8 | export function Markdown({ text }: MarkdownProps) { 9 | const { result } = useMemo( 10 | () => processor.processSync(text.trimStart()), 11 | [text], 12 | ); 13 | 14 | return result; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/MetaTags/index.tsx: -------------------------------------------------------------------------------- 1 | import { Helmet } from "react-helmet-async"; 2 | 3 | interface MetaTagsProps { 4 | title?: string; 5 | description?: string; 6 | } 7 | 8 | export function MetaTags({ title, description }: MetaTagsProps) { 9 | const siteTitle = title 10 | ? `${title} - OpenTofu Registry` 11 | : "OpenTofu Registry"; 12 | 13 | return ( 14 | 15 | {siteTitle} 16 | 17 | 18 | {description && } 19 | {description && } 20 | {description && } 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/ModuleInput/index.tsx: -------------------------------------------------------------------------------- 1 | import { HeadingLink } from "../HeadingLink"; 2 | import { Markdown } from "../Markdown"; 3 | import { Paragraph } from "../Paragraph"; 4 | 5 | interface ModuleInputProps { 6 | name: string; 7 | type: string; 8 | description: string; 9 | defaultValue?: unknown; 10 | } 11 | 12 | export function ModuleInput({ 13 | name, 14 | type, 15 | description, 16 | defaultValue, 17 | }: ModuleInputProps) { 18 | const showDefaultValue = defaultValue !== undefined; 19 | return ( 20 |
  • 21 |

    22 | {name}{" "} 23 | 24 | ({type}) 25 | 26 | 27 |

    28 | 29 | 30 | 31 | {showDefaultValue && ( 32 | 33 | Default value:{" "} 34 | 35 | {JSON.stringify(defaultValue)} 36 | 37 | 38 | )} 39 |
  • 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/components/ModuleOutput/index.tsx: -------------------------------------------------------------------------------- 1 | import { HeadingLink } from "../HeadingLink"; 2 | import { Paragraph } from "../Paragraph"; 3 | 4 | interface ModuleOutputProps { 5 | name: string; 6 | description: string; 7 | } 8 | 9 | export function ModuleOutput({ name, description }: ModuleOutputProps) { 10 | return ( 11 |
  • 12 |

    13 | {name} 14 | 15 |

    16 | {description} 17 |
  • 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/ModuleOutputs/index.tsx: -------------------------------------------------------------------------------- 1 | import { EmptyState } from "@/components/EmptyState"; 2 | import { definitions } from "@/api"; 3 | import { ModuleOutput } from "../ModuleOutput"; 4 | 5 | interface ModuleOutputsProps { 6 | outputs: Record; 7 | } 8 | 9 | export function ModuleOutputs({ outputs }: ModuleOutputsProps) { 10 | const outputsWithNames = Object.entries(outputs).map(([name, output]) => ({ 11 | name, 12 | ...output, 13 | })); 14 | 15 | return ( 16 |
    17 |

    Outputs

    18 | 19 | {outputsWithNames.length === 0 && ( 20 | 21 | )} 22 | 23 | {outputsWithNames.length > 0 && ( 24 |
      25 | {outputsWithNames.map((output) => ( 26 | 31 | ))} 32 |
    33 | )} 34 |
    35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/components/ModuleResources/index.tsx: -------------------------------------------------------------------------------- 1 | import { Paragraph } from "@/components/Paragraph"; 2 | import { EmptyState } from "@/components/EmptyState"; 3 | import { definitions } from "@/api"; 4 | 5 | interface ModuleResourcesProps { 6 | resources: Array; 7 | } 8 | 9 | export function ModuleResources({ resources }: ModuleResourcesProps) { 10 | return ( 11 |
    12 |

    Resources

    13 | 14 | When using this module, it may create some resources. Below is a list of 15 | the resources that the module may create. These resources are identified 16 | by their unique address. However it is possible that some of these 17 | resources may either be not created at all, or multiples of them may be 18 | be created. 19 | 20 | 21 | {resources.length === 0 && ( 22 | 26 | )} 27 | 28 | {resources.length > 0 && ( 29 |
      30 | {resources.map((resource) => ( 31 |
    • 35 | {resource.address} 36 |
    • 37 | ))} 38 |
    39 | )} 40 |
    41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/OldVersionBanner/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { info } from "../../icons/info"; 3 | import { Icon } from "../Icon"; 4 | 5 | interface OldVersionBannerProps { 6 | latestVersionLink: string; 7 | } 8 | 9 | export function OldVersionBanner({ latestVersionLink }: OldVersionBannerProps) { 10 | return ( 11 |
    12 | 13 | 14 | You are viewing an outdated version.{" "} 15 | 16 | View the latest version. 17 | 18 | 19 |
    20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/components/PageTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface PageTitleProps { 4 | children: ReactNode; 5 | } 6 | 7 | export function PageTitle({ children }: PageTitleProps) { 8 | return

    {children}

    ; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/Paragraph/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { ReactNode } from "react"; 3 | 4 | interface ParagraphProps { 5 | children: ReactNode; 6 | className?: string; 7 | } 8 | 9 | export function Paragraph({ children, className }: ParagraphProps) { 10 | return ( 11 |

    12 | {children} 13 |

    14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/PatternBg/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./styles.module.css"; 2 | 3 | export default function PatternBg() { 4 | return ( 5 |
    9 |
    10 |
    11 |
    12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/RepoSidebarBlock/index.tsx: -------------------------------------------------------------------------------- 1 | import { github } from "@/icons/github"; 2 | import { Icon } from "../Icon"; 3 | import { SidebarBlock } from "../SidebarBlock"; 4 | 5 | function getLinkLabel(url: string) { 6 | try { 7 | const parsedUrl = new URL(url); 8 | 9 | switch (parsedUrl.hostname) { 10 | case "github.com": { 11 | const pathParts = parsedUrl.pathname.split("/"); 12 | 13 | return ( 14 | <> 15 | 16 | 17 | {pathParts[1]}/{pathParts[2]} 18 | 19 | 20 | ); 21 | } 22 | default: 23 | return `${parsedUrl.hostname}${parsedUrl.pathname}`; 24 | } 25 | } catch { 26 | return url; 27 | } 28 | } 29 | 30 | interface BlockProps { 31 | link?: string | undefined; 32 | } 33 | 34 | export function RepoSidebarBlock(props: BlockProps) { 35 | return ( 36 | 37 | {props.link ? ( 38 | 44 | {getLinkLabel(props.link)} 45 | 46 | ) : ( 47 | 48 | )} 49 | 50 | ); 51 | } 52 | 53 | export function RepoSidebarBlockSkeleton() { 54 | return ; 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/components/Search/ModuleResult.tsx: -------------------------------------------------------------------------------- 1 | import { SearchResult } from "./types"; 2 | 3 | interface SearchModuleResultProps { 4 | result: SearchResult; 5 | } 6 | 7 | export function SearchModuleResult({ result }: SearchModuleResultProps) { 8 | return ( 9 | <> 10 |
    {result.displayTitle}
    11 |
    12 | {result.description} 13 |
    14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/components/Search/OtherResult.tsx: -------------------------------------------------------------------------------- 1 | import { SearchResult } from "./types"; 2 | 3 | interface SearchOtherResultProps { 4 | result: SearchResult; 5 | } 6 | 7 | export function SearchOtherResult({ result }: SearchOtherResultProps) { 8 | return ( 9 | <> 10 |
    {result.addr}
    11 |
    12 | {result.description} 13 |
    14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/components/Search/ProviderResult.tsx: -------------------------------------------------------------------------------- 1 | import { SearchResult, SearchResultType } from "./types"; 2 | 3 | interface SearchProviderResultProps { 4 | result: SearchResult; 5 | } 6 | 7 | export function SearchProviderResult({ result }: SearchProviderResultProps) { 8 | const description = ( 9 |
    10 | {result.description} 11 |
    12 | ); 13 | 14 | if (result.type === SearchResultType.ProviderResource) { 15 | return ( 16 | <> 17 |
    Resource: {result.displayTitle}
    18 | {description} 19 | 20 | ); 21 | } else if (result.type === SearchResultType.ProviderDatasource) { 22 | return ( 23 | <> 24 |
    Data source: {result.displayTitle}
    25 | {description} 26 | 27 | ); 28 | } else if (result.type === SearchResultType.ProviderFunction) { 29 | return ( 30 | <> 31 |
    Function: {result.displayTitle}
    32 | {description} 33 | 34 | ); 35 | } 36 | 37 | return ( 38 | <> 39 |
    {result.addr}
    40 | {description} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/components/Search/types.ts: -------------------------------------------------------------------------------- 1 | export enum SearchResultType { 2 | Provider = "provider", 3 | Module = "module", 4 | ProviderResource = "provider/resource", 5 | ProviderDatasource = "provider/datasource", 6 | ProviderFunction = "provider/function", 7 | Other = "other", 8 | } 9 | 10 | export interface SearchResult { 11 | id: string; 12 | addr: string; 13 | title: string; 14 | description: string; 15 | link: string; 16 | type: SearchResultType; 17 | 18 | displayTitle: string; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/SidebarBlock/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface SidebarBlockProps { 4 | title: ReactNode; 5 | children: ReactNode; 6 | } 7 | 8 | export function SidebarBlock({ title, children }: SidebarBlockProps) { 9 | return ( 10 |
    11 |

    12 | {title} 13 |

    14 | 15 | {children} 16 |
    17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/SidebarLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from "../Footer"; 2 | import { Header } from "../Header"; 3 | import { ReactNode } from "react"; 4 | 5 | interface SidebarLayoutProps { 6 | children: ReactNode; 7 | before?: ReactNode; 8 | after?: ReactNode; 9 | } 10 | 11 | export function SidebarLayout({ children, before, after }: SidebarLayoutProps) { 12 | return ( 13 | <> 14 |
    15 |
    16 | {before} 17 |
    {children}
    18 | {after} 19 |
    20 |