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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
16 changes: 8 additions & 8 deletions .github/workflows/go-presubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ jobs:
- name: addon-examples-image # will build and tag the examples image
run: imagebuilder --allow-pull -t quay.io/open-cluster-management/addon-examples:latest -t quay.io/ocm/addon-examples:latest -f ./build/Dockerfile.example .
- name: setup kind
uses: engineerd/setup-kind@v0.5.0
uses: engineerd/setup-kind@v0.6.2
with:
version: v0.11.1
version: v0.31.0
name: cluster1
- name: Load image on the nodes of the cluster
run: |
Expand Down Expand Up @@ -136,9 +136,9 @@ jobs:
- name: addon-examples-image # will build and tag the examples image
run: imagebuilder --allow-pull -t quay.io/open-cluster-management/addon-examples:latest -t quay.io/ocm/addon-examples:latest -f ./build/Dockerfile.example .
- name: setup kind
uses: engineerd/setup-kind@v0.5.0
uses: engineerd/setup-kind@v0.6.2
with:
version: v0.11.1
version: v0.31.0
name: cluster1
- name: Load image on the nodes of the cluster
run: |
Expand Down Expand Up @@ -167,9 +167,9 @@ jobs:
- name: addon-examples-image # will build and tag the examples image
run: imagebuilder --allow-pull -t quay.io/open-cluster-management/addon-examples:latest -t quay.io/ocm/addon-examples:latest -f ./build/Dockerfile.example .
- name: setup kind
uses: engineerd/setup-kind@v0.5.0
uses: engineerd/setup-kind@v0.6.2
with:
version: v0.11.1
version: v0.31.0
name: cluster1
- name: Load image on the nodes of the cluster
run: |
Expand Down Expand Up @@ -198,9 +198,9 @@ jobs:
- name: addon-examples-image # will build and tag the examples image
run: imagebuilder --allow-pull -t quay.io/open-cluster-management/addon-examples:latest -t quay.io/ocm/addon-examples:latest -f ./build/Dockerfile.example .
- name: setup kind
uses: engineerd/setup-kind@v0.5.0
uses: engineerd/setup-kind@v0.6.2
with:
version: v0.11.1
version: v0.31.0
name: cluster1
- name: Load image on the nodes of the cluster
run: |
Expand Down
32 changes: 23 additions & 9 deletions .github/workflows/pr-verify.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
name: PR Verifier

on:
# NB: using `pull_request_target` runs this in the context of
# the base repository, so it has permission to upload to the checks API.
# This means changes won't kick in to this file until merged onto the
# main branch.
pull_request_target:
pull_request:
types: [opened, edited, reopened, synchronize]

permissions:
Expand All @@ -20,7 +16,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Verifier action
id: verifier
uses: kubernetes-sigs/kubebuilder-release-tools@v0.4.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
if [[ -z "$TITLE" ]]; then
echo "Error: PR title cannot be empty."
exit 1
fi

if ! [[ "$TITLE" =~ ^(:sparkles:|:bug:|:book:|:memo:|:warning:|:seedling:|:question:|$'\u2728'|$'\U0001F41B'|$'\U0001F4D6'|$'\U0001F4DD'|$'\u26A0'$'\uFE0F'?|$'\U0001F331'|$'\u2753') ]]; then
echo "Error: Invalid PR title format."
echo "Your PR title must start with one of the following indicators:"
echo "- :sparkles: ✨ feature"
echo "- :bug: 🐛 bug fix"
echo "- :book: 📖 docs"
echo "- :memo: 📝 proposal"
echo "- :warning: ⚠️ breaking change"
echo "- :seedling: 🌱 other/misc"
echo "- :question: ❓ requires manual review"
exit 1
fi

echo "PR title is valid: '$TITLE'"
70 changes: 70 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# AGENTS.md

This file provides guidance to coding agents (e.g. Claude Code, claude.ai/code) when working with code in this repository.

## Repository purpose

Go module `open-cluster-management.io/addon-framework` — the upstream OCM (Open Cluster Management) framework for building **addons**: cluster-side components that the OCM hub installs/manages across a fleet of managed clusters. The framework wraps the OCM `ClusterManagementAddOn` / `ManagedClusterAddOn` CRDs with three deployment strategies (Go templates, Helm charts, AddOnTemplate), handles registration/RBAC, and exposes utilities for hosted vs. standard hosting modes. Library + example binaries.

This is a **library repo**: downstream addons import `open-cluster-management.io/addon-framework/pkg/...` to build their own controller. The fork is mirrored to `kluster-management/addon-framework` (the AppsCode mirror); the **upstream is `open-cluster-management-io/addon-framework`** and this repo tracks it.

## Architecture

- `pkg/addonfactory/` — high-level addon builders: `addonfactory.go` is the entry point, with `helm_agentaddon.go`, `template_agentaddon.go`, `addondeploymentconfig.go`, and shared `helper.go` for plumbing values into the chosen rendering engine.
- `pkg/addonmanager/` — the addon manager runtime:
- `manager.go`, `base_manager.go`, `interface.go` — the manager surface.
- `controllers/` — the reconcilers it runs.
- `cloudevents/` — CloudEvents transport (used by `cloudevents` deployment style).
- `constants/`, `addontesting/` — shared constants and test helpers.
- `pkg/agent/` — agent-side helpers used by the deployed cluster-local controllers.
- `pkg/assets/`, `pkg/index/`, `pkg/lease/`, `pkg/utils/`, `pkg/version/` — supporting packages.
- `pkg/cmd/` — CLI helpers.
- `pkg/dependencymagnet/` — Go-import-only package that forces vendoring of side-effect deps (e.g. `kustomize`).
- `examples/` — runnable example addons: `helloworld/`, `helloworld_agent/`, `helloworld_helm/`, `helloworld_hosted/`, plus `deploy/` and `rbac/` manifests. The Makefile's `deploy-helloworld*` / `undeploy-*` targets exercise them.
- `test/` — integration tests; `test/integration-test.mk` provides `envtest-setup` and `test-{kube,cloudevents,v1alpha1-kube}-integration`.
- `build/Dockerfile.example` — the example image.
- `Makefile` — thin wrapper that **vendors in OpenShift's `build-machinery-go/make/*.mk`**, so most "standard" targets (`build`, `test`, `verify`, `update`, `images`) come from `vendor/github.com/openshift/build-machinery-go/`. Cluster-side deploy targets are defined locally.
- `vendor/` — checked-in deps (required: the build machinery and `kustomize` machinery are pulled from there).

The repo's API surface is consumed via `pkg/`; do not break import paths.

## Common commands

The Makefile pulls in OpenShift's `build-machinery-go`, so several targets are inherited from there in addition to the locally-defined ones.

Standard development:

- `make build` (alias `make all`) — Go build (from `golang.mk`).
- `make test` — Go tests (from `golang.mk`).
- `make verify` — runs `verify-gocilint` (the local target on top of the machinery's `verify`).
- `make update` — code/manifest regen (from the machinery's `update` chain).
- `make images` — container image builds (from `images.mk`, using `EXAMPLE_IMAGE = addon-examples` and `build/Dockerfile.example`).
- `make ensure-kustomize` — install the pinned `KUSTOMIZE_VERSION = 4.5.5`.

Integration tests (require kubebuilder/envtest assets — `setup-envtest` is fetched automatically):

- `make test-kube-integration`
- `make test-cloudevents-integration`
- `make test-v1alpha1-kube`

End-to-end deploy/undeploy against a local OCM hub (target a running cluster via `KUBECONFIG`):

- `make deploy-ocm` (standard), `make deploy-hosted-ocm` (hosted), `make deploy-ocm-csr-token`, `make deploy-ocm-grpc-token`, `make deploy-ocm-cloudevents`.
- `make deploy-helloworld` / `…-helm` / `…-hosted` / `…-template` / `…-cloudevents`, `make deploy-busybox`, `make deploy-kubernetes-dashboard`, plus matching `undeploy-*` targets.
- `MANAGED_CLUSTER_NAME` (default `hub`) and the `HOSTED_MANAGED_*` env vars control the test cluster naming.

Run a single Go test:

```
go test ./pkg/addonfactory/... -run TestName -v
```

## Conventions

- Module path is `open-cluster-management.io/addon-framework` (upstream). Do not rename it; imports must use the upstream path.
- This is the **upstream** OCM addon-framework repo (mirrored on GitHub under `kluster-management/addon-framework`). Prefer rebasing onto upstream over diverging; isolate AppsCode-only patches so they replay cleanly.
- License: Apache-2.0 (`LICENSE`, `OWNERS`, `SECURITY.md`, `CONTRIBUTING.md` follow the OCM project conventions).
- Sign off commits (`git commit -s`) — DCO file is checked into the repo.
- Vendor directory is checked in **and load-bearing**: the Makefile `include`s `.mk` files from `vendor/github.com/openshift/build-machinery-go/make/`. Do not break the build machinery vendoring with a careless `go mod vendor`.
- Addon agents follow the `ClusterManagementAddOn` / `ManagedClusterAddOn` API contract from `open-cluster-management.io/api/addon/v1beta1` — when changing the framework's expected manifest shape, coordinate with the upstream OCM API repo.
- New deployment strategies: implement an `AgentAddon` builder under `pkg/addonfactory/`. Follow the existing `helm_agentaddon.go` / `template_agentaddon.go` patterns.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ Open Cluster Management (OCM) is a Kubernetes-native solution for managing multi

The framework is built around several key Kubernetes custom resources:

- **[ClusterManagementAddOn](https://github.com/open-cluster-management-io/api/blob/main/addon/v1alpha1/types_clustermanagementaddon.go)**: Hub cluster resource that defines addon metadata and installation strategy
- **[ManagedClusterAddOn](https://github.com/open-cluster-management-io/api/blob/main/addon/v1alpha1/types_managedclusteraddon.go)**: Managed cluster resource that represents addon installation state
- **[ClusterManagementAddOn](https://github.com/open-cluster-management-io/api/blob/main/addon/v1beta1/types_clustermanagementaddon.go)**: Hub cluster resource that defines addon metadata and installation strategy
- **[ManagedClusterAddOn](https://github.com/open-cluster-management-io/api/blob/main/addon/v1beta1/types_managedclusteraddon.go)**: Managed cluster resource that represents addon installation state
- **AddOnTemplate**: Template-based addon deployment without dedicated controllers

## Getting Started
Expand Down
28 changes: 21 additions & 7 deletions cmd/example/helloworld/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
utilrand "k8s.io/apimachinery/pkg/util/rand"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
utilflag "k8s.io/component-base/cli/flag"
logs "k8s.io/component-base/logs/api/v1"
"k8s.io/klog/v2"
addonv1alpha1client "open-cluster-management.io/api/client/addon/clientset/versioned"
addonclient "open-cluster-management.io/api/client/addon/clientset/versioned"

"open-cluster-management.io/addon-framework/examples/helloworld"
"open-cluster-management.io/addon-framework/examples/helloworld_agent"
Expand All @@ -25,6 +26,7 @@ import (
cmdfactory "open-cluster-management.io/addon-framework/pkg/cmd/factory"
"open-cluster-management.io/addon-framework/pkg/utils"
"open-cluster-management.io/addon-framework/pkg/version"
sdktls "open-cluster-management.io/sdk-go/pkg/tls"
)

func main() {
Expand Down Expand Up @@ -75,17 +77,34 @@ func newControllerCommand() *cobra.Command {
cmd.Use = "controller"
cmd.Short = "Start the addon controller"
o.AddFlags(cmd)
cmd.Flags().StringVar(&c.namespace, "addon-manager-namespace", c.namespace,
"Namespace where the addon manager runs. Used to watch the TLS profile ConfigMap.")

return cmd
}

// addManagerConfig holds cloudevents configuration for addon manager
type addManagerConfig struct {
cloudeventsOptions *cloudevents.CloudEventsOptions
namespace string
}

func (c *addManagerConfig) runController(ctx context.Context, kubeConfig *rest.Config) error {
addonClient, err := addonv1alpha1client.NewForConfig(kubeConfig)
// Watch the ocm-tls-profile ConfigMap in the addon manager namespace on the hub.
// When it changes the manager restarts so the new TLS settings take effect.
if c.namespace != "" {
kubeClient, err := kubernetes.NewForConfig(kubeConfig)
if err != nil {
return err
}
if _, err := sdktls.StartTLSConfigMapWatcher(ctx, kubeClient, c.namespace, func() {
os.Exit(0)
}); err != nil {
klog.Errorf("Failed to start TLS ConfigMap watcher: %v", err)
}
}

addonClient, err := addonclient.NewForConfig(kubeConfig)
if err != nil {
return err
}
Expand All @@ -109,11 +128,6 @@ func (c *addManagerConfig) runController(ctx context.Context, kubeConfig *rest.C
utilrand.String(5),
)

// Set agent install namespace from addon deployment config if it exists
registrationOption.AgentInstallNamespace = utils.AgentInstallNamespaceFromDeploymentConfigFunc(
utils.NewAddOnDeploymentConfigGetter(addonClient),
)

agentAddon, err := addonfactory.NewAgentAddonFactory(helloworld.AddonName, helloworld.FS, "manifests/templates").
WithConfigGVRs(utils.AddOnDeploymentConfigGVR).
WithGetValuesFuncs(
Expand Down
10 changes: 2 additions & 8 deletions cmd/example/helloworld_helm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
utilflag "k8s.io/component-base/cli/flag"
logs "k8s.io/component-base/logs/api/v1"
"k8s.io/klog/v2"
addonv1alpha1client "open-cluster-management.io/api/client/addon/clientset/versioned"
addonclient "open-cluster-management.io/api/client/addon/clientset/versioned"

"open-cluster-management.io/addon-framework/examples/helloworld"
"open-cluster-management.io/addon-framework/examples/helloworld_agent"
Expand Down Expand Up @@ -84,7 +84,7 @@ func runController(ctx context.Context, kubeConfig *rest.Config) error {
return err
}

addonClient, err := addonv1alpha1client.NewForConfig(kubeConfig)
addonClient, err := addonclient.NewForConfig(kubeConfig)
if err != nil {
return err
}
Expand All @@ -99,12 +99,6 @@ func runController(ctx context.Context, kubeConfig *rest.Config) error {
kubeConfig,
helloworld_helm.AddonName,
utilrand.String(5))
// Set agent install namespace from addon deployment config if it exists
// Note: If the agentAddonFactory.WithAgentInstallNamespace is set, we recommend
// setting this to the same value or omitting this.
registrationOption.AgentInstallNamespace = utils.AgentInstallNamespaceFromDeploymentConfigFunc(
utils.NewAddOnDeploymentConfigGetter(addonClient),
)

agentAddon, err := addonfactory.NewAgentAddonFactory(helloworld_helm.AddonName, helloworld_helm.FS, "manifests/charts/helloworld").
WithConfigGVRs(
Expand Down
44 changes: 42 additions & 2 deletions examples/deploy/ocm-token/install-grpc-token.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ joincmd=$(${CLUSTERADM} get token | grep clusteradm)
token=$(echo ${joincmd} | sed -n 's/.*--hub-token \([^ ]*\).*/\1/p')
hubapi=$(echo ${joincmd} | sed -n 's/.*--hub-apiserver \([^ ]*\).*/\1/p')

echo "############ Waiting for 5 pods in open-cluster-management-hub to be running (timeout 2m)..."
TIMEOUT=120
INTERVAL=5
ELAPSED=0
while true; do
RUNNING_COUNT=$(${KUBECTL} get pods -n open-cluster-management-hub --field-selector=status.phase=Running --no-headers 2>/dev/null | wc -l | tr -d ' ')
if [ "$RUNNING_COUNT" -ge 5 ] 2>/dev/null; then
echo "############ All 5 pods in open-cluster-management-hub are running."
break
fi
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "############ Timed out waiting for pods in open-cluster-management-hub to be running. Running: ${RUNNING_COUNT}/5"
${KUBECTL} get pods -n open-cluster-management-hub
exit 1
fi
echo "Waiting for pods... (${RUNNING_COUNT}/5 running, ${ELAPSED}s/${TIMEOUT}s)"
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done

echo "############ Join hub to itself as managed cluster ${MANAGED_CLUSTER_NAME} using gRPC driver and token addon driver"
${CLUSTERADM} join \
--hub-token ${token} \
Expand All @@ -44,8 +64,28 @@ ${CLUSTERADM} join \
--wait \
--bundle-version=latest

echo "############ Cluster auto-approved, checking status..."
${KUBECTL} get managedcluster ${MANAGED_CLUSTER_NAME}
echo "############ Waiting for managed cluster ${MANAGED_CLUSTER_NAME} to become available (timeout 2m)..."
TIMEOUT=120
INTERVAL=5
ELAPSED=0
while true; do
STATUS=$(${KUBECTL} get managedcluster ${MANAGED_CLUSTER_NAME} -o jsonpath='{range .status.conditions[*]}{.type}{" "}{.status}{"\n"}{end}' 2>/dev/null | grep "ManagedClusterConditionAvailable" | awk '{print $2}')
if [ "$STATUS" = "True" ]; then
echo "############ Managed cluster ${MANAGED_CLUSTER_NAME} is available."
break
fi
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "############ Timed out waiting for managed cluster ${MANAGED_CLUSTER_NAME} to become available."
${KUBECTL} version
${KUBECTL} get managedcluster -o yaml
${KUBECTL} get pods -A
${KUBECTL} get pods -n open-cluster-management-agent | grep klusterlet-registration-agent | awk '{print $1}' | xargs ${KUBECTL} logs -n open-cluster-management-agent
exit 1
fi
echo "Waiting... (${ELAPSED}s/${TIMEOUT}s)"
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done

echo "############ All-in-one env is installed successfully!!"
echo "############ Managed cluster name: ${MANAGED_CLUSTER_NAME}"
Expand Down
10 changes: 5 additions & 5 deletions examples/helloworld/helloworld.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"open-cluster-management.io/addon-framework/pkg/addonfactory"
"open-cluster-management.io/addon-framework/pkg/agent"
"open-cluster-management.io/addon-framework/pkg/utils"
addonapiv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
addonapiv1beta1 "open-cluster-management.io/api/addon/v1beta1"
clusterv1 "open-cluster-management.io/api/cluster/v1"
)

Expand All @@ -26,14 +26,14 @@ var FS embed.FS

func NewRegistrationOption(kubeConfig *rest.Config, addonName, agentName string) *agent.RegistrationOption {
return &agent.RegistrationOption{
CSRConfigurations: agent.KubeClientSignerConfigurations(addonName, agentName),
CSRApproveCheck: utils.DefaultCSRApprover(agentName),
PermissionConfig: rbac.AddonRBAC(kubeConfig),
Configurations: agent.KubeClientSignerConfigurations(addonName, agentName),
CSRApproveCheck: utils.DefaultCSRApprover(agentName),
PermissionConfig: rbac.AddonRBAC(kubeConfig),
}
}

func GetDefaultValues(cluster *clusterv1.ManagedCluster,
addon *addonapiv1alpha1.ManagedClusterAddOn) (addonfactory.Values, error) {
addon *addonapiv1beta1.ManagedClusterAddOn) (addonfactory.Values, error) {

image := os.Getenv("EXAMPLE_IMAGE_NAME")
if len(image) == 0 {
Expand Down
Loading
Loading