diff --git a/docs/persistence/README.md b/docs/persistence/README.md index f705539fa..beab81b87 100644 --- a/docs/persistence/README.md +++ b/docs/persistence/README.md @@ -1,6 +1,6 @@ # Cluster Persistence -> **Note** +> **Note** > While the content of this document is still accurate, it doesn't cover the new > generic resource-oriented storage layer introduced in Consul 1.16. Please see > [Resources](../resources) for more information. diff --git a/docs/resources/README.md b/docs/resources/README.md index 1356da207..b2a05744a 100644 --- a/docs/resources/README.md +++ b/docs/resources/README.md @@ -1,5 +1,9 @@ # Resources +> **Note** +> Looking for guidance on adding new resources and controllers to Consul? Check +> out the [developer guide](./guide.md). + Consul 1.16 introduced a set of [generic APIs] for managing resources, and a [controller runtime] for building functionality on top of them. diff --git a/docs/resources/guide.md b/docs/resources/guide.md new file mode 100644 index 000000000..b3f389c00 --- /dev/null +++ b/docs/resources/guide.md @@ -0,0 +1,435 @@ +# 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"; + +package hashicorp.consul.foo.v1alpha1; + +message Bar { + 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" +) + +var BarV1Alpha1Type = &pbresource.Type{ + Group: "foo", + GroupVersion: "v1alpha1", + Kind: "bar", +} + +func RegisterTypes(r resource.Registry) { + r.Register(resource.Registration{ + Type: BarV1Alpha1Type, + Proto: &pbv1alpha1.Bar{}, + }) +} +``` + +Update the `registerResources` method in [`server.go`] to call your package's +type registration method: + +```Go +import ( + // … + "github.com/hashicorp/consul/internal/foo" + // … +) + +func (s *Server) registerResources() { + // … + foo.RegisterTypes(s.typeRegistry) + // … +} +``` + +[`server.go`]: ../../agent/consul/server.go + +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.ForType(BarV1Alpha1Type). + WithWatch(BazV1Alpha1Type, 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. + +[`controller.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 +[`controller.MapOwner`]: https://pkg.go.dev/github.com/hashicorp/consul/internal/controller#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.ForType(BarV1Alpha1Type). + 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, + // … + }, +}) +```