# Resource and Controller Developer Guide This is a whistle-stop tour through adding a new resource type and controller to Consul 🚂 ## Resource Schema Adding a new resource type begins with defining the object schema as a protobuf message, in the appropriate package under [`proto-public`](../../../proto-public). ```shell $ mkdir proto-public/pbfoo/v1alpha1 ``` ```proto // proto-public/pbfoo/v1alpha1/foo.proto syntax = "proto3"; import "pbresource/resource.proto"; import "pbresource/annotations.proto"; package hashicorp.consul.foo.v1alpha1; message Bar { option (hashicorp.consul.resource.spec) = {scope: SCOPE_NAMESPACE}; string baz = 1; hashicorp.consul.resource.ID qux = 2; } ``` ```shell $ make proto ``` Next, we must add our resource type to the registry. At this point, it's useful to add a package (e.g. under [`internal`](../../../internal)) to contain the logic associated with this resource type. The convention is to have this package export variables for its type identifiers along with a method for registering its types: ```Go // internal/foo/types.go package foo import ( "github.com/hashicorp/consul/internal/resource" pbv1alpha1 "github.com/hashicorp/consul/proto-public/pbfoo/v1alpha1" "github.com/hashicorp/consul/proto-public/pbresource" ) func RegisterTypes(r resource.Registry) { r.Register(resource.Registration{ Type: pbv1alpha1.BarType, Scope: resource.ScopePartition, Proto: &pbv1alpha1.Bar{}, }) } ``` Note that Scope reference the scope of the new resource, `resource.ScopePartition` mean that resource will be at the partition level and have no namespace, while `resource.ScopeNamespace` mean it will have both a namespace and a partition. Update the `NewTypeRegistry` method in [`type_registry.go`] to call your package's type registration method: [`type_registry.go`]: ../../../agent/consul/type_registry.go ```Go import ( // … "github.com/hashicorp/consul/internal/foo" // … ) func NewTypeRegistry() resource.Registry { // … foo.RegisterTypes(registry) // … } ``` That should be all you need to start using your new resource type. Test it out by starting an agent in dev mode: ```shell $ make dev $ consul agent -dev ``` You can now use [grpcurl](https://github.com/fullstorydev/grpcurl) to interact with the [resource service](../../../proto-public/pbresource/resource.proto): ```shell $ grpcurl -d @ \ -plaintext \ -protoset pkg/consul.protoset \ 127.0.0.1:8502 \ hashicorp.consul.resource.ResourceService.Write \ < **Warning** > Writing a status to the resource will cause it to be re-reconciled. To avoid > infinite loops, we recommend dirty checking the status before writing it with > [`resource.EqualStatus`]. [`resource.EqualStatus`]: https://pkg.go.dev/github.com/hashicorp/consul/internal/resource#EqualStatus ### Watching Other Resources In addition to watching their "managed" resources, controllers can also watch resources of different, related, types. For example, the service endpoints controller also watches workloads and services. ```Go func barController() controller.Controller { return controller.NewController("bar", pbv1alpha1.BarType). WithWatch(pbv1alpha1.BazType, controller.MapOwner) WithReconciler(barReconciler{}) } ``` The second argument to `WithWatch` is a [dependency mapper] function. Whenever a resource of the watched type is modified, the dependency mapper will be called to determine which of the controller's managed resources need to be reconciled. [`dependency.MapOwner`] is a convenience function which causes the watched resource's [owner](#ownership--cascading-deletion) to be reconciled. [dependency mapper]: https://pkg.go.dev/github.com/hashicorp/consul/internal/controller#DependencyMapper [`dependency.MapOwner`]: https://pkg.go.dev/github.com/hashicorp/consul/internal/controller/dependency#MapOwner ### Placement By default, only a single, leader-elected, replica of each controller will run within a cluster. Sometimes it's necessary to override this, for example when you want to run a copy of the controller on each server (e.g. to apply some configuration to the server whenever it changes). You can do this by changing the controller's placement. ```Go func barController() controller.Controller { return controller.NewController("bar", pbv1alpha1.BarType). WithPlacement(controller.PlacementEachServer) WithReconciler(barReconciler{}) } ``` > **Warning** > Controllers placed with [`controller.PlacementEachServer`] generally shouldn't > modify resources (as it could lead to race conditions). [`controller.PlacementEachServer`]: https://pkg.go.dev/github.com/hashicorp/consul/internal/controller#PlacementEachServer ## Ownership & Cascading Deletion The resource service implements a lightweight `1:N` ownership model where, on creation, you can mark a resource as being "owned" by another resource. When the owner is deleted, the owned resource will be deleted too. ```Go client.Write(ctx, &pbresource.WriteRequest{ Resource: &pbresource.Resource{, Owner: ownerID, // … }, }) ```