Module SDK
SDK to easy compile your hooks as a binary and integrate with addon operator
Usage
Module-SDK version compatibility with Deckhouse
| Module-SDK version |
Deckhouse version |
| v0.2.X |
< v1.71.0 |
| v0.3.X |
>=v1.71.0 |
Module-SDK features compatibility with Deckhouse
| Module-SDK version |
Deckhouse version |
| Readiness Probe |
>= v1.71.0 |
| Applications |
Coming soon |
| Applications Settings check |
Coming soon |
| Modules v2 |
Coming soon |
| Modules v2 Settings check |
Coming soon |
| Modules Settings check |
Coming soon |
One file example
This file must be in 'hooks/' folder to build binary (see examples for correct layout)
package main
import (
"context"
"log/slog"
"github.com/deckhouse/module-sdk/pkg"
"github.com/deckhouse/module-sdk/pkg/app"
objectpatch "github.com/deckhouse/module-sdk/pkg/object-patch"
"github.com/deckhouse/module-sdk/pkg/registry"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var _ = registry.RegisterFunc(config, handlerHook)
var config = &pkg.HookConfig{
Kubernetes: []pkg.KubernetesConfig{
{
Name: "apiservers",
APIVersion: "v1",
Kind: "Pod",
NamespaceSelector: &pkg.NamespaceSelector{
NameSelector: &pkg.NameSelector{
MatchNames: []string{"kube-system"},
},
},
LabelSelector: &v1.LabelSelector{
MatchLabels: map[string]string{"component": "kube-apiserver"},
},
JqFilter: ".metadata.name",
},
},
}
func handlerHook(_ context.Context, input *pkg.HookInput) error {
podNames, err := objectpatch.UnmarshalToStruct[string](input.Snapshots, "apiservers")
if err != nil {
return err
}
input.Logger.Info("found apiserver pods", slog.Any("podNames", podNames))
input.Values.Set("test.internal.apiServers", podNames)
return nil
}
func main() {
app.Run()
}
More examples you can find here.
Reusable building blocks
| Area |
What you get |
Read more |
| Common hooks |
Battery-included hooks for TLS, custom certificates, storage-class changes, external auth, CRD installation. |
common-hooks/ |
| Testing — unit tests |
InputBuilder, StaticSnapshots, RecordingPatchCollector, JQRunOn* and friends. |
testing/helpers/ |
| Testing — functional tests |
Deckhouse-style harness with a fake K8s cluster, snapshot generator, and patch replayer. |
testing/framework/ |
| Testing — strategy |
Picking the right test layer, project-wide conventions. |
TESTING.md |
Adding Readiness Probes
Readiness probes let your module report its ready status to the Deckhouse addon-operator.
How to Add a Readiness Probe
-
Create a Readiness Function
Create a function that checks if your module is ready. This function should return nil if everything is okay or an error if the module is not ready.
func checkReadiness(ctx context.Context, input *pkg.HookInput) error {
// Perform your ready checks here
// Return nil if ready, or error if not ready
return nil
}
-
Register Your Readiness Probe
Register your readiness function when initializing your application:
func main() {
readinessConfig := &app.ReadinessConfig{
// you can override it with environment variable READINESS_INTERVAL_IN_SECONDS
IntervalInSeconds: 12,
ProbeFunc: checkReadiness,
}
app.Run(app.WithReadiness(readinessConfig))
}
-
Example: HTTP Endpoint Check
Here's a complete example that checks if an HTTP endpoint is available:
package main
import (
"context"
"fmt"
"net/http"
"github.com/deckhouse/module-sdk/pkg"
"github.com/deckhouse/module-sdk/pkg/app"
"github.com/deckhouse/module-sdk/pkg/registry"
)
var _ = registry.RegisterFunc(config, handlerHook)
// Your regular hook config and handler here
func checkAPIEndpoint(ctx context.Context, input *pkg.HookInput) error {
client := input.DC.GetHTTPClient()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://api.example.com/readyz", nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("API endpoint unreachable: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("API endpoint returned non-OK status: %d", resp.StatusCode)
}
return nil
}
func main() {
readinessConfig := &app.ReadinessConfig{
ProbeFunc: checkAPIEndpoint,
}
app.Run(app.WithReadiness(readinessConfig))
}
Behavior
- When your readiness function succeeds (returns
nil), the module status changes to Ready
- When it fails (returns an error), the status changes to
Reconciling with the error message
- The module resource's
IsReady condition is updated to reflect the current state
- This lets other components in Deckhouse know when your module is operational
Configuration Options
You can configure the readiness probe using environment variables:
| Variable |
Description |
Default |
READINESS_INTERVAL_IN_SECONDS |
How often to check readiness (in seconds) |
15 |
MODULE_NAME |
Module name used in readiness reporting |
default-module |
Adding Settings Validation
Settings validation allows you to validate module configuration values before they are applied, helping prevent misconfigurations.
How to Add Settings Validation
-
Create a Validation Function
Create a function that validates your module's settings. This function receives the settings and returns a validation result.
func validateSettings(_ context.Context, input settingscheck.Input) settingscheck.Result {
replicas := input.Settings.Get("replicas").Int()
if replicas == 0 {
return settingscheck.Reject("replicas cannot be 0")
}
if replicas > 3 {
return settingscheck.Reject("replicas cannot be greater than 3")
}
// You can also return warnings
var warnings []string
if replicas == 2 {
warnings = append(warnings, "using 2 replicas is not recommended for high availability")
}
return settingscheck.Allow(warnings...)
}
-
Register Your Validation Function
Register your validation function when initializing your application:
func main() {
app.Run(app.WithSettingsCheck(validateSettings))
}
-
Complete Example
package main
import (
"context"
"github.com/deckhouse/module-sdk/pkg/app"
"github.com/deckhouse/module-sdk/pkg/settingscheck"
)
func validateSettings(_ context.Context, input settingscheck.Input) settingscheck.Result {
replicas := input.Settings.Get("replicas").Int()
if replicas == 0 {
return settingscheck.Reject("replicas cannot be 0")
}
if replicas > 3 {
return settingscheck.Reject("replicas cannot be greater than 3")
}
return settingscheck.Allow()
}
func main() {
app.Run(app.WithSettingsCheck(validateSettings))
}
Behavior
- When validation succeeds (returns
Allow()), the settings are accepted
- When validation fails (returns
Reject()), the settings are rejected with an error message
- You can include warnings in allowed settings using
Allow(warnings...)
- The validation runs before settings are applied to your module
Testing
The SDK ships with a layered testing toolkit that lets you test hooks at three levels of fidelity:
- Unit tests — quick handler-level tests using
testing/helpers: InputBuilder, real values store, RecordingPatchCollector, JQ helpers.
- Functional tests — deckhouse-style end-to-end tests using
testing/framework: a fake Kubernetes cluster, real snapshot generation, replayed patches.
- Mocks — minimock-generated mocks for every
pkg.* interface (testing/mock) when you need precise control over a single collaborator.
Quick hook unit test:
import "github.com/deckhouse/module-sdk/testing/helpers"
func TestMyHook(t *testing.T) {
in := helpers.NewInputBuilder(t).
WithSnapshot("nodes", helpers.SnapshotJSON(`{"name":"n1"}`)).
WithValuesJSON(`{}`).
Build()
require.NoError(t, MyHook(context.Background(), in))
require.Len(t, in.Values.GetPatches(), 1)
}
Quick hook functional test:
import "github.com/deckhouse/module-sdk/testing/framework"
func TestMyHook_Functional(t *testing.T) {
f := framework.HookExecutionConfigInit(t, cfg, MyHook, `{}`, `{}`)
f.KubeStateSet(`apiVersion: v1
kind: Node
metadata: {name: n1}`)
f.RunHook()
require.NoError(t, f.HookError())
require.Len(t, f.Snapshots().Get("nodes"), 1)
}
Pure JQ filter test:
helpers.JQRunOnString(ctx, ".metadata.name", `{"metadata":{"name":"x"}}`, &out)
For the project-wide testing strategy and conventions, see TESTING.md.
For deckhouse developers
Environment variables
| Parameter |
Required |
Default value |
Description |
| BINDING_CONTEXT_PATH |
|
in/binding_context.json |
Path to binding context file |
| VALUES_PATH |
|
in/values_path.json |
Path to values file |
| CONFIG_VALUES_PATH |
|
in/config_values_path.json |
Path to config values file |
| METRICS_PATH |
|
out/metrics.json |
Path to metrics file |
| KUBERNETES_PATCH_PATH |
|
out/kubernetes.json |
Path to kubernetes patch file |
| VALUES_JSON_PATCH_PATH |
|
out/values.json |
Path to values patch file |
| CONFIG_VALUES_JSON_PATCH_PATH |
|
out/config_values.json |
Path to config values patch file |
| HOOK_CONFIG_PATH |
|
out/hook_config.json |
Path to dump hook configurations in file |
| CREATE_FILES |
|
false |
Allow hook to create files by himself (by default, waiting for addon operator to create) |
| MODULE_NAME |
|
default-module |
Name of the module, hooks align |
| READINESS_INTERVAL_IN_SECONDS |
|
15 |
Interval in seconds for module readiness checks (override user values) |
| LOG_LEVEL |
|
FATAL |
Log level (suppressed by default) |
Work sequence
Deckhouse register process
- To register your hooks, add them to import section in main package like in examples
- Compile your binary and deliver to "hooks" folder in Deckhouse
- Addon operator finds it automatically and register all your hooks in binary, corresponding with your HookConfigs
- When addon operator has a reason, it calls hook in your binary
- After executing hook, addon operator process hook output
Calling hook
- Addon operator creates temporary files for input and output data (see ENV for examples)
- Addon operator executes hook with corresponding ID and ENV variables pointed to files
- Hook reads all files and passes incoming data in HookInput
- Hook executes and writes all resulting data from collectors contained in HookInput
- Addon operator reads info from temporary output files
Development Commands
Here are some useful commands from the Makefile to help with development:
| Command |
Description |
make test |
Run all tests |
make lint |
Run linters to check code quality |
make examples |
Test example modules |
make go-module-version |
Get current commit "go get" command |