Skip to content

Commit

Permalink
Merge branch 'main' into zs.hcp-waypoint-api-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
zchsh committed Sep 13, 2024
2 parents 5404ec2 + fa0e92e commit 44dc368
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 48 deletions.
33 changes: 33 additions & 0 deletions build-libs/__tests__/get-latest-content-sha-for-product.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
}
})
})
53 changes: 53 additions & 0 deletions build-libs/get-latest-content-sha-for-product.js
Original file line number Diff line number Diff line change
@@ -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<string>}
*/
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
63 changes: 29 additions & 34 deletions build-libs/redirects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
*
Expand All @@ -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<Redirect[]>}
*/
async function getRedirectsFromContentRepo(
repoName,
redirectsPath = 'website/redirects.js'
) {
async function getRedirectsFromContentRepo(repoName, redirectsPath) {
/**
* Note: These constants are declared for clarity in build context intent.
*/
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -407,6 +401,7 @@ async function redirectsConfig() {
}

module.exports = {
PRODUCT_REDIRECT_ENTRIES,
redirectsConfig,
splitRedirectsByType,
groupSimpleRedirects,
Expand Down
118 changes: 118 additions & 0 deletions src/pages/hcp/api-docs/vagrant-box-registry/[[...page]].tsx
Original file line number Diff line number Diff line change
@@ -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<OpenApiDocsParams>) => {
// 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
17 changes: 16 additions & 1 deletion src/views/docs-view/loaders/remote-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<product>.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}`
}
Expand Down
14 changes: 1 addition & 13 deletions src/views/docs-view/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<product>.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

Expand Down

0 comments on commit 44dc368

Please sign in to comment.