From 1512f68a37593f158cce5c25b6ff65dabcfe6935 Mon Sep 17 00:00:00 2001 From: Robert Main <50675045+rmainwork@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:16:25 -0400 Subject: [PATCH 1/3] Remove edit button only for terraform enterprise (#2564) * Simplify isPublicContentRepo expression Since `isHcp` and `isSentinel` aren't used elsewhere, this expression can be simplified down to a one-liner Co-authored-by: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> * Use URL to detect terraform enterprise This will hide the "Edit on GitHub" link for terraform enterprise only * Move PTFE detection logic from server.ts to loader * Move private repo logic from server.ts to loader Since logic was implemented in the loader to hide the "Edit this page on GitHub" link for PTFE, it makes sense to move the other (similar) logic there too to keep everything together. Additionally, this also allows the expression to be cleaned up to use `this.opts.product` and `Array.includes()` to detect private repos. --------- Co-authored-by: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> --- src/views/docs-view/loaders/remote-content.ts | 17 ++++++++++++++++- src/views/docs-view/server.ts | 14 +------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/views/docs-view/loaders/remote-content.ts b/src/views/docs-view/loaders/remote-content.ts index 36b55e43e2..89d9862ba2 100644 --- a/src/views/docs-view/loaders/remote-content.ts +++ b/src/views/docs-view/loaders/remote-content.ts @@ -264,7 +264,22 @@ export default class RemoteContentLoader implements DataLoader { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion versionMetadataList.find((e) => e.version === document.version)! .isLatest - if (isLatest) { + + /** + * We want to show "Edit on GitHub" links for public content repos only. + * Currently, HCP, PTFE and Sentinel docs are stored in private + * repositories. + * + * Note: If we need more granularity here, we could change this to be + * part of `rootDocsPath` configuration in `src/data/.json`. + */ + const isPrivateContentRepo = [ + 'hcp-docs', + 'sentinel', + 'ptfe-releases', + ].includes(this.opts.product) + + if (isLatest && !isPrivateContentRepo) { // GitHub only allows you to modify a file if you are on a branch, not a commit githubFileUrl = `https://github.com/hashicorp/${this.opts.product}/blob/${this.opts.mainBranch}/${document.githubFile}` } diff --git a/src/views/docs-view/server.ts b/src/views/docs-view/server.ts index b60f05a9f2..18fe0184c7 100644 --- a/src/views/docs-view/server.ts +++ b/src/views/docs-view/server.ts @@ -410,19 +410,7 @@ export function getStaticGenerationFunctions< validVersions.length > 0 && (validVersions.length > 1 || validVersions[0].version !== 'v0.0.x') - /** - * We want to show "Edit on GitHub" links for public content repos only. - * Currently, HCP and Sentinel docs are stored in private repositories. - * - * Note: If we need more granularity here, we could change this to be - * part of `rootDocsPath` configuration in `src/data/.json`. - */ - const isHcp = product.slug == 'hcp' - const isSentinel = product.slug == 'sentinel' - const isPublicContentRepo = !isHcp && !isSentinel - if (isPublicContentRepo) { - layoutProps.githubFileUrl = githubFileUrl - } + layoutProps.githubFileUrl = githubFileUrl const { hideVersionSelector, projectName } = options From a612e0bde70509f306929b307e83d62690937aa9 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:50:08 -0400 Subject: [PATCH 2/3] get latest redirects based on nav-data not version metadata (#2563) * split target function into separate file * refactor to fetch with latest sha not ref * update working in test log Co-authored-by: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> --------- Co-authored-by: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> --- ...get-latest-content-sha-for-product.test.ts | 33 ++++++++++ .../get-latest-content-sha-for-product.js | 53 ++++++++++++++++ build-libs/redirects.js | 63 +++++++++---------- 3 files changed, 115 insertions(+), 34 deletions(-) create mode 100644 build-libs/__tests__/get-latest-content-sha-for-product.test.ts create mode 100644 build-libs/get-latest-content-sha-for-product.js diff --git a/build-libs/__tests__/get-latest-content-sha-for-product.test.ts b/build-libs/__tests__/get-latest-content-sha-for-product.test.ts new file mode 100644 index 0000000000..d345d39682 --- /dev/null +++ b/build-libs/__tests__/get-latest-content-sha-for-product.test.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import getLatestContentShaForProduct from '../get-latest-content-sha-for-product' +import fetchGithubFile from '@build-libs/fetch-github-file' +import { PRODUCT_REDIRECT_ENTRIES } from '@build-libs/redirects' + +describe('getLatestContentShaForProduct', () => { + PRODUCT_REDIRECT_ENTRIES.forEach(({ repo, path }) => { + it(`fetches the latest SHA for the "${repo}" repo`, async () => { + const latestSha = await getLatestContentShaForProduct(repo) + expect(typeof latestSha).toBe('string') + }) + + if (['hcp-docs', 'sentinel', 'ptfe-releases'].includes(repo)) { + console.log(`Skipping test for private repo "${repo}"`) + } else { + it(`fetches the latest SHA for the "${repo}" repo, then validates the SHA by fetching redirects`, async () => { + const latestSha = await getLatestContentShaForProduct(repo) + expect(typeof latestSha).toBe('string') + const redirectsFileString = await fetchGithubFile({ + owner: 'hashicorp', + repo: repo, + path: path, + ref: latestSha, + }) + expect(typeof redirectsFileString).toBe('string') + }) + } + }) +}) diff --git a/build-libs/get-latest-content-sha-for-product.js b/build-libs/get-latest-content-sha-for-product.js new file mode 100644 index 0000000000..6ea83fe127 --- /dev/null +++ b/build-libs/get-latest-content-sha-for-product.js @@ -0,0 +1,53 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +/** + * We're using the docs endpoints to fetch the latest SHA, so we use + * the env var for the docs API. + */ +const MKTG_CONTENT_DOCS_API = process.env.MKTG_CONTENT_DOCS_API + +/** + * A map of all possible `product` slugs to known content API endpoints that + * will return an object with a `ref` property that accurately reflects the + * ref from which the latest content was uploaded. + */ +const KNOWN_LATEST_REF_ENDPOINTS = { + boundary: '/api/content/boundary/nav-data/latest/docs', + nomad: '/api/content/nomad/nav-data/latest/docs', + vault: '/api/content/vault/nav-data/latest/docs', + vagrant: '/api/content/vagrant/nav-data/latest/docs', + packer: '/api/content/packer/nav-data/latest/docs', + consul: '/api/content/consul/nav-data/latest/docs', + 'terraform-docs-common': + '/api/content/terraform-docs-common/nav-data/latest/docs', + 'hcp-docs': '/api/content/hcp-docs/nav-data/latest/docs', + 'ptfe-releases': '/api/content/ptfe-releases/nav-data/latest/enterprise', + sentinel: '/api/content/sentinel/nav-data/latest/sentinel', +} + +/** + * Fetch the latest sha from the content API for a given product. + * This relies on known `nav-data` endpoints for each product. + * + * @param {string} product + * @returns {Promise} + */ +async function getLatestContentShaForProduct(product) { + const contentUrl = new URL(MKTG_CONTENT_DOCS_API) + const knownEndpoint = KNOWN_LATEST_REF_ENDPOINTS[product] + if (!knownEndpoint) { + throw new Error( + `getLatestContentShaForProduct failed, with unknown product: ${product}. Please add a known endpoint for this product to KNOWN_LATEST_REF_ENDPOINTS.` + ) + } + contentUrl.pathname = knownEndpoint + const latestSha = await fetch(contentUrl.toString()) + .then((resp) => resp.json()) + .then((json) => json.result.sha) + return latestSha +} + +module.exports = getLatestContentShaForProduct diff --git a/build-libs/redirects.js b/build-libs/redirects.js index d39addf36f..8158cbff3e 100644 --- a/build-libs/redirects.js +++ b/build-libs/redirects.js @@ -10,6 +10,7 @@ const path = require('path') const { isDeployPreview } = require('../src/lib/env-checks') const fetchGithubFile = require('./fetch-github-file') +const getLatestContentShaForProduct = require('./get-latest-content-sha-for-product') const { getTutorialRedirects } = require('./tutorial-redirects') const { getDocsDotHashiCorpRedirects, @@ -26,21 +27,6 @@ const HOSTNAME_MAP = { 'test-st.hashi-mktg.com': 'sentinel', } -/** - * Fetch the latest ref from the content API to ensure the redirects are accurate. - * - * @TODO save the redirects to the content database and expose them directly via the API - */ -async function getLatestContentRefForProduct(product) { - const contentUrl = new URL('https://content.hashicorp.com') - contentUrl.pathname = `/api/content/${product}/version-metadata/latest` - const latestRef = await fetch(contentUrl.toString()) - .then((resp) => resp.json()) - .then((json) => json.result.ref) - - return latestRef -} - /** * Load redirects from a content repository. * @@ -63,13 +49,10 @@ async function getLatestContentRefForProduct(product) { * redirects and return early with an empty array. * * @param {string} repoName The name of the repo, owner is always `hashicorp`. - * @param {string?} redirectsPath Optional, custom path to the redirects file. + * @param {string} redirectsPath Path within the repo to the redirects file. * @returns {Promise} */ -async function getRedirectsFromContentRepo( - repoName, - redirectsPath = 'website/redirects.js' -) { +async function getRedirectsFromContentRepo(repoName, redirectsPath) { /** * Note: These constants are declared for clarity in build context intent. */ @@ -82,12 +65,12 @@ async function getRedirectsFromContentRepo( let redirectsFileString if (isDeveloperBuild) { // For `hashicorp/dev-portal` builds, load redirects remotely - const latestContentRef = await getLatestContentRefForProduct(repoName) + const latestContentSha = await getLatestContentShaForProduct(repoName) redirectsFileString = await fetchGithubFile({ owner: 'hashicorp', repo: repoName, path: redirectsPath, - ref: latestContentRef, + ref: latestContentSha, }) } else if (isLocalContentBuild) { // Load redirects from the filesystem, so that authors can see their changes @@ -106,6 +89,24 @@ async function getRedirectsFromContentRepo( return validRedirects } +/** + * @type {{ repo: string, path: string}[]} An array of redirect + * entries. Each entry specifies a repo and the path within that repo to the + * redirects file. + */ +const PRODUCT_REDIRECT_ENTRIES = [ + { repo: 'boundary', path: 'website/redirects.js' }, + { repo: 'nomad', path: 'website/redirects.js' }, + { repo: 'vault', path: 'website/redirects.js' }, + { repo: 'vagrant', path: 'website/redirects.js' }, + { repo: 'packer', path: 'website/redirects.js' }, + { repo: 'consul', path: 'website/redirects.js' }, + { repo: 'terraform-docs-common', path: 'website/redirects.js' }, + { repo: 'hcp-docs', path: '/redirects.js' }, + { repo: 'ptfe-releases', path: 'website/redirects.js' }, + { repo: 'sentinel', path: 'website/redirects.js' }, +] + async function buildProductRedirects() { // Fetch author-oriented redirects from product repos, // and merge those with dev-oriented redirects from @@ -115,18 +116,11 @@ async function buildProductRedirects() { } const productRedirects = ( - await Promise.all([ - getRedirectsFromContentRepo('boundary'), - getRedirectsFromContentRepo('nomad'), - getRedirectsFromContentRepo('vault'), - getRedirectsFromContentRepo('vagrant'), - getRedirectsFromContentRepo('packer'), - getRedirectsFromContentRepo('consul'), - getRedirectsFromContentRepo('terraform-docs-common'), - getRedirectsFromContentRepo('hcp-docs', '/redirects.js'), - getRedirectsFromContentRepo('ptfe-releases'), - getRedirectsFromContentRepo('sentinel'), - ]) + await Promise.all( + PRODUCT_REDIRECT_ENTRIES.map((entry) => + getRedirectsFromContentRepo(entry.repo, entry.path) + ) + ) ).flat() return productRedirects @@ -407,6 +401,7 @@ async function redirectsConfig() { } module.exports = { + PRODUCT_REDIRECT_ENTRIES, redirectsConfig, splitRedirectsByType, groupSimpleRedirects, From fa0e92e17f62a2d78b886a2f7ae4d1c0c8c1794c Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:58:24 -0400 Subject: [PATCH 3/3] add HCP Vagrant Box Registry API docs (#2572) --- .../vagrant-box-registry/[[...page]].tsx | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/pages/hcp/api-docs/vagrant-box-registry/[[...page]].tsx diff --git a/src/pages/hcp/api-docs/vagrant-box-registry/[[...page]].tsx b/src/pages/hcp/api-docs/vagrant-box-registry/[[...page]].tsx new file mode 100644 index 0000000000..49ccadbec4 --- /dev/null +++ b/src/pages/hcp/api-docs/vagrant-box-registry/[[...page]].tsx @@ -0,0 +1,118 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +// Lib +import { fetchCloudApiVersionData } from 'lib/api-docs/fetch-cloud-api-version-data' +// View +import OpenApiDocsView from 'views/open-api-docs-view' +import { + getStaticPaths, + getStaticProps as getOpenApiDocsStaticProps, +} from 'views/open-api-docs-view/server' +// Types +import type { GetStaticProps, GetStaticPropsContext } from 'next' +import type { OpenAPIV3 } from 'openapi-types' +import type { + OpenApiDocsParams, + OpenApiDocsViewProps, + OpenApiDocsPageConfig, +} from 'views/open-api-docs-view/types' +import { + schemaModShortenHcp, + schemaModComponent, + shortenProtobufAnyDescription, +} from 'views/open-api-docs-view/utils/massage-schema-utils' + +/** + * OpenApiDocsView server-side page configuration + */ +const PAGE_CONFIG: OpenApiDocsPageConfig = { + productSlug: 'hcp', + serviceProductSlug: 'vagrant', + basePath: '/hcp/api-docs/vagrant-box-registry', + githubSourceDirectory: { + owner: 'hashicorp', + repo: 'hcp-specs', + path: 'specs/cloud-vagrant-box-registry', + ref: 'main', + }, + groupOperationsByPath: true, + statusIndicatorConfig: { + pageUrl: 'https://status.hashicorp.com', + endpointUrl: + 'https://status.hashicorp.com/api/v2/components/1mdm36t0fkx1.json', + }, + navResourceItems: [ + { + title: 'Tutorial Library', + href: '/tutorials/library?product=vagrant', + }, + { + title: 'Community', + href: 'https://discuss.hashicorp.com/', + }, + { + title: 'Support', + href: 'https://www.hashicorp.com/customer-success', + }, + ], + + /** + * Massage the schema data a little bit + */ + massageSchemaForClient: (schemaData: OpenAPIV3.Document) => { + // Replace "HashiCorp Cloud Platform" with "HCP" in the title + const withShortTitle = schemaModShortenHcp(schemaData) + /** + * Shorten the description of the protobufAny schema + * + * Note: ideally this would be done at the content source, + * but until we've got that work done, this shortening + * seems necessary to ensure incremental static regeneration works + * for past versions of the API docs. Without this shortening, + * it seems the response size ends up crossing a threshold that + * causes the serverless function that renders the page to fail. + * + * Related task: + * https://app.asana.com/0/1207339219333499/1207339701271604/f + */ + const withShortProtobufDocs = schemaModComponent( + withShortTitle, + 'google.protobuf.Any', + shortenProtobufAnyDescription + ) + // Return the schema data with modifications + return withShortProtobufDocs + }, +} + +/** + * Get static paths, using `versionData` fetched from GitHub. + */ +export { getStaticPaths } + +/** + * Get static props, using `versionData` fetched from GitHub. + * + * We need all version data for the version selector, + * and of course we need specific data for the current version. + */ +export const getStaticProps: GetStaticProps< + OpenApiDocsViewProps, + OpenApiDocsParams +> = async ({ params }: GetStaticPropsContext) => { + // Fetch all version data, based on remote `stable` & `preview` subfolders + const versionData = await fetchCloudApiVersionData( + PAGE_CONFIG.githubSourceDirectory + ) + // Generate static props based on page configuration, params, and versionData + return await getOpenApiDocsStaticProps({ + ...PAGE_CONFIG, + context: { params }, + versionData, + }) +} + +export default OpenApiDocsView