Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions dependency/consul_namespaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package dependency

import (
"fmt"
"github.com/hashicorp/consul/api"
"log"
"net/url"
"slices"
"strings"
"time"
)

// Ensure implements
var (
_ Dependency = (*ListNamespacesQuery)(nil)

// ListNamespacesQuerySleepTime is the amount of time to sleep between
// queries, since the endpoint does not support blocking queries.
ListNamespacesQuerySleepTime = DefaultNonBlockingQuerySleepTime
)

type Namespace struct {
Name string
Description string
}

// ListNamespacesQuery is the representation of a requested namespaces
// dependency from inside a template.
type ListNamespacesQuery struct {
stopCh chan struct{}
}

func (c *ListNamespacesQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) {
opts = opts.Merge(&QueryOptions{})

log.Printf("[TRACE] %s: GET %s", c, &url.URL{
Path: "/v1/namespaces",
RawQuery: opts.String(),
})

// This is certainly not elegant, but the namespaces 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 namespaces actually
// change, but is technically not edge-triggering.
if opts.WaitIndex != 0 {
log.Printf("[TRACE] %s: long polling for %s", c, ListNamespacesQuerySleepTime)

select {
case <-c.stopCh:
return nil, nil, ErrStopped
case <-time.After(ListNamespacesQuerySleepTime):
}
}

namespaces, _, err := clients.Consul().Namespaces().List(opts.ToConsulOpts())
if err != nil {
if strings.Contains(err.Error(), "Invalid URL path") {
return nil, nil, fmt.Errorf("%s: Namespaces are an enterprise feature: %w", c.String(), err)
}

return nil, nil, fmt.Errorf("%s: %w", c.String(), err)
}

log.Printf("[TRACE] %s: returned %d results", c, len(namespaces))

slices.SortFunc(namespaces, func(i, j *api.Namespace) int {
return strings.Compare(i.Name, j.Name)
})

resp := []*Namespace{}
for _, namespace := range namespaces {
if namespace != nil {
resp = append(resp, &Namespace{
Name: namespace.Name,
Description: namespace.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 when consul-template is running in de-duplication mode.
func (c *ListNamespacesQuery) CanShare() bool {
return true
}

func (c *ListNamespacesQuery) String() string {
return "list.namespaces"
}

func (c *ListNamespacesQuery) Stop() {
close(c.stopCh)
}

func (c *ListNamespacesQuery) Type() Type {
return TypeConsul
}
13 changes: 13 additions & 0 deletions docs/templating-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ provides the following functions:
* [`safeLs`](#safels)
* [`node`](#node)
* [`nodes`](#nodes)
* [`namespaces`](#namespaces)
* [`partitions`](#partitions)
* [`peerings`](#peerings)
* [`secret`](#secret)
Expand Down Expand Up @@ -526,6 +527,18 @@ 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.

### `namespaces`

Query [Consul][consul] for all namespaces in the Consul cluster.

**Note:** Namespaces are an Enterprise-only feature of Consul.

```golang
{{ range namespaces }}
{{ .Name }} : {{ .Description }}
{{ end }}
```

### `partitions`

Query [Consul][consul] for all partitions.
Expand Down
22 changes: 22 additions & 0 deletions template/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,28 @@ func partitionsFunc(b *Brain, used, missing *dep.Set) func() ([]*dep.Partition,
}
}

// namespacesFunc returns or accumulates namespace dependencies.
func namespacesFunc(b *Brain, used, missing *dep.Set) func() ([]*dep.Namespace, error) {
return func() ([]*dep.Namespace, error) {
result := []*dep.Namespace{}

d, err := dep.NewListNamespacesQuery()
if err != nil {
return result, err
}

used.Add(d)

if value, ok := b.Recall(d); ok {
return value.([]*dep.Namespace), nil
}

missing.Add(d)

return result, nil
}
}

// exportedServicesFunc returns or accumulates partition dependencies.
func exportedServicesFunc(b *Brain, used, missing *dep.Set) func(...string) ([]dep.ExportedService, error) {
return func(s ...string) ([]dep.ExportedService, error) {
Expand Down
1 change: 1 addition & 0 deletions template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ func funcMap(i *funcMapInput) template.FuncMap {
"node": nodeFunc(i.brain, i.used, i.missing),
"nodes": nodesFunc(i.brain, i.used, i.missing),
"partitions": partitionsFunc(i.brain, i.used, i.missing),
"namespaces": namespacesFunc(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),
Expand Down