# 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 ### Initializer If your controller needs to execute setup steps when the controller first starts and before any resources are reconciled, you can add an Initializer. If the controller has an Initializer, it will not start unless the Initialize method is successful. The controller does not have retry logic for the initialize method specifically, but the controller is restarted on error. When restarted, the controller will attempt to execute the initialization again. The example below has the controller creating a default resource as part of initialization. ```Go package foo import ( "context" "github.com/hashicorp/consul/internal/controller" pbv1alpha1 "github.com/hashicorp/consul/proto-public/pbfoo/v1alpha1" "github.com/hashicorp/consul/proto-public/pbresource" ) func barController() controller.Controller { return controller.ForType(pbv1alpha1.BarType). WithReconciler(barReconciler{}). WithInitializer(barInitializer{}) } type barInitializer struct{} func (barInitializer) Initialize(ctx context.Context, rt controller.Runtime) error { _, err := rt.Client.Write(ctx, &pbresource.WriteRequest{ Resource: &pbresource.Resource{ Id: &pbresource.ID{ Name: "default", Type: pbv1alpha1.BarType, }, }, }, ) if err != nil { return err } return nil } ``` ## 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, // … }, }) ``` ## Testing Now that you have created your controller its time to test it. The types of tests each controller should have and boiler plat for test files is documented [here](./testing.md)