- name
- implement-calico-api-resource
- description
- Implements a new Calico API resource by plumbing it through all layers of the codebase. Use after API design is complete (see design-kubernetes-api skill).
## Prerequisites
Before using this skill, ensure you have:
- A designed API resource (Go structs with kubebuilder annotations, json tags, etc.) — use the `design-kubernetes-api` skill first.
- Know whether the resource is **namespaced** or **cluster-scoped**.
- Know the **Kind**, **plural name**, and any **short names** for kubectl.
## Overview of Layers
Adding a new Calico API resource touches these layers (in dependency order):
1. **API type definition** (`api/pkg/apis/projectcalico/v3/`)
2. **Code generation** (deepcopy, clients, informers, listers, OpenAPI)
3. **CRD operator types** (`libcalico-go/lib/apis/crd.projectcalico.org/v1/`)
4. **CRD v1 scheme registration** (`libcalico-go/lib/apis/crd.projectcalico.org/v1/scheme/scheme.go`)
5. **Backend model registration** (`libcalico-go/lib/backend/model/resource.go`)
6. **K8s backend resource client** (`libcalico-go/lib/backend/k8s/resources/`)
7. **K8s backend client registration** (`libcalico-go/lib/backend/k8s/client.go`)
8. **Namespace helper** (if namespaced) (`libcalico-go/lib/namespace/resource.go`)
9. **Validator** (`libcalico-go/lib/validator/v3/validator.go`)
10. **clientv3 typed client** (`libcalico-go/lib/clientv3/`)
11. **Apiserver registry** (storage, strategy, REST) (`apiserver/pkg/registry/projectcalico/`)
12. **Apiserver Calico storage adapter** (`apiserver/pkg/storage/calico/`)
13. **Apiserver storage interface switch** (`apiserver/pkg/storage/calico/storage_interface.go`)
14. **Apiserver converter** (`apiserver/pkg/storage/calico/converter.go`)
15. **Apiserver REST storage provider** (`apiserver/pkg/registry/projectcalico/rest/storage_calico.go`)
16. **Felix syncer** (if Felix needs this resource) (`libcalico-go/lib/backend/syncersv1/felixsyncer/`)
17. **RBAC** (Helm chart RBAC for node/operator)
18. **Manifests and CRD YAML** (generated)
## Workflow
Work through the following steps in order. Each step references specific files and patterns to follow.
### Step 1: API Type Definition
Create the Go type file in `api/pkg/apis/projectcalico/v3/`.
**File:** `api/pkg/apis/projectcalico/v3/<resourcename>.go`
Follow this pattern (using BGPFilter as a clean example):
```go
// Copyright (c) <YEAR> Tigera, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 ...
package v3
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
const (
KindMyResource = "MyResource"
KindMyResourceList = "MyResourceList"
)
// +genclient:nonNamespaced (cluster-scoped only)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// MyResourceList contains a list of MyResource resources.
type MyResourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
Items []MyResource `json:"items" protobuf:"bytes,2,rep,name=items"`
}
// For cluster-scoped:
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:scope=Cluster,shortName={myres}
// For namespaced:
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:scope=Namespaced,shortName={myres}
// MyResource represents <description suitable for CRD schema docs>.
type MyResource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
Spec MyResourceSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"`
}
// MyResourceSpec contains the specification for a MyResource resource.
type MyResourceSpec struct {
// ... fields with kubebuilder validation annotations
}
// NewMyResource creates a new (zeroed) MyResource struct with TypeMetadata initialised.
func NewMyResource() *MyResource {
return &MyResource{
TypeMeta: metav1.TypeMeta{
Kind: KindMyResource,
APIVersion: GroupVersionCurrent,
},
}
}
```
**Key annotations:**
- `+genclient` — generates typed client code
- `+genclient:nonNamespaced` — for cluster-scoped resources (put on BOTH the list type and the main type)
- `+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object` — generates DeepCopyObject()
- `+kubebuilder:resource:scope=Cluster|Namespaced` — CRD scope
- `+kubebuilder:resource:shortName={...}` — kubectl short names
**Gotcha:** The `List` type needs `+genclient:nonNamespaced` too (for cluster-scoped resources), and `+k8s:deepcopy-gen:interfaces=...` on both types. Do NOT put `+kubebuilder:resource` on the List type — only on the main resource type.
### Step 2: Register in API Scheme
**File:** `api/pkg/apis/projectcalico/v3/register.go`
Add both types to the `AllKnownTypes` slice:
```go
AllKnownTypes = []runtime.Object{
// ... existing types ...
&MyResource{},
&MyResourceList{},
}
```
### Step 3: Run API Code Generation
```bash
cd api && make gen-files
```
This generates the DeepCopy methods, typed Kubernetes client, informers, listers, and OpenAPI schema needed for compilation of downstream layers:
- `api/pkg/apis/projectcalico/v3/zz_generated.deepcopy.go` — DeepCopy methods
- `api/pkg/client/clientset_generated/` — typed Kubernetes client
- `api/pkg/client/informers_generated/` — informer factories
- `api/pkg/client/listers_generated/` — listers
- `api/pkg/openapi/generated.openapi.go` — OpenAPI schema
Run this early so the remaining steps can compile against the generated types. A full `make generate` at the project root is still needed later (Step 19) to pick up CRDs, manifests, and other downstream generated files.
### Step 4: CRD Operator Types (crd.projectcalico.org/v1)
Calico has a dual-CRD system. Older clusters use `crd.projectcalico.org/v1` CRDs, newer ones use `projectcalico.org/v3`. New resources need a type definition in the v1 scheme for backward compatibility.
**File:** `libcalico-go/lib/apis/crd.projectcalico.org/v1/<myresource>_types.go`
```go
package v1
import (
v3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:scope=Cluster // or Namespaced
type MyResource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec v3.MyResourceSpec `json:"spec,omitempty"`
}
```
Note: the v1 type re-uses the v3 Spec struct — only the top-level wrapper differs.
### Step 5: Register in CRD v1 Scheme
**File:** `libcalico-go/lib/apis/crd.projectcalico.org/v1/scheme/scheme.go`
Add the type to the `BuilderCRDv1()` function's `AddKnownTypes` call:
```go
&apiv3.MyResource{},
&apiv3.MyResourceList{},
```
### Step 6: Backend Model Registration
**File:** `libcalico-go/lib/backend/model/resource.go`
Add to the `init()` function:
```go
registerResourceInfo[apiv3.MyResource](apiv3.KindMyResource, "myresources")
```
The plural name here must match the CRD plural. This enables the generic `ResourceKey`-based storage path.
### Step 7: K8s Backend Resource Client
**File:** `libcalico-go/lib/backend/k8s/resources/<myresource>.go`
For simple resources that use the v3 CRDs directly (typical for new resources):
```go
package resources
import (
"reflect"
apiv3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3"
"k8s.io/client-go/rest"
)
const (
MyResourceResourceName = "MyResources"
)
func NewMyResourceClient(r rest.Interface, group BackingAPIGroup) K8sResourceClient {
return &customResourceClient{
restClient: r,
resource: MyResourceResourceName,
k8sResourceType: reflect.TypeOf(apiv3.MyResource{}),
k8sListType: reflect.TypeOf(apiv3.MyResourceList{}),
kind: apiv3.KindMyResource,
apiGroup: group,
}
}
```
**Gotcha:** The `resource` field is the CRD resource name (plural, PascalCase used by the REST client).
### Step 8: Register in K8s Backend Client
**File:** `libcalico-go/lib/backend/k8s/client.go`
Add to the resource client registration block:
```go
c.registerResourceClient(
reflect.TypeOf(model.ResourceKey{}),
reflect.TypeOf(model.ResourceListOptions{}),
apiv3.KindMyResource,
resources.NewMyResourceClient(restClient, group),
)
```
If there is any CRD cleanup or garbage-collection mechanism for v3 kinds in this client, ensure your new kind is included there as appropriate.
### Step 9: Namespace Helper (if namespaced)
**File:** `libcalico-go/lib/namespace/resource.go`
If your resource is namespaced, add its Kind to the `IsNamespaced` switch:
```go
func IsNamespaced(kind string) bool {
switch kind {
case // ... existing cases ...,
apiv3.KindMyResource:
return true
// ...
}
}
```
### Step 10: Validator
**File:** `libcalico-go/lib/validator/v3/validator.go`
**Prefer kubebuilder annotations** for validation wherever possible (e.g., `+kubebuilder:validation:Enum`, `+kubebuilder:validation:Pattern`, `+kubebuilder:validation:Required`). Kubebuilder annotations generate CRD schema validation that is enforced by the Kubernetes API server. The Go struct validator in this file is only executed by the `crd.projectcalico.org/v1` code path — it is **not** executed for `projectcalico.org/v3` CRDs, so any validation that only lives here will be silently skipped on newer clusters.
If your resource needs cross-field or complex validation that cannot be expressed with kubebuilder annotations, register a struct validator:
```go
// In the init/registration function:
registerStructValidator(validate, validateMyResourceSpec, api.MyResourceSpec{})
// Validation function:
func validateMyResourceSpec(structLevel validator.StructLevel) {
spec := structLevel.Current().Interface().(api.MyResourceSpec)
// ... validation logic ...
}
```
Simple resources may not need custom validation if kubebuilder annotations are sufficient.
### Step 11: clientv3 Typed Client
**File:** `libcalico-go/lib/clientv3/<myresource>.go`
Create the typed client interface and implementation:
```go
package clientv3
import (
"context"
v3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3"
"github.com/projectcalico/calico/libcalico-go/lib/options"
validator "github.com/projectcalico/calico/libcalico-go/lib/validator/v3"
"github.com/projectcalico/calico/libcalico-go/lib/watch"
)
// MyResourceInterface has methods to work with MyResource resources.
type MyResourceInterface interface {
Create(ctx context.Context, res *v3.MyResource, opts options.SetOptions) (*v3.MyResource, error)
Update(ctx context.Context, res *v3.MyResource, opts options.SetOptions) (*v3.MyResource, error)
Delete(ctx context.Context, name string, opts options.DeleteOptions) (*v3.MyResource, error)
Get(ctx context.Context, name string, opts options.GetOptions) (*v3.MyResource, error)
List(ctx context.Context, opts options.ListOptions) (*v3.MyResourceList, error)
Watch(ctx context.Context, opts options.ListOptions) (watch.Interface, error)
}
// myResources implements MyResourceInterface
type myResources struct {
client client
}
func (r myResources) Create(ctx context.Context, res *v3.MyResource, opts options.SetOptions) (*v3.MyResource, error) {
if err := validator.Validate(res); err != nil {
return nil, err
}
out, err := r.client.resources.Create(ctx, opts, v3.KindMyResource, res)
if out != nil {
return out.(*v3.MyResource), err
}
return nil, err
}
// ... Update, Delete, Get, List, Watch follow the same pattern.
// For namespaced: Delete/Get take (namespace, name) instead of just name.
// For namespaced: use namespace parameter instead of noNamespace constant.
```
**File:** `libcalico-go/lib/clientv3/interface.go`
Add the client interface:
```go
type MyResourceClient interface {
MyResources() MyResourceInterface
}
```
Add `MyResourceClient` to the main `Interface` interface.
**File:** `libcalico-go/lib/clientv3/client.go`
Add the accessor method:
```go
func (c client) MyResources() MyResourceInterface {
return myResources{client: c}
}
```
### Step 12: Apiserver Registry
Create a new directory: `apiserver/pkg/registry/projectcalico/<myresource>/`
**File:** `apiserver/pkg/registry/projectcalico/<myresource>/storage.go`
Follow the pattern from `apiserver/pkg/registry/projectcalico/ipamconfig/storage.go`:
- `EmptyObject()` returns `&calico.MyResource{}`
- `NewList()` returns `&calico.MyResourceList{}`
- `NewREST()` creates the registry.Store with:
- `KeyRootFunc` / `KeyFunc` using `opts.KeyRootFunc(namespaced)` / `opts.KeyFunc(namespaced)`
GitHub에서 보기