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-apiskill 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):
- API type definition (
api/pkg/apis/projectcalico/v3/) - Code generation (deepcopy, clients, informers, listers, OpenAPI)
- CRD operator types (
libcalico-go/lib/apis/crd.projectcalico.org/v1/) - CRD v1 scheme registration (
libcalico-go/lib/apis/crd.projectcalico.org/v1/scheme/scheme.go) - Backend model registration (
libcalico-go/lib/backend/model/resource.go) - K8s backend resource client (
libcalico-go/lib/backend/k8s/resources/) - K8s backend client registration (
libcalico-go/lib/backend/k8s/client.go) - Namespace helper (if namespaced) (
libcalico-go/lib/namespace/resource.go) - Validator (
libcalico-go/lib/validator/v3/validator.go) - clientv3 typed client (
libcalico-go/lib/clientv3/) - Apiserver registry (storage, strategy, REST) (
apiserver/pkg/registry/projectcalico/) - Apiserver Calico storage adapter (
apiserver/pkg/storage/calico/) - Apiserver storage interface switch (
apiserver/pkg/storage/calico/storage_interface.go) - Apiserver converter (
apiserver/pkg/storage/calico/converter.go) - Apiserver REST storage provider (
apiserver/pkg/registry/projectcalico/rest/storage_calico.go) - Felix syncer (if Felix needs this resource) (
libcalico-go/lib/backend/syncersv1/felixsyncer/) - RBAC (Helm chart RBAC for node/operator)
- 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):
// 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:
AllKnownTypes = []runtime.Object{
// ... existing types ...
&MyResource{},
&MyResourceList{},
}
Step 3: Run API Code Generation
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 methodsapi/pkg/client/clientset_generated/— typed Kubernetes clientapi/pkg/client/informers_generated/— informer factoriesapi/pkg/client/listers_generated/— listersapi/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
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:
&apiv3.MyResource{},
&apiv3.MyResourceList{},
Step 6: Backend Model Registration
File: libcalico-go/lib/backend/model/resource.go
Add to the init() function:
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):
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:
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:
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:
// 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:
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:
type MyResourceClient interface {
MyResources() MyResourceInterface
}
Add MyResourceClient to the main Interface interface.
File: libcalico-go/lib/clientv3/client.go
Add the accessor method:
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/KeyFuncusingopts.KeyRootFunc(namespaced)/opts.KeyFunc(namespaced)NoNamespaceKeyFunc(cluster-scoped) orNamespaceKeyFunc(namespaced)DefaultQualifiedResource: calico.Resource("myresources")
File: apiserver/pkg/registry/projectcalico/<myresource>/strategy.go
Follow the pattern from apiserver/pkg/registry/projectcalico/ipamconfig/strategy.go:
NamespaceScoped()returns true/false based on resource scopeGetAttrs,MatchMyResource,MyResourceToSelectableFieldsfunctions
Step 13: Apiserver Calico Storage Adapter
File: apiserver/pkg/storage/calico/<myresource>_storage.go
Follow the pattern from apiserver/pkg/storage/calico/ipamconfig_storage.go. This wires the apiserver to the libcalico-go clientv3:
func NewMyResourceStorage(opts Options) (registry.DryRunnableStorage, factory.DestroyFunc) {
c := CreateClientFromConfig()
createFn := func(ctx context.Context, c clientv3.Interface, obj resourceObject, opts clientOpts) (resourceObject, error) {
oso := opts.(options.SetOptions)
res := obj.(*api.MyResource)
return c.MyResources().Create(ctx, res, oso)
}
// ... update, get, delete, list, watch functions ...
// Build resourceStore with converter
}
Include a converter struct that handles convertToLibcalico, convertToAAPI, and convertToAAPIList. For simple resources where the API type is the same in both layers, the converter is a straightforward field copy.
Step 14: Apiserver Storage Interface Switch
File: apiserver/pkg/storage/calico/storage_interface.go
Add a case to the NewStorage switch:
case "projectcalico.org/myresources":
return NewMyResourceStorage(opts)
Step 15: Apiserver Converter
File: apiserver/pkg/storage/calico/converter.go
Add a case to the convertToAAPI function:
case *v3.MyResource:
aapi := &v3.MyResource{}
MyResourceConverter{}.convertToAAPI(obj, aapi)
return aapi
Step 16: Apiserver REST Storage Provider
File: apiserver/pkg/registry/projectcalico/rest/storage_calico.go
- Add import for the new registry package
- Create REST options and server.Options (follow the existing pattern)
- Add to the storage map:
storage["myresources"] = rESTInPeace(calico<myresource>.NewREST(scheme, *myresourceOpts))
Step 17: Syncers (Conditional)
Only needed if a component (Felix, confd, etc.) needs to watch this resource.
New resources use ResourceKey and are passed directly through the syncer layer without transformation. This means you typically do NOT need an update processor.
File: libcalico-go/lib/backend/syncersv1/felixsyncer/felixsyncerv1.go
Add to the appropriate section (always-on or leader-only):
{
ListInterface: model.ResourceListOptions{Kind: apiv3.KindMyResource},
},
No UpdateProcessor is needed for resources using ResourceKey — they pass through as-is to Felix/confd.
Alternative: Components using Kubernetes informers — Some components (kube-controllers, webhooks) use the generated Kubernetes informers from api/pkg/client/informers_generated/ instead of the syncer layer. These components automatically pick up new resources after code generation (Step 3) without additional plumbing. Check if your consuming component uses:
- Syncer (Felix, Typha via syncer): needs explicit registration in the syncer.
- Informers (kube-controllers, etc.): automatically available after codegen, but may need wiring in the controller.
Step 18: calicoctl Resource Manager
File: calicoctl/calicoctl/resourcemgr/<myresource>.go
Register the resource for calicoctl CRUD commands (create, get, update, delete, replace). This is a single file with an init() function — no other calicoctl files need changing.
package resourcemgr
import (
"context"
api "github.com/projectcalico/api/pkg/apis/projectcalico/v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
client "github.com/projectcalico/calico/libcalico-go/lib/clientv3"
"github.com/projectcalico/calico/libcalico-go/lib/options"
)
func init() {
registerResource(
api.NewMyResource(),
newMyResourceList(),
false, // isNamespaced
[]string{"myresource", "myresources"},
[]string{"NAME"},
[]string{"NAME"},
map[string]string{
"NAME": "{{.ObjectMeta.Name}}",
},
func(ctx context.Context, client client.Interface, resource ResourceObject) (ResourceObject, error) {
r := resource.(*api.MyResource)
return client.MyResources().Create(ctx, r, options.SetOptions{})
},
func(ctx context.Context, client client.Interface, resource ResourceObject) (ResourceObject, error) {
r := resource.(*api.MyResource)
return client.MyResources().Update(ctx, r, options.SetOptions{})
},
func(ctx context.Context, client client.Interface, resource ResourceObject) (ResourceObject, error) {
r := resource.(*api.MyResource)
return client.MyResources().Delete(ctx, r.Name, options.DeleteOptions{ResourceVersion: r.ResourceVersion})
},
func(ctx context.Context, client client.Interface, resource ResourceObject) (ResourceObject, error) {
r := resource.(*api.MyResource)
return client.MyResources().Get(ctx, r.Name, options.GetOptions{ResourceVersion: r.ResourceVersion})
},
func(ctx context.Context, client client.Interface, resource ResourceObject) (ResourceListObject, error) {
r := resource.(*api.MyResource)
return client.MyResources().List(ctx, options.ListOptions{ResourceVersion: r.ResourceVersion, Name: r.Name})
},
)
}
func newMyResourceList() *api.MyResourceList {
return &api.MyResourceList{
TypeMeta: metav1.TypeMeta{
Kind: api.KindMyResourceList,
APIVersion: api.GroupVersionCurrent,
},
}
}
For namespaced resources: set isNamespaced to true, add "NAMESPACE": "{{.ObjectMeta.Namespace}}" to the headings map, and pass r.Namespace as the first argument to Delete/Get/List client calls.
The init() function auto-registers the resource — calicoctl's generic CRUD commands, help text, and resource name resolution all pick it up automatically.
Step 19: RBAC
File: charts/calico/templates/calico-node-rbac.yaml (and/or operator role templates)
Add RBAC rules for the new resource if components need to access it:
- apiGroups: ["projectcalico.org"]
resources: ["myresources"]
verbs: ["get", "list", "watch"]
Step 20: Full Generation, Formatting, and Commit
# Regenerate everything — CRDs, manifests, CI config, and any remaining generated files
make generate
# This also runs make fix-changed automatically at the end.
# Verify
make yaml-lint
make check-go-mod
Gotcha: make generate at the project root produces many downstream files beyond the api/ directory — CRD YAML in manifests/, Helm chart outputs, Semaphore CI config, etc. You MUST commit all generated files alongside your source changes. CI will reject PRs with stale generated files.
Checklist Summary
Use this checklist to verify completeness:
- API type file in
api/pkg/apis/projectcalico/v3/ - Registered in
api/pkg/apis/projectcalico/v3/register.go(AllKnownTypes) - Code generation run (
cd api && make gen-files) - CRD v1 type in
libcalico-go/lib/apis/crd.projectcalico.org/v1/ - CRD v1 scheme registration in
.../scheme/scheme.go - Backend model registered in
libcalico-go/lib/backend/model/resource.go - K8s backend resource client in
libcalico-go/lib/backend/k8s/resources/ - K8s backend client registration in
libcalico-go/lib/backend/k8s/client.go - Namespace helper updated (if namespaced) in
libcalico-go/lib/namespace/resource.go - Validator in
libcalico-go/lib/validator/v3/validator.go(if needed) - clientv3 typed client in
libcalico-go/lib/clientv3/ - clientv3 interface updated in
libcalico-go/lib/clientv3/interface.go - clientv3 client accessor in
libcalico-go/lib/clientv3/client.go - Apiserver registry (storage.go + strategy.go) in
apiserver/pkg/registry/projectcalico/<resource>/ - Apiserver Calico storage adapter in
apiserver/pkg/storage/calico/<resource>_storage.go - Apiserver storage interface switch in
apiserver/pkg/storage/calico/storage_interface.go - Apiserver converter case in
apiserver/pkg/storage/calico/converter.go - Apiserver REST storage provider in
apiserver/pkg/registry/projectcalico/rest/storage_calico.go - Felix syncer (if needed) in
libcalico-go/lib/backend/syncersv1/felixsyncer/felixsyncerv1.go - calicoctl resource manager in
calicoctl/calicoctl/resourcemgr/<resource>.go - RBAC rules in Helm charts
- Formatting applied (
make fix-changed) - All generated files committed
Common Gotchas
Forgetting to regenerate: Always run
cd api && make gen-filesafter changing API types, andmake generateat root level for CRDs and manifests.CRD plural mismatch: The plural name must be consistent across
model/resource.go, the apiserver storage interface, the REST storage provider, and the CRD YAML. Kubernetes lowercases everything.Missing scheme registration: Resources must be registered in BOTH
api/pkg/apis/projectcalico/v3/register.go(the API scheme) ANDlibcalico-go/lib/apis/crd.projectcalico.org/v1/scheme/scheme.go(the CRD v1 scheme).Dual CRD versions: Calico supports both
crd.projectcalico.org/v1andprojectcalico.org/v3CRDs. New resources need type definitions and resource clients that handle both API groups, or at minimum a type in the v1 scheme.Syncer vs Informer confusion: Felix/Typha use the syncer layer (explicit registration needed). Kube-controllers and webhooks use Kubernetes informers (automatic from codegen). Check which pattern your consuming component uses.
ResourceKey passthrough: New resources should use
ResourceKey/ResourceListOptionsfor the syncer. They do NOT need update processors — the resource passes through as-is. Only legacy resources that need v1-to-v3 conversion use update processors.Status subresource: If your resource has a Status field, you need additional apiserver plumbing for the
/statussubresource endpoint (seekubecontrollersconfigfor an example).