Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit b403164

Browse files
committed
feat: when inlining guidebook snippets, rewrite relative image links
Also updates the iter8 tutorial1.md to pull the content from iter8 Also adds support for renaming wizard steps in wizard topmatter
1 parent 13758b1 commit b403164

File tree

7 files changed

+91
-144
lines changed

7 files changed

+91
-144
lines changed

plugins/plugin-client-common/src/components/Content/Markdown/KuiFrontmatter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface WizardSteps {
2525
* heading in the markdown source. Optionally, a step description
2626
* may be overlaid
2727
*/
28-
steps: (string | { name: string; description: string })[]
28+
steps: (string | { match?: string; name: string; description: string })[]
2929
}
3030
}
3131

plugins/plugin-client-common/src/components/Content/Markdown/components/code/remark-codeblocks-topmatter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ function isCode(node: Node): node is Code {
2828
/** Scan and process the `codeblocks` schema of the given `frontmatter` */
2929
export default function preprocessCodeBlocks(tree /*: Root */, frontmatter: KuiFrontmatter) {
3030
if (hasCodeBlocks(frontmatter)) {
31-
const codeblocks = frontmatter.codeblocks.map(_ => Object.assign({}, _, { match: new RegExp(_.match) }))
31+
const codeblocks = frontmatter.codeblocks.map(_ =>
32+
Object.assign({}, _, { match: new RegExp(_.match.replace(/\./g, '\\.')) })
33+
)
3234

3335
visit(tree, 'code', node => {
3436
if (isCode(node)) {

plugins/plugin-client-common/src/components/Content/Markdown/frontmatter.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export function tryFrontmatter(
4040
}
4141
}
4242

43+
/** In case you only want the body part of a `markdownText` */
44+
export function stripFrontmatter(markdownText: string) {
45+
return tryFrontmatter(markdownText).body
46+
}
47+
4348
export function splitTarget(node) {
4449
if (node.type === 'raw') {
4550
const match = node.value.match(/<!-- ____KUI__SECTION_START____ (.*)/)
@@ -88,6 +93,15 @@ function preprocessWizardSteps(tree: Root, frontmatter: KuiFrontmatter) {
8893
frontmatter.layout = 'wizard'
8994
}
9095

96+
// since the user defined wizard steps in the topmatter, we need
97+
// to remove existing thematicBreaks, for now
98+
visitParents(tree, 'thematicBreak', (node, ancestors) => {
99+
const parent = ancestors[ancestors.length - 1]
100+
const childIdx = parent.children.findIndex(_ => _ === node)
101+
102+
parent.children.splice(childIdx, 1)
103+
})
104+
91105
let nth = 0
92106
visitParents<Heading>(tree, 'heading', (node, ancestors) => {
93107
if (ancestors.length > 0 && node.children && node.children[0]) {
@@ -98,7 +112,11 @@ function preprocessWizardSteps(tree: Root, frontmatter: KuiFrontmatter) {
98112
const childIdx = parent.children.findIndex(_ => _ === node)
99113

100114
const matchingStep = frontmatter.wizard.steps.find(step =>
101-
typeof step === 'string' ? step === firstChild.value : step.name === firstChild.value
115+
typeof step === 'string'
116+
? step === firstChild.value
117+
: step.match
118+
? new RegExp(step.match.replace(/\./g, '\\.')).test(firstChild.value)
119+
: step.name === firstChild.value
102120
)
103121

104122
const headingIdx = nth++
@@ -107,12 +125,14 @@ function preprocessWizardSteps(tree: Root, frontmatter: KuiFrontmatter) {
107125
if (parent.children[childIdx - 1].type !== 'thematicBreak') {
108126
// splice in a ---, which below we use to indicate steps
109127
parent.children.splice(childIdx, 0, u('thematicBreak'))
128+
}
110129

111-
// splice in a description? below, this will be parsed
112-
// out as Title: Description
113-
if (typeof matchingStep !== 'string' && matchingStep.description) {
114-
firstChild.value = firstChild.value + ': ' + matchingStep.description
115-
}
130+
// splice in a description? below, this will be parsed
131+
// out as Title: Description
132+
if (typeof matchingStep !== 'string') {
133+
firstChild.value =
134+
(matchingStep.name || firstChild.value) +
135+
(matchingStep.description ? ': ' + matchingStep.description : '')
116136
}
117137
}
118138
} else if (childIdx >= 0 && headingIdx === 0 && frontmatter.wizard.description) {

plugins/plugin-client-common/src/controller/snippets.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
*/
1616

1717
import Debug from 'debug'
18-
import { isAbsolute as pathIsAbsolute, join as pathJoin } from 'path'
18+
import { isAbsolute as pathIsAbsolute, dirname as pathDirname, join as pathJoin } from 'path'
1919
import { Arguments } from '@kui-shell/core'
2020
import { loadNotebook } from '@kui-shell/plugin-client-common/notebook'
2121

22+
import { stripFrontmatter } from '../components/Content/Markdown/frontmatter'
23+
2224
const debug = Debug('plugin-client-common/markdown/snippets')
2325

2426
const RE_SNIPPET = /^--(-*)8<--(-*)\s+"([^"]+)"(\s+"([^"]+)")?$/
@@ -27,6 +29,16 @@ function isUrl(a: string) {
2729
return /^https?:/.test(a)
2830
}
2931

32+
function dirname(a: string) {
33+
if (isUrl(a)) {
34+
const url = new URL(a)
35+
url.pathname = pathDirname(url.pathname)
36+
return url.toString()
37+
} else {
38+
return pathDirname(a)
39+
}
40+
}
41+
3042
function join(a: string, b: string) {
3143
if (isUrl(a)) {
3244
const url = new URL(a)
@@ -45,6 +57,14 @@ function toString(data: string | object) {
4557
return typeof data === 'string' ? data : JSON.stringify(data)
4658
}
4759

60+
/** Rewrite any relative image links to use the given basePath */
61+
function rerouteImageLinks(basePath: string, data: string) {
62+
return data.replace(
63+
/\[(.+)\]\((.+)\)/g, // e.g. [linky](https://linky.com)
64+
(_, p1, p2) => `[${p1}](${join(basePath, p2)})`
65+
)
66+
}
67+
4868
/**
4969
* Simplistic approximation of
5070
* https://facelessuser.github.io/pymdown-extensions/extensions/snippets/.
@@ -65,6 +85,13 @@ export default function inlineSnippets(snippetBasePath?: string) {
6585
return isAbsolute(basePath) ? basePath : srcFilePath ? join(srcFilePath, basePath) : undefined
6686
}
6787

88+
// Call ourselves recursively, in case a fetched snippet
89+
// fetches other files. We also may need to reroute relative
90+
// image links according to the given `basePath`.
91+
const recurse = (basePath: string, data: string) => {
92+
return inlineSnippets(basePath)(rerouteImageLinks(basePath, toString(data)), snippetFileName, args)
93+
}
94+
6895
const candidates = match[5]
6996
? [match[5]]
7097
: snippetBasePath
@@ -73,7 +100,7 @@ export default function inlineSnippets(snippetBasePath?: string) {
73100

74101
const snippetData = isUrl(snippetFileName)
75102
? await loadNotebook(snippetFileName, args)
76-
.then(data => inlineSnippets(snippetBasePath)(toString(data), snippetFileName, args))
103+
.then(data => recurse(snippetBasePath || dirname(snippetFileName), toString(data)))
77104
.catch(err => {
78105
debug('Warning: could not fetch inlined content', snippetFileName, err)
79106
return ''
@@ -85,7 +112,7 @@ export default function inlineSnippets(snippetBasePath?: string) {
85112
.filter(Boolean)
86113
.map(mySnippetBasePath =>
87114
loadNotebook(join(mySnippetBasePath, snippetFileName), args)
88-
.then(data => inlineSnippets(mySnippetBasePath)(toString(data), snippetFileName, args))
115+
.then(data => recurse(mySnippetBasePath, toString(data)))
89116
.catch(err => {
90117
debug('Warning: could not fetch inlined content', mySnippetBasePath, err)
91118
return ''
@@ -98,7 +125,10 @@ export default function inlineSnippets(snippetBasePath?: string) {
98125
return line
99126
} else {
100127
debug('successfully fetched inlined content')
101-
return toString(snippetData)
128+
129+
// for now, we completely strip off the topmatter from
130+
// snippets. TODO?
131+
return stripFrontmatter(toString(snippetData))
102132
}
103133
}
104134
})

plugins/plugin-client-common/src/test/core/wizards-in-markdown.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const IN3 = {
6262
description: 'WizardDescriptionInTopmatter',
6363
expectedSplitCount: 1,
6464
steps: [
65-
{ name: 'Before you begin', body: 'Before you can get started', description: '', codeBlocks: [] },
65+
{ name: 'TestRewritingOfStepName', body: 'Before you can get started', description: '', codeBlocks: [] },
6666
{ name: 'Prepare local Kubernetes cluster', body: 'You can use', description: 'TestDescription2', codeBlocks: [] }
6767
]
6868
}

plugins/plugin-client-common/tests/data/wizard-steps-in-topmatter.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
wizard:
33
description: WizardDescriptionInTopmatter
44
steps:
5-
- Before you begin
5+
- match: Before you begin
6+
name: TestRewritingOfStepName
67
- name: Prepare local Kubernetes cluster
78
description: TestDescription2
89
- Install the Kubernetes CLI
Lines changed: 24 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
---
22
title: Iter8 &mdash; Getting Started
3-
layout: wizard
3+
wizard:
4+
steps:
5+
- Introduction
6+
- match: 1. Install Iter8
7+
name: Install the CLI
8+
description: The iter8 CLI gives you an easy way to manage your experiments
9+
- match: 2. Download experiment chart
10+
name: Download experiment
11+
description: You may craft an experiment by hand, or, as we do here, you may use iter8 to download a previously constructed experiment definition
12+
- match: 3. Run experiment
13+
name: Run experiment
14+
description: Run load against the application, and monitor error rate and response time
15+
- match: 4. Assert outcomes
16+
name: Assert outcomes
17+
codeblocks:
18+
- match: brew install iter8
19+
validate: iter8 -v
20+
- match: go install
21+
validate: ${GOPATH-~/go}/bin go -v
22+
- match: iter8 hub -e load-test
23+
validate: "[[ -f /tmp/load-test/values.yaml ]] && [[ -f /tmp/load-test/Chart.yaml ]] || exit 1"
24+
- match: iter8 run
25+
validate: "[[ -f /tmp/load-test/experiment/values.yaml ]] || exit 1"
426
---
527

628
# Iter8: Kubernetes Release Engineering
@@ -13,8 +35,6 @@ release velocity and business value with their apps/ML models while
1335
protecting end-user experience. Use Iter8 for SLO validation, A/B
1436
testing and progressive rollouts of K8s apps/ML models.
1537

16-
---
17-
1838
## Introduction
1939

2040
This tutorial uses an [Iter8 experiment](concepts.md#what-is-an-iter8-experiment) to load test https://example.com and validate latency and error-related service level objectives (SLOs).
@@ -23,130 +43,4 @@ This tutorial uses an [Iter8 experiment](concepts.md#what-is-an-iter8-experiment
2343
>
2444
> An Iter8 experiment is a sequence of tasks that produce metrics-driven insights for your app/ML model versions, validates them, and optionally performs a rollout. Iter8 provides a set of pre-defined and customizable tasks.
2545
26-
---
27-
28-
## Install the CLI: The iter8 CLI gives you an easy way to manage your experiments
29-
30-
=== "Mac"
31-
32-
On macOS, [Homebrew](https://brew.sh) makes it easy to install the `iter8` CLI.
33-
34-
```shell
35-
---
36-
id: install-iter8-cli
37-
validate: brew info iter8
38-
---
39-
brew tap iter8-tools/iter8
40-
brew install iter8
41-
```
42-
43-
=== "Go 1.16+"
44-
Install Iter8 using [Go 1.16+](https://golang.org/) as follows.
45-
46-
```shell
47-
---
48-
id: install-iter8-cli
49-
before: export PATH=~/${GOPATH-~/go}/bin:$PATH
50-
validate: iter8 -v
51-
---
52-
go install github.com/iter8-tools/iter8@latest
53-
```
54-
55-
=== "Binaries"
56-
Pre-compiled Iter8 binaries for many platforms are available [here](https://github.com/iter8-tools/iter8/releases). Uncompress the iter8-X-Y.tar.gz archive for your platform, and move the iter8 binary to any folder in your PATH.
57-
58-
---
59-
60-
## Download experiment: You may craft an experiment by hand, or, as we do here, you may use iter8 to download a previously constructed experiment definition
61-
62-
Download the `load-test` experiment folder from [Iter8 hub](../user-guide/topics/iter8hub.md) as follows.
63-
64-
```shell
65-
---
66-
id: download-load-test
67-
validate: "[[ -f /tmp/load-test/values.yaml ]] && [[ -f /tmp/load-test/Chart.yaml ]] || exit 1"
68-
status: done
69-
---
70-
cd /tmp && iter8 hub -e load-test
71-
```
72-
73-
---
74-
75-
## Run experiment: Run load against the application, and monitor error rate and response time
76-
77-
[Iter8 experiments](concepts.md#what-is-an-iter8-experiment) are specified using the `experiment.yaml` file. The `iter8 run` command reads this file, runs the specified experiment, and writes the results of the experiment into the `result.yaml` file.
78-
79-
Run the experiment you downloaded above as follows.
80-
81-
```shell
82-
---
83-
id: run-experiment
84-
validate: "[[ -f /tmp/load-test/experiment/values.yaml ]] || exit 1"
85-
---
86-
cd /tmp/load-test && iter8 run --set url=https://example.com
87-
```
88-
89-
??? note "Look inside experiment.yaml"
90-
91-
This experiment contains the [`gen-load-and-collect-metrics` task](../user-guide/tasks/collect.md) for generating load and collecting metrics, and the [`assess-app-versions` task](../user-guide/tasks/assess.md) for validating SLOs.
92-
93-
```yaml
94-
# task 1: generate HTTP requests for https://example.com and
95-
# collect Iter8's built-in latency and error-related metrics
96-
- task: gen-load-and-collect-metrics
97-
with:
98-
versionInfo:
99-
- url: https://example.com
100-
# task 2: validate if the app (hosted at https://example.com) satisfies
101-
# service level objectives (SLOs)
102-
# this task uses the built-in metrics collected by task 1 for validation
103-
- task: assess-app-versions
104-
with:
105-
SLOs:
106-
# error rate must be 0
107-
- metric: built-in/error-rate
108-
upperLimit: 0
109-
# 95th percentile latency must be under 100 msec
110-
- metric: built-in/p95.0
111-
upperLimit: 100
112-
```
113-
114-
??? note "Iter8 and Helm"
115-
116-
If you are familiar with Helm, you probably noticed that the load-test folder resembles a Helm chart. This is because, Iter8 experiment charts are Helm charts under the covers. The iter8 run command used above combines the experiment chart with values to generate the experiments.yaml file, much like how Helm charts can be combined with values to produce Kubernetes manifests.
117-
118-
## Assert outcomes
119-
120-
Assert that the experiment completed without any failures and SLOs are satisfied
121-
122-
```shell
123-
---
124-
id: assert-success
125-
---
126-
cd /tmp/load-test && iter8 assert -c completed -c nofailure -c slos
127-
```
128-
129-
## Generate report
130-
131-
Generate a report of the experiment in HTML or text formats as follows.
132-
133-
=== "HTML"
134-
135-
```shell
136-
iter8 report -o html > report.html
137-
# open report.html with a browser. In MacOS, you can use the command:
138-
# open report.html
139-
```
140-
141-
???+ note "The HTML report looks as follows"
142-
143-
![HTML report](https://iter8.tools/0.8/getting-started/images/report.html.png)
144-
145-
=== "Text"
146-
147-
```shell
148-
---
149-
id: generate-text-report
150-
---
151-
cd /tmp/load-test && iter8 report -o text
152-
```
46+
--8<-- "https://raw.githubusercontent.com/iter8-tools/iter8/master/mkdocs/docs/getting-started/your-first-experiment.md"

0 commit comments

Comments
 (0)