Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/operator-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ jobs:
- name: Check for changes
id: git-check
run: |
git diff --exit-code deploy/charts/operator-crds/crds || echo "crd-changes=true" >> $GITHUB_OUTPUT
git diff --exit-code deploy/charts/operator-crds/templates || echo "crd-changes=true" >> $GITHUB_OUTPUT
git diff --exit-code deploy/charts/operator/templates || echo "operator-changes=true" >> $GITHUB_OUTPUT

- name: Fail if CRDs are not up to date
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ cmd/thv-operator/.task/checksum/crdref-gen

# Test coverage
coverage*

crd-helm-wrapper
21 changes: 17 additions & 4 deletions cmd/thv-operator/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,28 @@ tasks:

operator-manifests:
desc: Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects
vars:
PROJECT_ROOT:
sh: git rev-parse --show-toplevel || pwd
CONTROLLER_GEN_PATHS:
sh: |
if [[ "$PWD" == *"/cmd/thv-operator"* ]]; then
echo "./..."
else
echo "./cmd/thv-operator/..."
fi
cmds:
- cmd: mkdir -p bin
- cmd: mkdir -p {{.PROJECT_ROOT}}/cmd/thv-operator/bin
platforms: [linux, darwin]
- cmd: cmd.exe /c mkdir bin
- cmd: cmd.exe /c mkdir {{.PROJECT_ROOT}}/cmd/thv-operator/bin
platforms: [windows]
ignore_error: true # Windows has no mkdir -p, so just ignore error if it exists
- go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.17.3
- $(go env GOPATH)/bin/controller-gen crd webhook paths="./cmd/thv-operator/..." output:crd:artifacts:config=deploy/charts/operator-crds/crds
- $(go env GOPATH)/bin/controller-gen rbac:roleName=toolhive-operator-manager-role paths="./cmd/thv-operator/..." output:rbac:artifacts:config=deploy/charts/operator/templates/clusterrole
- $(go env GOPATH)/bin/controller-gen rbac:roleName=toolhive-operator-manager-role paths="{{.CONTROLLER_GEN_PATHS}}" output:rbac:artifacts:config={{.PROJECT_ROOT}}/deploy/charts/operator/templates/clusterrole
- $(go env GOPATH)/bin/controller-gen crd webhook paths="{{.CONTROLLER_GEN_PATHS}}" output:crd:artifacts:config={{.PROJECT_ROOT}}/deploy/charts/operator-crds/files/crds
# Wrap CRDs with Helm templates for conditional installation
- go run {{.PROJECT_ROOT}}/deploy/charts/operator-crds/crd-helm-wrapper/main.go -source {{.PROJECT_ROOT}}/deploy/charts/operator-crds/files/crds -target {{.PROJECT_ROOT}}/deploy/charts/operator-crds/templates
# - "{{.PROJECT_ROOT}}/deploy/charts/operator-crds/scripts/wrap-crds.sh"

operator-test:
desc: Run tests for the operator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ var _ = BeforeSuite(func() {

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "files", "crds")},
ErrorIfCRDPathMissing: true,
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/thv-operator/test-integration/mcp-group/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ var _ = BeforeSuite(func() {

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "files", "crds")},
ErrorIfCRDPathMissing: true,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ var _ = BeforeSuite(func() {
testEnv = &envtest.Environment{
UseExistingCluster: &useExistingCluster,
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds"),
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "files", "crds"),
},
ErrorIfCRDPathMissing: true,
BinaryAssetsDirectory: kubebuilderAssets,
Expand Down
2 changes: 1 addition & 1 deletion cmd/thv-operator/test-integration/mcp-server/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ var _ = BeforeSuite(func() {

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "files", "crds")},
ErrorIfCRDPathMissing: true,
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/thv-operator/test-integration/virtualmcp/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ var _ = BeforeSuite(func() {

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "files", "crds")},
ErrorIfCRDPathMissing: true,
}

Expand Down
6 changes: 6 additions & 0 deletions deploy/charts/operator-crds/.helmignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@
.idea/
*.tmproj
.vscode/
# Source CRD files and wrapper tool (only wrapped templates are needed)
files/
crd-helm-wrapper/
# Documentation
CLAUDE.md
CONTRIBUTING.md
2 changes: 1 addition & 1 deletion deploy/charts/operator-crds/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ apiVersion: v2
name: toolhive-operator-crds
description: A Helm chart for installing the ToolHive Operator CRDs into Kubernetes.
type: application
version: 0.0.79
version: 0.0.80
appVersion: "0.0.1"
21 changes: 20 additions & 1 deletion deploy/charts/operator-crds/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ToolHive Operator CRDs Helm Chart

![Version: 0.0.79](https://img.shields.io/badge/Version-0.0.79-informational?style=flat-square)
![Version: 0.0.80](https://img.shields.io/badge/Version-0.0.80-informational?style=flat-square)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)

A Helm chart for installing the ToolHive Operator CRDs into Kubernetes.
Expand Down Expand Up @@ -40,3 +40,22 @@ To uninstall/delete the `toolhive-operator-crds` deployment:
helm uninstall <release_name>
```

## Why CRDs in templates/?

Helm does not upgrade CRDs placed in the `crds/` directory during `helm upgrade` operations. This is a [known Helm limitation](https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#some-caveats-and-explanations) to prevent accidental data loss. As a result, users running `helm upgrade` would silently have stale CRDs.

To ensure CRDs are upgraded alongside the chart, this chart places CRDs in `templates/` with Helm conditionals. This follows the pattern used by several popular projects.

However, placing CRDs in `templates/` means they would be deleted when the Helm release is uninstalled, which could result in data loss. To prevent this, CRDs are annotated with `helm.sh/resource-policy: keep` by default (controlled by `crds.keep`). This ensures CRDs persist even after uninstalling the chart.

## Values

| Key | Type | Default | Description |
|-----|-------------|------|---------|
| crds | object | `{"install":{"registry":true,"server":true,"virtualMcp":true},"keep":true}` | CRD installation configuration |
| crds.install | object | `{"registry":true,"server":true,"virtualMcp":true}` | Feature flags for CRD groups |
| crds.install.registry | bool | `true` | Install Registry CRDs (mcpregistries) |
| crds.install.server | bool | `true` | Install Server CRDs (mcpservers, mcpremoteproxies, mcptoolconfigs, mcpgroups) |
| crds.install.virtualMcp | bool | `true` | Install VirtualMCP CRDs (virtualmcpservers, virtualmcpcompositetooldefinitions) |
| crds.keep | bool | `true` | Whether to add the "helm.sh/resource-policy: keep" annotation to CRDs When true, CRDs will not be deleted when the Helm release is uninstalled |

8 changes: 8 additions & 0 deletions deploy/charts/operator-crds/README.md.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ To uninstall/delete the `toolhive-operator-crds` deployment:
helm uninstall <release_name>
```

## Why CRDs in templates/?

Helm does not upgrade CRDs placed in the `crds/` directory during `helm upgrade` operations. This is a [known Helm limitation](https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#some-caveats-and-explanations) to prevent accidental data loss. As a result, users running `helm upgrade` would silently have stale CRDs.

To ensure CRDs are upgraded alongside the chart, this chart places CRDs in `templates/` with Helm conditionals. This follows the pattern used by several popular projects.

However, placing CRDs in `templates/` means they would be deleted when the Helm release is uninstalled, which could result in data loss. To prevent this, CRDs are annotated with `helm.sh/resource-policy: keep` by default (controlled by `crds.keep`). This ensures CRDs persist even after uninstalling the chart.

{{ template "chart.requirementsSection" . }}

{{ template "chart.valuesSection" . }}
Expand Down
123 changes: 123 additions & 0 deletions deploy/charts/operator-crds/crd-helm-wrapper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# CRD Helm Wrapper

A Go tool that wraps Kubernetes CRD YAML files with Helm template conditionals for:
- **Feature flags** (`crds.install.server`, `crds.install.registry`, `crds.install.virtualMcp`)
- **Resource policy annotations** (`crds.keep` → `helm.sh/resource-policy: keep`)

## Why This Tool?

Helm does not upgrade CRDs during `helm upgrade` operations. CRDs placed in the `crds/` directory are only processed during initial installation. By placing CRDs in `templates/` with conditionals, they are upgraded alongside the chart.

Raw CRDs generated by `controller-gen` don't have Helm templating. This tool wraps them with conditionals so users can:
- Enable/disable specific CRD groups via feature flags
- Prevent Helm from deleting CRDs on uninstall

## Usage

```bash
# Run the tool
go run main.go \
-source ../files/crds \
-target ../templates \
-verbose
```

## Flags

| Flag | Description | Required |
|------|-------------|----------|
| `-source` | Source directory containing raw CRD YAML files | Yes |
| `-target` | Target directory for wrapped Helm templates | Yes |
| `-verbose` | Enable verbose output | No |

## Feature Flag Groups

CRDs are grouped by feature flags. Some CRDs belong to multiple groups:

| Flag | CRDs |
|------|------|
| `crds.install.server` | mcpservers, mcpremoteproxies, mcptoolconfigs, mcpgroups |
| `crds.install.registry` | mcpregistries |
| `crds.install.virtualMcp` | virtualmcpservers, virtualmcpcompositetooldefinitions |
| `crds.install.server` OR `crds.install.virtualMcp` | mcpexternalauthconfigs (shared) |

## Output Format

For single-flag CRDs:

```yaml
{{- if .Values.crds.install.server }}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
{{- if .Values.crds.keep }}
helm.sh/resource-policy: keep
{{- end }}
controller-gen.kubebuilder.io/version: v0.17.3
name: mcpservers.toolhive.stacklok.dev
spec:
...
{{- end }}
```

For shared CRDs (multiple groups):

```yaml
{{- if or .Values.crds.install.server .Values.crds.install.virtualMcp }}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
{{- if .Values.crds.keep }}
helm.sh/resource-policy: keep
{{- end }}
controller-gen.kubebuilder.io/version: v0.17.3
name: mcpexternalauthconfigs.toolhive.stacklok.dev
spec:
...
{{- end }}
```

## Values Configuration

The wrapped CRDs expect these values:

```yaml
crds:
# Whether to add the "helm.sh/resource-policy: keep" annotation
keep: true
# Feature flags for CRD groups
install:
server: true
registry: true
virtualMcp: true
```

## Template Escaping

CRD descriptions may contain Go template-like syntax (e.g., `{{.steps.step_id.output}}`). The tool automatically escapes these to prevent Helm from interpreting them as template directives.

## Template Files

The tool uses embedded template files in the `templates/` directory:

| File | Purpose |
|------|---------|
| `header.tpl` | Opening conditional with `__FEATURE_CONDITION__` placeholder |
| `footer.tpl` | Closing `{{- end }}` |
| `keep-annotation.tpl` | Conditional `helm.sh/resource-policy: keep` annotation |

## Adding New CRD Groups

To add a new feature flag group, update the `crdFeatureFlags` map in `main.go`:

```go
var crdFeatureFlags = map[string][]string{
"newcrdtype": {"newFeature"},
// For shared CRDs:
"sharedcrd": {"server", "newFeature"},
}
```

Then add the corresponding value to `values.yaml`.
Loading
Loading