Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,17 @@ generate-modules-ci: generate-modules
exit 1; \
fi

.PHONY: generate-readme-usage
generate-readme-usage: ## Update the README usage block from the main binary
ifeq ($(BUILD_IN_CONTAINER),true)
docker run --rm \
-v $(shell go env GOPATH)/pkg:/go/pkg$(MOUNT_FLAGS) \
-v $(shell pwd):/src/cluster-api-provider-$(INFRA_PROVIDER)$(MOUNT_FLAGS) \
$(BUILDER_IMAGE):$(BUILDER_IMAGE_VERSION) $@;
else
./hack/update-readme-usage.sh
endif

generate-manifests: ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
ifeq ($(BUILD_IN_CONTAINER),true)
docker run --rm \
Expand Down Expand Up @@ -753,7 +764,7 @@ else
endif

.PHONY: generate
generate: generate-manifests generate-go-deepcopy generate-boilerplate generate-modules generate-mocks ## Generate Files
generate: generate-manifests generate-go-deepcopy generate-boilerplate generate-modules generate-mocks generate-readme-usage ## Generate Files

ALL_VERIFY_CHECKS = boilerplate shellcheck starlark manifests capi-version
.PHONY: verify
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- [What is CAPH](#-what-is-the-cluster-api-provider-hetzner)
- [Documentation](#-documentation)
- [Getting Started](#-getting-started)
- [CLI Usage](#-cli-usage)
- [Version Compatibility](#%EF%B8%8F-compatibility-with-cluster-api-and-kubernetes-versions)
- [Node Images](#-operating-system-images)
- [Contributing](#-getting-involved-and-contributing)
Expand Down Expand Up @@ -67,6 +68,41 @@ Additional resources from the documentation:

In addition to the pure creation and operation of Kubernetes clusters, this provider can also validate and approve certificate signing requests. This increases security as the kubelets of the nodes can be operated with signed certificates, and enables the metrics-server to run securely. [Click here](https://syself.com/docs/caph/topics/advanced/csr-controller) to read more about the CSR controller.

## 🧭 CLI Usage

The main binary prints its usage text before trying to connect to a Kubernetes cluster. The block below is updated from the binary itself.

<!-- BEGIN MAIN BINARY USAGE -->
```console
$ cluster-api-provider-hetzner --help
Usage:
cluster-api-provider-hetzner [flags]

Flags:
--baremetal-image-url-command string Command to run (in rescue-system) to provision an baremetal machine. Docs: https://syself.com/docs/caph/developers/image-url-command
--baremetal-ssh-after-install-image Connect to the baremetal machine after install-image and ensure it is provisioned. Current default is true, but we might change that to false. Background: Users might not want the controller to be able to ssh onto the servers (default true)
--debug-hcloud-api-calls Debug all calls to the hcloud API.
--disable-csr-approval Disables builtin workload cluster CSR validation and approval.
--hcloudmachine-concurrency int Number of HcloudMachines to process simultaneously (default 1)
--health-probe-bind-address string The address the probe endpoint binds to. (default ":9440")
--hetznerbaremetalhost-concurrency int Number of HetznerBareMetalHosts to process simultaneously (default 1)
--hetznerbaremetalmachine-concurrency int Number of HetznerBareMetalMachines to process simultaneously (default 1)
--hetznercluster-concurrency int Number of HetznerClusters to process simultaneously (default 1)
--kubeconfig string Paths to a kubeconfig. Only required if out-of-cluster.
--leader-elect Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. (default true)
--leader-elect-namespace string Namespace that the controller performs leader election in. If unspecified, the controller will discover which namespace it is running in.
--log-level string Specifies log level. Options are 'debug', 'info' and 'error' (default "info")
--metrics-bind-address string The address the metric endpoint binds to. (default "localhost:8080")
--namespace string Namespace that the controller watches to reconcile cluster-api objects. If unspecified, the controller watches for cluster-api objects across all namespaces.
--pre-provision-command string Command to run (in rescue-system) before installing the image on bare metal servers. You can use that to check if the machine is healthy before installing the image. If the exit value is non-zero, the machine is considered unhealthy. This command must be accessible by the controller pod. You can use an initContainer to copy the command to a shared emptyDir.
--rate-limit duration The rate limiting for HCloud controller (e.g. 5m) (default 5m0s)
--skip-crd-migration-phases strings List of CRD migration phases to skip. Valid values are: StorageVersionMigration, CleanupManagedFields.
--skip-webhooks Skip setting up webhooks. Together with --leader-elect=false, this lets you run CAPH in a cluster connected via KUBECONFIG. You should scale down the deployed CAPH controller to 0 before doing that. This is only for testing.
--sync-period duration The minimum interval at which watched resources are reconciled (e.g. 3m) (default 3m0s)
--watch-filter string Label value that the controller watches to reconcile cluster-api objects. Label key is always cluster.x-k8s.io/watch-filter. If unspecified, the controller watches for all cluster-api objects.
```
<!-- END MAIN BINARY USAGE -->

## 🖇️ Compatibility with Cluster API and Kubernetes Versions

[Compatibility Table](./docs/caph/01-getting-started/01-introduction.md#compatibility-with-cluster-api-and-kubernetes-versions)
Expand Down
95 changes: 95 additions & 0 deletions hack/update-readme-usage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env bash

# Copyright 2026 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better, if we add a brief introduction for the script.

# Regenerate the README CLI usage block from the current binary's --help output.
# Run this after changing CLI flags or help text.

# Bash Strict Mode: https://github.com/guettli/bash-strict-mode
trap 'echo -e "\n🤷 🚨 🔥 Warning: A command has failed. Exiting the script. Line was ($0:$LINENO): $(sed -n "${LINENO}p" "$0" 2>/dev/null || true) 🔥 🚨 🤷 "; exit 3' ERR
set -Eeuo pipefail

readonly REPO_ROOT="$(git rev-parse --show-toplevel)"
readonly README_FILE="$REPO_ROOT/README.md"
readonly START_MARKER="<!-- BEGIN MAIN BINARY USAGE -->"
readonly END_MARKER="<!-- END MAIN BINARY USAGE -->"

cd "$REPO_ROOT"

if ! grep -Fq "$START_MARKER" "$README_FILE"; then
echo "Missing README marker: $START_MARKER"
exit 1
fi

if ! grep -Fq "$END_MARKER" "$README_FILE"; then
echo "Missing README marker: $END_MARKER"
exit 1
fi

tmp_dir="$(mktemp -d)"
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT

binary_path="$tmp_dir/cluster-api-provider-hetzner"
usage_output_file="$tmp_dir/usage.txt"
replacement_file="$tmp_dir/replacement.txt"
updated_readme_file="$tmp_dir/README.md"

go build -o "$binary_path" .
"$binary_path" --help >"$usage_output_file"

{
echo "$START_MARKER"
echo '```console'
echo '$ cluster-api-provider-hetzner --help'
cat "$usage_output_file"
echo '```'
echo "$END_MARKER"
} >"$replacement_file"

awk \
-v start="$START_MARKER" \
-v end="$END_MARKER" \
-v replacement="$replacement_file" \
'
BEGIN {
while ((getline line < replacement) > 0) {
replacement_text = replacement_text line ORS
}
}
$0 == start {
printf "%s", replacement_text
in_block = 1
found_start = 1
next
}
$0 == end {
in_block = 0
found_end = 1
next
}
!in_block {
print
}
END {
if (!found_start || !found_end) {
exit 1
}
}
' "$README_FILE" >"$updated_readme_file"

mv "$updated_readme_file" "$README_FILE"
36 changes: 31 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -66,6 +67,8 @@ var (
commandRegex = regexp.MustCompile(`^[a-z][a-z0-9_.-]+[a-z0-9]$`)
)

const binaryName = "cluster-api-provider-hetzner"

func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
Expand Down Expand Up @@ -109,8 +112,7 @@ func (m *strictManager) GetClient() client.Client {
return m.strictClient
}

func main() {
fs := pflag.CommandLine
func registerFlags(fs *pflag.FlagSet) {
fs.StringVar(&metricsAddr, "metrics-bind-address", "localhost:8080", "The address the metric endpoint binds to.")
fs.StringVar(&probeAddr, "health-probe-bind-address", ":9440", "The address the probe endpoint binds to.")
fs.BoolVar(&enableLeaderElection, "leader-elect", true, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.")
Expand All @@ -127,11 +129,35 @@ func main() {
fs.DurationVar(&rateLimitWaitTime, "rate-limit", 5*time.Minute, "The rate limiting for HCloud controller (e.g. 5m)")
fs.BoolVar(&hcloudclient.DebugAPICalls, "debug-hcloud-api-calls", false, "Debug all calls to the hcloud API.")
fs.StringVar(&preProvisionCommand, "pre-provision-command", "", "Command to run (in rescue-system) before installing the image on bare metal servers. You can use that to check if the machine is healthy before installing the image. If the exit value is non-zero, the machine is considered unhealthy. This command must be accessible by the controller pod. You can use an initContainer to copy the command to a shared emptyDir.")
fs.BoolVar(&skipWebhooks, "skip-webhooks", false, "Skip setting up of webhooks. Together with --leader-elect=false, you can use `go run main.go` to run CAPH in a cluster connected via KUBECONFIG. You should scale down the caph deployment to 0 before doing that. This is only for testing!")
fs.BoolVar(&skipWebhooks, "skip-webhooks", false, "Skip setting up webhooks. Together with --leader-elect=false, this lets you run CAPH in a cluster connected via KUBECONFIG. You should scale down the deployed CAPH controller to 0 before doing that. This is only for testing.")
fs.StringSliceVar(&skipCRDMigrationPhases, "skip-crd-migration-phases", []string{}, "List of CRD migration phases to skip. Valid values are: StorageVersionMigration, CleanupManagedFields.")

pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
fs.AddGoFlagSet(flag.CommandLine)
}

func printUsage(fs *pflag.FlagSet, out io.Writer) {
fs.SetOutput(out)
defer fs.SetOutput(io.Discard)

_, _ = fmt.Fprintf(out, "Usage:\n %s [flags]\n\nFlags:\n", binaryName)
fs.PrintDefaults()
}

func main() {
fs := pflag.NewFlagSet(binaryName, pflag.ContinueOnError)
fs.SetOutput(io.Discard)
registerFlags(fs)

if err := fs.Parse(os.Args[1:]); err != nil {
if errors.Is(err, pflag.ErrHelp) {
printUsage(fs, os.Stdout)
return
}

_, _ = fmt.Fprintf(os.Stderr, "Error: %v\n\n", err)
printUsage(fs, os.Stderr)
os.Exit(2)
}

ctrl.SetLogger(utils.GetDefaultLogger(logLevel))

Expand Down
Loading