Skip to content

Commit 22fc0ed

Browse files
Add new extension guides (#1958)
* Add new extension guides * Update extension create section order * Iván feedback * Remove "Remove example code" section * Apply to next * Rename extension template guide
1 parent 8c7d379 commit 22fc0ed

10 files changed

+950
-6
lines changed
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
---
2+
title: 'Create an extension with a template'
3+
menuTitle: 'Create an extension with a template'
4+
description: 'Learn how to to create a k6 extension that handles ascii85 encoding using the xk6-example GitHub repository and GitHub Codespaces, along with best practices.'
5+
weight: 200
6+
---
7+
8+
# Create an extension with a template
9+
10+
This guide explains a step-by-step process for creating a k6 extension using the GitHub k6 extension template repository.
11+
12+
In this guide, you’ll learn how to:
13+
14+
- Create a GitHub repository using the k6 extension template repository.
15+
- Create a TypeScript declaration file to document your API.
16+
- Create [ascii85](https://en.wikipedia.org/wiki/Ascii85) encoding and decoding implementation.
17+
- Build a k6 binary with the extension.
18+
- Use the custom k6 binary to run a test.
19+
- Best practices for creating tests, checking for security vulnerabilities, and static analysis for your extension.
20+
21+
For this guide, you’ll implement two functions that handle ascii85 encoding, which is a feature that’s not natively supported by k6. This will be implemented using Go.
22+
23+
## Before you begin
24+
25+
To follow along, you’ll need:
26+
27+
- A [GitHub account](https://docs.github.com/en/get-started/start-your-journey/creating-an-account-on-github).
28+
29+
Having a GitHub account simplifies the process of developing k6 extensions, which the guide will cover. [GitHub Codespaces](https://github.com/features/codespaces) provides a streamlined development experience for k6 extensions, reducing the need for local setup.
30+
31+
## Create a GitHub repository
32+
33+
The first step is to create a GitHub repository using the [grafana/xk6-example](https://github.com/grafana/xk6-example) template repository. This can be done interactively in a browser by clicking [here](https://github.com/new?template_name=xk6-example&template_owner=grafana). Name the repository "xk6-example-ascii85", and set the visibility to **Public**.
34+
35+
Alternatively, use the [GitHub CLI](https://cli.github.com/) to create the repository.
36+
37+
```bash
38+
gh repo create -p grafana/xk6-example -d "Experimental k6 extension" --public xk6-example-ascii85
39+
```
40+
41+
## Create a codespace
42+
43+
GitHub Codespaces is a GitHub feature that lets you create and use a fully configured development environment in the cloud.
44+
45+
To create a GitHub codespace:
46+
47+
- Go to the xk6-example-ascii85 repository you created in the previous step.
48+
- On the repository page, click the green **Code** button and then select **Codespaces** from the dropdown menu.
49+
- Click **Create new codespace**.
50+
51+
Once the codespace is ready, it will open in your browser as a Visual Studio Code-like environment, letting you begin working on your project with the repository code already checked out.
52+
53+
Alternatively, use the [GitHub CLI](https://cli.github.com/) to create the codespace, replacing `USER` with your GitHub username:
54+
55+
```bash
56+
gh codespace create --repo USER/xk6-example-ascii85 --web
57+
```
58+
59+
## API declaration
60+
61+
This step is optional but recommended. It is a good practice to document the API of the k6 extension before implementing it.
62+
63+
Create a TypeScript declaration file named `index.d.ts` and add the following code:
64+
65+
```typescript
66+
/**
67+
* **Example ascii85 encoding for k6**
68+
*
69+
* @module example_ascii85
70+
*/
71+
72+
export as namespace example_ascii85;
73+
74+
/**
75+
* ascii85encode returns the ASCII85 encoding of src.
76+
*
77+
* @param src The input to encode.
78+
*/
79+
export declare function encode(src: ArrayBuffer): string;
80+
81+
/**
82+
* ascii85decode returns the decoded bytes represented by the string str.
83+
*
84+
* @param str The string to decode.
85+
*/
86+
export declare function decode(str: string): ArrayBuffer;
87+
```
88+
89+
## Add encoding and decoding functions
90+
91+
The `encode()` function's implementation is straightforward, as the k6 runtime handles all type conversions. The Go standard `ascii85` package provides the ASCII85 encoding implementation, requiring only a parameter for its use.
92+
93+
Add the following function to the `module.go` file. The `ascii85` package import will be added automatically by the IDE.
94+
95+
```go
96+
func (*module) encode(data []byte) string {
97+
dst := make([]byte, ascii85.MaxEncodedLen(len(data)))
98+
n := ascii85.Encode(dst, data)
99+
100+
return string(dst[:n])
101+
}
102+
```
103+
104+
The `decode()` function should return an `ArrayBuffer`, which requires type conversion by the JavaScript runtime. The `sobek.ArrayBuffer` go struct corresponds to the JavaScript `ArrayBuffer`, so an instance of it must be returned. Refer to the [sobek.Runtime#ExportTo()](https://pkg.go.dev/github.com/grafana/sobek#Runtime.ExportTo) documentation for mapping details.
105+
106+
Add the following function to the `module.go` file:
107+
108+
```go
109+
func (m *module) decode(str string) (sobek.ArrayBuffer, error) {
110+
dst := make([]byte, len(str))
111+
112+
n, _, err := ascii85.Decode(dst, []byte(str), true)
113+
if err != nil {
114+
return sobek.ArrayBuffer{}, err
115+
}
116+
117+
return m.vu.Runtime().NewArrayBuffer(dst[:n]), nil
118+
}
119+
```
120+
121+
To make the `encode()` and `decode()` functions usable within the JavaScript runtime, you have to export them. Add them to the exported symbols in the `module.go` file.
122+
123+
```go
124+
func (m *module) Exports() modules.Exports {
125+
return modules.Exports{
126+
Named: map[string]any{
127+
"encode": m.encode,
128+
"decode": m.decode,
129+
},
130+
}
131+
}
132+
```
133+
134+
The Go implementation of the extension is complete.
135+
136+
## Build a custom k6 binary
137+
138+
To use the `xk6-example-ascii85` extension, a custom k6 build must be created using the `xk6 build` subcommand.
139+
140+
```bash
141+
xk6 build --with github.com/USER/xk6-example-ascii85=.
142+
```
143+
144+
Replace `USER` with your GitHub username.
145+
146+
This command creates a custom k6 executable in the current folder.
147+
148+
## Run a test with the custom k6 binary
149+
150+
To showcase the extension's functionality, create a JavaScript file named `script.js` and add the following code to it:
151+
152+
```js
153+
import { encode } from 'k6/x/example_ascii85';
154+
155+
export default function () {
156+
console.log(encode(new Uint8Array([72, 101, 108, 108, 111, 33]).buffer)); // 87cURD]o
157+
}
158+
```
159+
160+
And then run the script using the custom k6 binary:
161+
162+
```bash
163+
./k6 run script.js
164+
```
165+
166+
The script outputs `87cURD]o` to the console. This string is the ascii85 encoded representation of `Hello!`.
167+
168+
## Best practices
169+
170+
### Create a smoke test
171+
172+
For initial verification before writing comprehensive integration tests, you can create a basic smoke test in `test/smoke.test.js`.
173+
174+
```js
175+
import { encode, decode } from 'k6/x/example_ascii85';
176+
import { check } from 'k6';
177+
178+
export const options = {
179+
thresholds: {
180+
checks: ['rate==1'],
181+
},
182+
};
183+
184+
export default function () {
185+
const bytes = new Uint8Array([72, 101, 108, 108, 111, 33]).buffer;
186+
187+
check(encode(bytes), {
188+
encoded: (str) => str == '87cURD]o',
189+
reverse: (str) => equal(bytes, decode(str)),
190+
});
191+
}
192+
193+
const equal = (a, b) => new Uint8Array(a).toString() === new Uint8Array(b).toString();
194+
```
195+
196+
This test ensures the correctness of ascii85 encoding and decoding. It uses a fixed `Hello!` string as a test case for both encoding and decoding processes.
197+
198+
### Create Go module tests
199+
200+
Go tests offer the quickest method for verifying extension implementations. Standard unit testing practices apply. For a module-level integration test example, refer to the module_test.go file. This setup facilitates comprehensive integration testing between the Go implementation and the JavaScript runtime.
201+
202+
```go
203+
package example_ascii85
204+
205+
import (
206+
_ "embed"
207+
"testing"
208+
209+
"github.com/stretchr/testify/require"
210+
"go.k6.io/k6/js/modulestest"
211+
)
212+
213+
func Test_module(t *testing.T) { //nolint:tparallel
214+
t.Parallel()
215+
216+
runtime := modulestest.NewRuntime(t)
217+
err := runtime.SetupModuleSystem(map[string]any{importPath: new(rootModule)}, nil, nil)
218+
require.NoError(t, err)
219+
220+
_, err = runtime.RunOnEventLoop(`let mod = require("` + importPath + `")`)
221+
require.NoError(t, err)
222+
223+
tests := []struct {
224+
name string
225+
check string
226+
}{
227+
// Add your test cases here
228+
// Example: {name: "myFunc()", check: `mod.myFunc() == expectedValue`},
229+
{
230+
name: "encode()",
231+
check: `mod.encode(new Uint8Array([72, 101, 108, 108, 111, 33]).buffer) == "87cURD]o"`,
232+
},
233+
}
234+
for _, tt := range tests { //nolint:paralleltest
235+
t.Run(tt.name, func(t *testing.T) {
236+
got, err := runtime.RunOnEventLoop(tt.check)
237+
238+
require.NoError(t, err)
239+
require.True(t, got.ToBoolean())
240+
})
241+
}
242+
}
243+
```
244+
245+
The provided test code creates an extension instance and integrates it into the JavaScript runtime, accessible as `mod`. The JavaScript code defining the test is then executed within the JavaScript runtime's event loop.
246+
247+
### Generate API documentation
248+
249+
You can generate HTML API documentation from the `index.d.ts` API declaration file using [TypeDoc](https://typedoc.org/). To do this, run the following command that creates the extension API documentation from the `index.d.ts` file and saves it in the `build/docs` directory.
250+
251+
```bash
252+
bun x typedoc --out build/docs
253+
```
254+
255+
### Security and vulnerability
256+
257+
Ensure the Go source code of your k6 extension is checked for security vulnerabilities using the [gosec](https://github.com/securego/gosec) tool. Like any Go project, security scanning is crucial for your extension's codebase.
258+
259+
```bash
260+
gosec -quiet ./...
261+
```
262+
263+
Generally, extensions rely on external Go module dependencies. It is advisable to use the [govulncheck](https://github.com/golang/vuln) tool to identify potential vulnerabilities within these dependencies.
264+
265+
```bash
266+
govulncheck ./...
267+
```
268+
269+
Security and vulnerability checks are a requirement for registering the extension in the [k6 Extension Registry](https://registry.k6.io).
270+
271+
### Static analysis
272+
273+
Analyzing the Go source code of your k6 extension statically can proactively identify subtle bugs. [golangci-lint](https://golangci-lint.run/) is a popular static code analysis tool that even can be used without configuration.
274+
275+
```bash
276+
golangci-lint run ./...
277+
```
278+
279+
## Reference
280+
281+
The complete Go source code (`module.go`) for the extension implementation is provided for reference.
282+
283+
```go
284+
// Package example_ascii85 contains the xk6-example-ascii85 extension.
285+
package example_ascii85
286+
287+
import (
288+
"encoding/ascii85"
289+
290+
"github.com/grafana/sobek"
291+
"go.k6.io/k6/js/modules"
292+
)
293+
294+
type rootModule struct{}
295+
296+
func (*rootModule) NewModuleInstance(vu modules.VU) modules.Instance {
297+
return &module{vu}
298+
}
299+
300+
type module struct {
301+
vu modules.VU
302+
}
303+
304+
func (m *module) Exports() modules.Exports {
305+
return modules.Exports{
306+
Named: map[string]any{
307+
"encode": m.encode,
308+
"decode": m.decode,
309+
},
310+
}
311+
}
312+
313+
func (*module) encode(data []byte) string {
314+
dst := make([]byte, ascii85.MaxEncodedLen(len(data)))
315+
n := ascii85.Encode(dst, data)
316+
317+
return string(dst[:n])
318+
}
319+
320+
func (m *module) decode(str string) (sobek.ArrayBuffer, error) {
321+
dst := make([]byte, len(str))
322+
323+
n, _, err := ascii85.Decode(dst, []byte(str), true)
324+
if err != nil {
325+
return sobek.ArrayBuffer{}, err
326+
}
327+
328+
return m.vu.Runtime().NewArrayBuffer(dst[:n]), nil
329+
}
330+
331+
var _ modules.Module = (*rootModule)(nil)
332+
```
333+
334+
In addition, `register.go` contains the registration of the extension with the k6 runtime.
335+
336+
```go
337+
package example_ascii85
338+
339+
import "go.k6.io/k6/js/modules"
340+
341+
const importPath = "k6/x/example_ascii85"
342+
343+
func init() {
344+
modules.Register(importPath, new(rootModule))
345+
}
346+
```
347+
348+
### Reference to the JavaScript runtime
349+
350+
In the k6 runtime, each VU (data structure representing a virtual user) has a dedicated JavaScript runtime instance, which can be accessed with the `Runtime()` function.
351+
352+
```go
353+
m.vu.Runtime()
354+
```
355+
356+
## Additional resources
357+
358+
- [k6 go API documentation](https://pkg.go.dev/go.k6.io/k6)
359+
- [k6 JavaScript engine documentation](https://pkg.go.dev/github.com/grafana/sobek)
360+
- [xk6 - k6 extension development toolbox](https://github.com/grafana/xk6)

docs/sources/k6/next/extensions/create/javascript-extensions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: 'JavaScript Extensions'
33
description: 'Follow these steps to build a JS extension for k6.'
4-
weight: 01
4+
weight: 300
55
---
66

77
# JavaScript Extensions

docs/sources/k6/next/extensions/create/output-extensions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: 'Output Extensions'
33
description: 'Follow these steps to build an output extension for k6.'
4-
weight: 02
4+
weight: 400
55
---
66

77
# Output Extensions

0 commit comments

Comments
 (0)