diff --git a/.prettierignore b/.prettierignore index 571c41c52774..d7dc06ca1977 100644 --- a/.prettierignore +++ b/.prettierignore @@ -37,3 +37,6 @@ content/docs/reference/pkg tsconfig.json package.json typedoc.json + +# Ignore the files we generate during the build and deployment process. +origin-bucket-metadata.json diff --git a/assets/css/bundle.css b/assets/css/bundle.css index 8d8bd8e3bda4..6dbada395efd 100644 --- a/assets/css/bundle.css +++ b/assets/css/bundle.css @@ -3127,6 +3127,26 @@ div.highlight.line-numbers pre.chroma code span.line::before{ } } +#ai-sidebar-host{ + display:none +} + +#ai-sidebar-target{ + position:fixed; + z-index:10; + top:0; + right:0; + bottom:0 +} + +.section- #ai-sidebar-target{ + top:200px +} + +.section-docs #ai-sidebar-target{ + top:108px +} + div.highlight{ display:flex } diff --git a/config/_default/config.yml b/config/_default/config.yml index ad9f0bd00174..3fdeadbf820d 100644 --- a/config/_default/config.yml +++ b/config/_default/config.yml @@ -8,6 +8,7 @@ security: - ASSET_BUNDLE_ID - PULUMI_CONVERT_URL - PULUMI_AI_WS_URL + - PULUMI_COPILOT_URL - GITHUB_TOKEN - ALGOLIA_APP_ID - ALGOLIA_APP_SEARCH_KEY diff --git a/infrastructure/Pulumi.www-production.yaml b/infrastructure/Pulumi.www-production.yaml index 56650a9f0ea9..fea917f9cd1a 100644 --- a/infrastructure/Pulumi.www-production.yaml +++ b/infrastructure/Pulumi.www-production.yaml @@ -1,6 +1,7 @@ config: aws:region: us-west-2 www.pulumi.com:addSecurityHeaders: "true" + www.pulumi.com:copilotUrl: https://app.pulumi.com/ai www.pulumi.com:certificateArn: "arn:aws:acm:us-east-1:388588623842:certificate/9db6a76b-f7ba-465b-ab96-ce1d3b8ae02c" www.pulumi.com:doAIAnswersRewrites: "true" www.pulumi.com:doEdgeRedirects: "true" diff --git a/infrastructure/index.ts b/infrastructure/index.ts index 24d11eb750ff..f2cc504cd6b8 100644 --- a/infrastructure/index.ts +++ b/infrastructure/index.ts @@ -61,6 +61,7 @@ const config = { const aiAppStack = new pulumi.StackReference('pulumi/pulumi-ai-app-infra/prod'); const aiAppDomain = aiAppStack.requireOutput('aiAppDistributionDomain'); +const cloudAiAppDomain = aiAppStack.requireOutput('cloudAiAppDistributionDomain'); // originBucketName is the name of the S3 bucket to use as the CloudFront origin for the // website. This bucket is presumed to exist prior to the Pulumi run; if it doesn't, this @@ -277,33 +278,40 @@ const allViewerExceptHostHeaderId = "b689b0a8-53d0-40ab-baf2-68738e2966ac"; // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html const cachingDisabledId = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"; -const SecurityHeadersPolicy = new aws.cloudfront.ResponseHeadersPolicy('security-headers', { - securityHeadersConfig: { - frameOptions: { - frameOption: config.addSecurityHeaders ? 'DENY' : 'SAMEORIGIN', - override: false, - }, - // These remaining options are derived from: - // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-response-headers-policies.html#managed-response-headers-policies-security - // "SecurityHeadersPolicy" with ID "67f7725c-6f97-4210-82d7-5512b31e9d03" - referrerPolicy: { - referrerPolicy: 'strict-origin-when-cross-origin', - override: false, - }, - contentTypeOptions: { - override: true, - }, - strictTransportSecurity: { - accessControlMaxAgeSec: 31536000, - override: false, - }, - xssProtection: { - protection: true, - modeBlock: true, - override: false, +function newSecurityHeadersPolicy(name: string, frameOption: string) { + return new aws.cloudfront.ResponseHeadersPolicy(name, { + securityHeadersConfig: { + frameOptions: { + frameOption, + override: false, + }, + // These remaining options are derived from: + // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-response-headers-policies.html#managed-response-headers-policies-security + // "SecurityHeadersPolicy" with ID "67f7725c-6f97-4210-82d7-5512b31e9d03" + referrerPolicy: { + referrerPolicy: 'strict-origin-when-cross-origin', + override: false, + }, + contentTypeOptions: { + override: true, + }, + strictTransportSecurity: { + accessControlMaxAgeSec: 31536000, + override: false, + }, + xssProtection: { + protection: true, + modeBlock: true, + override: false, + } } - } -}) + }); +} + +// Most of the site +const SecurityHeadersPolicy = newSecurityHeadersPolicy('security-headers', config.addSecurityHeaders ? 'DENY' : 'SAMEORIGIN'); +// Copilot lives in an iframe +const CopilotSecurityHeadersPolicy = newSecurityHeadersPolicy('copilot-security-headers', 'SAMEORIGIN'); const baseCacheBehavior: aws.types.input.cloudfront.DistributionDefaultCacheBehavior = { targetOriginId: originBucket.arn, @@ -421,6 +429,18 @@ const distributionArgs: aws.cloudfront.DistributionArgs = { originKeepaliveTimeout: 60, }, }, + { + originId: cloudAiAppDomain, + domainName: cloudAiAppDomain, + customOriginConfig: { + originProtocolPolicy: "https-only", + httpPort: 80, + httpsPort: 443, + originSslProtocols: ["TLSv1.2"], + originReadTimeout: 60, + originKeepaliveTimeout: 60, + }, + }, ...registryOrigins, ], @@ -580,6 +600,38 @@ const distributionArgs: aws.cloudfront.DistributionArgs = { cachePolicyId: cachingDisabledId, lambdaFunctionAssociations: config.doAIAnswersRewrites ? [getAIAnswersRewriteAssociation()] : [], forwardedValues: undefined, // forwardedValues conflicts with cachePolicyId, so we unset it. + }, + + // Copilot app + { + ...baseCacheBehavior, + // allow all methods + allowedMethods: ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"], + cachedMethods: [ + "GET", "HEAD", "OPTIONS", + ], + targetOriginId: cloudAiAppDomain, + pathPattern: '/_pulumi/cloud-ai', + originRequestPolicyId: allViewerExceptHostHeaderId, + cachePolicyId: cachingDisabledId, + lambdaFunctionAssociations: [], + forwardedValues: undefined, // forwardedValues conflicts with cachePolicyId, so we unset it. + responseHeadersPolicyId: CopilotSecurityHeadersPolicy.id, + }, + { + ...baseCacheBehavior, + // allow all methods + allowedMethods: ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"], + cachedMethods: [ + "GET", "HEAD", "OPTIONS", + ], + targetOriginId: cloudAiAppDomain, + pathPattern: '/_pulumi/cloud-ai/*', + originRequestPolicyId: allViewerExceptHostHeaderId, + cachePolicyId: cachingDisabledId, + lambdaFunctionAssociations: [], + forwardedValues: undefined, // forwardedValues conflicts with cachePolicyId, so we unset it. + responseHeadersPolicyId: CopilotSecurityHeadersPolicy.id, } ], diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index 217812e18991..9a8006390bcd 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -9,6 +9,8 @@
{{ block "main" . }} {{ end }} + + {{ partial "copilot/sidebar.html" . }}
{{ block "footer" . }} diff --git a/layouts/partials/copilot/sidebar.html b/layouts/partials/copilot/sidebar.html new file mode 100644 index 000000000000..2240f996c95d --- /dev/null +++ b/layouts/partials/copilot/sidebar.html @@ -0,0 +1,5 @@ +{{ $copilotApiUrl := getenv "PULUMI_COPILOT_URL" }} +{{ if $copilotApiUrl }} + + +{{ end }} diff --git a/scripts/build-site.sh b/scripts/build-site.sh index c04cc20edb87..c0bc21bfdf06 100755 --- a/scripts/build-site.sh +++ b/scripts/build-site.sh @@ -8,6 +8,13 @@ source ./scripts/common.sh export PULUMI_CONVERT_URL="${PULUMI_CONVERT_URL:-$(pulumi stack output --stack pulumi/tf2pulumi-service/production url)}" export PULUMI_AI_WS_URL=${PULUMI_AI_WS_URL:-$(pulumi stack output --stack pulumi/pulumigpt-api/corp websocketUri)} +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INFRA_PATH="$SCRIPT_DIR/../infrastructure" +# Read Copilot API URL from Pulumi config, ignoring any errors. +# If the config value is not set Copilot will not be available. +export PULUMI_COPILOT_URL=${PULUMI_COPILOT_URL:-$(pulumi --cwd "$INFRA_PATH" config get copilotUrl 2>/dev/null || echo "")} +printf "Copilot URL: $PULUMI_COPILOT_URL\n" + printf "Compiling theme JavaScript and CSS...\n\n" export ASSET_BUNDLE_ID="$(build_identifier)" diff --git a/theme/src/scss/_copilot.scss b/theme/src/scss/_copilot.scss new file mode 100644 index 000000000000..cecf2c43b0eb --- /dev/null +++ b/theme/src/scss/_copilot.scss @@ -0,0 +1,38 @@ +// Host iframe +// Should not be shown, when loaded it will portal the content to the sidebar target +#ai-sidebar-host { + display: none +} + +// Element that will be the target of the portal +// If empty it will have a width of 0 and will not be visible. +#ai-sidebar-target { + // Relative to the viewport so copilot moves w/ the scrolling + position: fixed; + + // Popovers on site use z-index: 20 + z-index: 10; + + // Default use up the full height of the viewport + top: 0; + right: 0; + bottom: 0; +} + +// Per page positions +// ------------------ + +// Main page +// --- + +/* FIXME: no section suffix? */ +.section- #ai-sidebar-target { + top: 200px; +} + +// Docs pages +// --- + +.section-docs #ai-sidebar-target { + top: calc(38px + 8px + 8px + 54px); +} \ No newline at end of file diff --git a/theme/src/scss/main.scss b/theme/src/scss/main.scss index bd88e2ea6339..708c25b08bd3 100644 --- a/theme/src/scss/main.scss +++ b/theme/src/scss/main.scss @@ -21,6 +21,7 @@ @import "marketing/dismissable-banner"; @import "code"; @import "container"; +@import "copilot"; @import "copy-button"; @import "docs/cloud-overview"; @import "docs/continuous-delivery";