From 1bd9b0d07f7df34f059e9aa7776c6c74a14d532a Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Tue, 21 May 2024 12:23:53 -0400 Subject: [PATCH] Add partitions func for listing partitions in the local datacenter --- dependency/consul_partitions.go | 112 ++++++++++++++++++++++++++++++++ docs/templating-language.md | 10 +++ template/funcs.go | 27 +++++++- template/template.go | 6 +- 4 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 dependency/consul_partitions.go diff --git a/dependency/consul_partitions.go b/dependency/consul_partitions.go new file mode 100644 index 000000000..bc2276a58 --- /dev/null +++ b/dependency/consul_partitions.go @@ -0,0 +1,112 @@ +package dependency + +import ( + "context" + "log" + "net/url" + "slices" + "strings" + "time" + + "github.com/hashicorp/consul/api" + "github.com/pkg/errors" +) + +var ( + // Ensure implements + _ Dependency = (*ListPartitionsQuery)(nil) + + // ListPartitionsQuerySleepTime is the amount of time to sleep between + // queries, since the endpoint does not support blocking queries. + ListPartitionsQuerySleepTime = 15 * time.Second +) + +// Partition is a partition in Consul. +type Partition struct { + Name string + Description string +} + +// ListPartitionsQuery is the representation of a requested partitions +// dependency from inside a template. +type ListPartitionsQuery struct { + stopCh chan struct{} +} + +func NewListPartitionsQuery() (*ListPartitionsQuery, error) { + return &ListPartitionsQuery{ + stopCh: make(chan struct{}, 1), + }, nil +} + +func (c *ListPartitionsQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { + opts = opts.Merge(&QueryOptions{}) + + log.Printf("[TRACE] %s: GET %s", c, &url.URL{ + Path: "/v1/partitions", + RawQuery: opts.String(), + }) + + // This is certainly not elegant, but the partitions endpoint does not support + // blocking queries, so we are going to "fake it until we make it". When we + // first query, the LastIndex will be "0", meaning we should immediately + // return data, but future calls will include a LastIndex. If we have a + // LastIndex in the query metadata, sleep for 15 seconds before asking Consul + // again. + // + // This is probably okay given the frequency in which partitions actually + // change, but is technically not edge-triggering. + if opts.WaitIndex != 0 { + log.Printf("[TRACE] %s: long polling for %s", c, ListPartitionsQuerySleepTime) + + select { + case <-c.stopCh: + return nil, nil, ErrStopped + case <-time.After(ListPartitionsQuerySleepTime): + } + } + + // TODO Consider using a proper context + partitions, _, err := clients.Consul().Partitions().List(context.Background(), opts.ToConsulOpts()) + if err != nil { + return nil, nil, errors.Wrapf(err, c.String()) + } + + log.Printf("[TRACE] %s: returned %d results", c, len(partitions)) + + slices.SortFunc(partitions, func(i, j *api.Partition) int { + return strings.Compare(i.Name, j.Name) + }) + + resp := []*Partition{} + for _, partition := range partitions { + if partition != nil { + resp = append(resp, &Partition{ + Name: partition.Name, + Description: partition.Description, + }) + } + } + + // Use respWithMetadata which always increments LastIndex and results + // in fetching new data for endpoints that don't support blocking queries + return respWithMetadata(resp) +} + +// CanShare returns if this dependency is shareable. +// TODO What is this? +func (c *ListPartitionsQuery) CanShare() bool { + return true +} + +func (c *ListPartitionsQuery) String() string { + return "list.partitions" +} + +func (c *ListPartitionsQuery) Stop() { + close(c.stopCh) +} + +func (c *ListPartitionsQuery) Type() Type { + return TypeConsul +} diff --git a/docs/templating-language.md b/docs/templating-language.md index 36e7afc98..831052d7d 100644 --- a/docs/templating-language.md +++ b/docs/templating-language.md @@ -19,6 +19,7 @@ provides the following functions: * [`safeLs`](#safels) * [`node`](#node) * [`nodes`](#nodes) + * [`partitions`](#partitions) * [`peerings`](#peerings) * [`secret`](#secret) + [Format](#format) @@ -506,6 +507,15 @@ To query a different data center and order by shortest trip time to ourselves: To access map data such as `TaggedAddresses` or `Meta`, use [Go's text/template][text-template] map indexing. +### `partitions` + +Query [Consul][consul] for all partitions. + +```golang +{{ range partitions }} +{{ . }}{{ end }} +``` + ### `peerings` Query [Consul][consul] for all peerings. diff --git a/template/funcs.go b/template/funcs.go index 09db41976..42d7a16c7 100644 --- a/template/funcs.go +++ b/template/funcs.go @@ -27,14 +27,15 @@ import ( "github.com/BurntSushi/toml" spewLib "github.com/davecgh/go-spew/spew" - dep "github.com/hashicorp/consul-template/dependency" "github.com/hashicorp/consul/api" socktmpl "github.com/hashicorp/go-sockaddr/template" "github.com/imdario/mergo" "github.com/pkg/errors" "golang.org/x/text/cases" "golang.org/x/text/language" - yaml "gopkg.in/yaml.v2" + "gopkg.in/yaml.v2" + + dep "github.com/hashicorp/consul-template/dependency" ) // now is function that represents the current time in UTC. This is here @@ -74,6 +75,28 @@ func datacentersFunc(b *Brain, used, missing *dep.Set) func(ignore ...bool) ([]s } } +// partitionsFunc returns or accumulates partition dependencies. +func partitionsFunc(b *Brain, used, missing *dep.Set) func() ([]string, error) { + return func() ([]string, error) { + result := []string{} + + d, err := dep.NewListPartitionsQuery() + if err != nil { + return result, err + } + + used.Add(d) + + if value, ok := b.Recall(d); ok { + return value.([]string), nil + } + + missing.Add(d) + + return result, nil + } +} + // envFunc returns a function which checks the value of an environment variable. // Invokers can specify their own environment, which takes precedences over any // real environment variables diff --git a/template/template.go b/template/template.go index 28b5265d1..0125ad8b6 100644 --- a/template/template.go +++ b/template/template.go @@ -12,10 +12,11 @@ import ( "text/template" "github.com/Masterminds/sprig/v3" - "github.com/hashicorp/consul-template/config" - dep "github.com/hashicorp/consul-template/dependency" "github.com/pkg/errors" "golang.org/x/exp/maps" + + "github.com/hashicorp/consul-template/config" + dep "github.com/hashicorp/consul-template/dependency" ) var ( @@ -337,6 +338,7 @@ func funcMap(i *funcMapInput) template.FuncMap { "safeLs": safeLsFunc(i.brain, i.used, i.missing), "node": nodeFunc(i.brain, i.used, i.missing), "nodes": nodesFunc(i.brain, i.used, i.missing), + "partitions": partitionsFunc(i.brain, i.used, i.missing), "peerings": peeringsFunc(i.brain, i.used, i.missing), "secret": secretFunc(i.brain, i.used, i.missing), "secrets": secretsFunc(i.brain, i.used, i.missing),