Skip to content

Commit 61dd902

Browse files
Mpdreamzcoderabbitai[bot]claude
authored
Add air-gapped docs container (#2978)
* Add air-gapped docs container build Adds two new build commands and a distroless nginx container for serving documentation offline without network access. Made-with: Cursor * Update build/Targets.fs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Add dedicated air-gapped publish environment - Add `air-gapped` environment to assembler.yml with no GTM/Optimizely, allow_indexing: false, and AIR_GAPPED feature flag - Add AirGappedEnabled feature flag to FeatureFlags.cs - Add AirGapped flag to FrontendConfig (disables telemetry, signals frontend) - Assembler header conditionally renders elastic-docs-header instead of elastic-nav.js for air-gapped, avoiding all external CDN dependencies - Load isolated.ts (registers <elastic-docs-header> web component) when airGapped is true; override --offset-top via body.air-gapped CSS class - Skip API health checks in NavigationSearch, FullPageSearch, and useSearchAvailability for air-gapped builds - Hide DeploymentInfo section in header via explicit air-gapped prop - Switch Air_Gapped_Build target to use --environment air-gapped Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5c04bd6 commit 61dd902

16 files changed

Lines changed: 202 additions & 27 deletions

File tree

build/CommandLine.fs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ type Build =
5454
| [<CliPrefix(CliPrefix.None);Hidden;SubCommand>] PublishContainers
5555
| [<CliPrefix(CliPrefix.None);Hidden;SubCommand>] PublishZip
5656

57+
| [<CliPrefix(CliPrefix.None);SubCommand>] Air_Gapped_Build
58+
| [<CliPrefix(CliPrefix.None);SubCommand>] Air_Gapped_Run
59+
5760
| [<CliPrefix(CliPrefix.None);SubCommand>] Release
5861

5962
| [<Inherit;AltCommandLine("-s")>] Single_Target
@@ -73,6 +76,9 @@ with
7376
| Integrate -> "alias to providing: test --test-suite=integration"
7477
| Test -> "runs a clean build and then runs all the tests unless --test-suite is provided"
7578

79+
| Air_Gapped_Build -> "Clones, builds docs with html exporter, and packages into an air-gapped Docker container"
80+
| Air_Gapped_Run -> "Runs the air-gapped docs container on port 8080"
81+
7682
| Release -> "runs build, tests, and create and validates the packages shy of publishing them"
7783
| Publish -> "Publishes artifacts"
7884
| Format _ -> "runs dotnet format"

build/Targets.fs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
module Targets
66

7+
open System.IO
8+
open System.IO.Compression
79
open Argu
810
open CommandLine
911
open Fake.Core
@@ -150,6 +152,73 @@ let private runTests (testSuite: TestSuite) _ =
150152
)
151153
}
152154

155+
let private compressibleExtensions = set [".html"; ".css"; ".js"; ".json"; ".svg"; ".xml"; ".txt"]
156+
157+
let private gzipCompressDirectory (directory: string) =
158+
// Remove stale .gz files from any previous run to avoid orphaned pairs
159+
Directory.EnumerateFiles(directory, "*.gz", SearchOption.AllDirectories)
160+
|> Seq.toArray
161+
|> Array.iter File.Delete
162+
163+
let files =
164+
Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)
165+
|> Seq.filter (fun f ->
166+
match Path.GetExtension(f) with
167+
| null -> false
168+
| ext -> compressibleExtensions.Contains(ext.ToLowerInvariant())
169+
)
170+
|> Seq.toArray
171+
172+
printfn $"Compressing %d{files.Length} files in %s{directory}"
173+
174+
files
175+
|> Array.Parallel.iter (fun file ->
176+
let gzPath = file + ".gz"
177+
use input = File.OpenRead(file)
178+
use output = File.Create(gzPath)
179+
use gzip = new GZipStream(output, CompressionLevel.SmallestSize)
180+
input.CopyTo(gzip)
181+
)
182+
183+
// Replace originals with zero-byte placeholders so nginx try_files can resolve
184+
// paths, while gzip_static always serves the .gz version
185+
for file in files do
186+
File.Delete(file)
187+
File.Create(file).Dispose()
188+
189+
printfn "Compression complete"
190+
191+
let private airGappedBuild _ =
192+
let assemblyDir = Path.Combine(Paths.Root.FullName, ".artifacts", "assembly")
193+
if Directory.Exists(assemblyDir) then
194+
Directory.Delete(assemblyDir, true)
195+
196+
exec { run "dotnet" "run" "--project" "src/tooling/docs-builder" "--" "assembler" "clone" "--environment" "air-gapped" }
197+
exec { run "dotnet" "run" "--project" "src/tooling/docs-builder" "--" "assembler" "build" "--exporters" "html" "--environment" "air-gapped" }
198+
let searchDir = Path.Combine(assemblyDir, "docs", "_search")
199+
if Directory.Exists(searchDir) then
200+
Directory.Delete(searchDir, true)
201+
202+
File.WriteAllText(
203+
Path.Combine(assemblyDir, "index.html"),
204+
"""<meta http-equiv="refresh" content="0;url=/docs">"""
205+
)
206+
207+
gzipCompressDirectory assemblyDir
208+
209+
exec {
210+
run "docker" "build"
211+
"-f" "build/air-gapped/Dockerfile"
212+
"--build-context" "content=.artifacts/assembly"
213+
"--build-context" "config=build/air-gapped"
214+
"-t" "elastic-docs-air-gapped"
215+
"build/air-gapped"
216+
}
217+
218+
let private airGappedRun _ =
219+
printfn "Running at http://localhost:8080"
220+
exec { run "docker" "run" "-p" "127.0.0.1:8080:8080" "elastic-docs-air-gapped" }
221+
153222
let private validateLicenses _ =
154223
let args = ["-u"; "-t"; "-i"; "docs-builder.sln"; "--use-project-assets-json"
155224
"--forbidden-license-types"; "build/forbidden-license-types.json"
@@ -196,6 +265,8 @@ let Setup (parsed:ParseResults<Build>) =
196265
| RunLocalContainer -> Build.Step runLocalContainer
197266
| PublishZip -> Build.Step publishZip
198267
| ValidateLicenses -> Build.Step validateLicenses
268+
| Air_Gapped_Build -> Build.Cmd [] [] airGappedBuild
269+
| Air_Gapped_Run -> Build.Cmd [] [] airGappedRun
199270

200271
// flags
201272
| Single_Target

build/air-gapped/Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# syntax=docker/dockerfile:1
2+
3+
FROM cgr.dev/chainguard/nginx:latest
4+
COPY --from=config nginx.conf /etc/nginx/nginx.conf
5+
COPY --from=content . /usr/share/nginx/html/
6+
EXPOSE 8080

build/air-gapped/nginx.conf

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
worker_processes auto;
2+
error_log /dev/stderr warn;
3+
pid /tmp/nginx.pid;
4+
5+
events {
6+
worker_connections 1024;
7+
}
8+
9+
http {
10+
include /etc/nginx/mime.types;
11+
default_type application/octet-stream;
12+
13+
access_log /dev/stdout combined;
14+
15+
sendfile on;
16+
tcp_nopush on;
17+
keepalive_timeout 65;
18+
19+
gzip_static always;
20+
gunzip on;
21+
22+
server {
23+
listen 8080;
24+
server_name _;
25+
root /usr/share/nginx/html;
26+
27+
add_header X-Content-Type-Options "nosniff" always;
28+
add_header X-Frame-Options "SAMEORIGIN" always;
29+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
30+
31+
index index.html;
32+
33+
location / {
34+
try_files $uri $uri/index.html =404;
35+
}
36+
37+
error_page 404 /404.html;
38+
}
39+
}

config/assembler.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ environments:
4646
enabled: false
4747
feature_flags:
4848
SEARCH_OR_ASK_AI: true
49+
air-gapped:
50+
uri: http://localhost:8080
51+
path_prefix: docs
52+
content_source: current
53+
allow_indexing: false
54+
feature_flags:
55+
AIR_GAPPED: true
4956

5057
shared_configuration:
5158
stack: &stack

src/Elastic.Documentation.Configuration/Builder/FeatureFlags.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ public bool StagingElasticNavEnabled
3838
set => _featureFlags["staging-elastic-nav"] = value;
3939
}
4040

41+
public bool AirGappedEnabled
42+
{
43+
get => IsEnabled("air-gapped");
44+
set => _featureFlags["air-gapped"] = value;
45+
}
46+
4147
public bool DiagnosticsPanelEnabled
4248
{
4349
get => IsEnabled("diagnostics-panel");

src/Elastic.Documentation.Site/Assets/assembler.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@
2828
min-height: var(--offset-top);
2929
}
3030

31+
/* Air-gapped: uses elastic-docs-header web component instead of elastic nav */
32+
body.air-gapped {
33+
--offset-top: 48px;
34+
}
35+
36+
body.air-gapped elastic-docs-header .euiHeader {
37+
padding-inline: calc((100vw - var(--max-layout-width)) / 2 + 8px);
38+
}
39+
3140
#elastic-nav {
3241
@media screen and (min-width: 1200px) {
3342
[data-component='Container'] {

src/Elastic.Documentation.Site/Assets/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface DocsConfig {
1010
telemetryEnabled: boolean
1111
rootPath: string // '/docs' for assembler, '' for codex and isolated
1212
apiBasePath: string // '/docs/_api' for assembler, '/api' for codex
13+
airGapped: boolean
1314
}
1415

1516
declare global {
@@ -24,6 +25,7 @@ const DEFAULT_CONFIG: DocsConfig = {
2425
telemetryEnabled: false,
2526
rootPath: '',
2627
apiBasePath: '/docs/_api',
28+
airGapped: false,
2729
}
2830

2931
export const config: DocsConfig = window.__DOCS_CONFIG__ ?? DEFAULT_CONFIG

src/Elastic.Documentation.Site/Assets/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import('./web-components/AppliesToPopover')
4141
import('./web-components/FullPageSearch/FullPageSearchComponent')
4242
import('./web-components/Diagnostics/DiagnosticsComponent')
4343

44-
if (config.buildType === 'isolated') {
44+
if (config.buildType === 'isolated' || config.airGapped) {
4545
import('./isolated')
4646
} else if (config.buildType === 'codex') {
4747
import('./codex')

src/Elastic.Documentation.Site/Assets/web-components/FullPageSearch/FullPageSearchComponent.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ const FullPageSearchInner = () => {
2323
},
2424
staleTime: 60 * 60 * 1000, // 60 minutes
2525
retry: false,
26+
enabled: !config.airGapped,
2627
})
2728

28-
if (!isApiAvailable) {
29+
if (config.airGapped || !isApiAvailable) {
2930
return null
3031
}
3132

0 commit comments

Comments
 (0)