From c0163bd9afa28d42f0beb22c5f9f2695704f69a3 Mon Sep 17 00:00:00 2001 From: Marcus Noble Date: Sat, 29 Jul 2023 21:48:11 +0100 Subject: [PATCH] Initial release Signed-off-by: Marcus Noble --- .github/workflows/docker.yaml | 71 ++++++++++++++++++++++++++++ Dockerfile | 21 +++++++++ README.md | 88 +++++++++++++++++++++++++++++++++++ go.mod | 26 +++++++++++ go.sum | 50 ++++++++++++++++++++ main.go | 38 +++++++++++++++ pkg/metrics/devices.go | 72 ++++++++++++++++++++++++++++ pkg/metrics/keys.go | 56 ++++++++++++++++++++++ pkg/metrics/metrics.go | 19 ++++++++ pkg/tailscale/client.go | 64 +++++++++++++++++++++++++ 10 files changed, 505 insertions(+) create mode 100644 .github/workflows/docker.yaml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 pkg/metrics/devices.go create mode 100644 pkg/metrics/keys.go create mode 100644 pkg/metrics/metrics.go create mode 100644 pkg/tailscale/client.go diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000..21766bc --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,71 @@ +name: Docker Image CI + +on: + push: + branches: [ main ] + tags: + - v* + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Prepare + id: prepare + run: | + DOCKER_IMAGE=averagemarcus/tailscale-exporter + DOCKER_PLATFORMS=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le,linux/s390x + + VERSION=latest + if [[ $GITHUB_REF == refs/tags/* ]]; then + VERSION=${GITHUB_REF#refs/tags/} + fi + + TAGS="--tag ${DOCKER_IMAGE}:${VERSION}" + if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + TAGS="$TAGS --tag ${DOCKER_IMAGE}:latest" + fi + + echo ::set-output name=tags::${TAGS} + echo ::set-output name=platforms::${DOCKER_PLATFORMS} + + - name: Set up Docker Buildx + uses: crazy-max/ghaction-docker-buildx@v3 + + - name: Cache Docker layers + uses: actions/cache@v2 + id: cache + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Docker Buildx (build) + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform ${{ steps.prepare.outputs.platforms }} \ + --output "type=image,push=false" \ + ${{ steps.prepare.outputs.tags }} \ + . + + - name: Docker Login + env: + DOCKER_USERNAME: averagemarcus + DOCKER_PASSWORD: ${{ secrets.DOCKER_TOKEN }} + run: | + echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin + + - name: Docker Buildx (push) + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform ${{ steps.prepare.outputs.platforms }} \ + --output "type=image,push=true" \ + ${{ steps.prepare.outputs.tags }} \ + . diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..338fe2c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.20-alpine as builder + +ARG TARGETPLATFORM +ARG BUILDPLATFORM +ARG TARGETOS +ARG TARGETARCH + +RUN apk update && apk add -U --no-cache ca-certificates + +WORKDIR /app/ +ADD go.mod go.sum ./ +RUN go mod download +ADD main.go . +ADD pkg/ ./pkg +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-w -s" -o tailscale-exporter main.go + +FROM --platform=${TARGETPLATFORM:-linux/amd64} scratch +WORKDIR /app/ +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /app/tailscale-exporter /app/tailscale-exporter +ENTRYPOINT ["/app/tailscale-exporter"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..4653816 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# tailscale-exporter + +Exports Prometheus metrics for Tailscale devices and keys. + +## Metrics + +* `tailscale_devices_expiry_seconds_remaining` - The number of seconds remaining until the device authentication expires +* `tailscale_devices_expiry_time` - The timestamp (as Unix timestamp) that the device expires +* `tailscale_devices_update_available` - Whether the device can be updated to a newer version of Tailscale or not +* `tailscale_keys_expiry_seconds_remaining` - The number of seconds remaining until the key expires +* `tailscale_keys_expiry_time` - The timestamp (as Unix timestamp) that the key expires + +## Configuration + +The following environment variable can be used to configure the exporter: + +* `TAILSCALE_API_KEY` - A valid Tailscale API key [Required] +* `TAILSCALE_TAILNET` - The Tailnet to export metrics for [Required] +* `PORT` - The port to run the exporter on (Defaults to `8080`) + +## Running with Docker + +```shell +export TAILSCALE_API_KEY="my-tailscale-api-key" +export TAILSCALE_TAILNET="my-tailnet.github" +docker run --rm -it -p 8080:8080 -e TAILSCALE_API_KEY -e TAILSCALE_TAILNET averagemarcus/tailscale-exporter:latest +``` + +Then visit: [http://localhost:8080/metrics](http://localhost:8080/metrics) + +## Deploying to Kubernetes + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: tailscale-exporter + labels: + app.kubernetes.io/name: tailscale-exporter +stringData: + TAILSCALE_API_KEY: xxxx + TAILSCALE_TAILNET: xxxx +--- +apiVersion: v1 +kind: Service +metadata: + name: tailscale-exporter + labels: + app.kubernetes.io/name: tailscale-exporter + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8080" +spec: + type: ClusterIP + ports: + - port: 8080 + targetPort: 8080 + selector: + app: tailscale-exporter +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tailscale-exporter + labels: + app.kubernetes.io/name: tailscale-exporter +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: tailscale-exporter + template: + metadata: + labels: + app.kubernetes.io/name: tailscale-exporter + spec: + containers: + - name: tailscale-exporter + image: averagemarcus/tailscale-exporter:latest + imagePullPolicy: Always + ports: + - containerPort: 8080 + name: metrics + envFrom: + - secretRef: + name: tailscale-exporter + +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e5e3481 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module tailscale-exporter + +go 1.20 + +replace github.com/tailscale/tailscale-client-go => github.com/AverageMarcus/tailscale-client-go v0.0.0-20230729201523-e4a8f131596d + +require ( + github.com/prometheus/client_golang v1.16.0 + github.com/tailscale/tailscale-client-go v1.9.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + github.com/tailscale/hujson v0.0.0-20220506213045-af5ed07155e5 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/sys v0.8.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.30.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cd85ebf --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +github.com/AverageMarcus/tailscale-client-go v0.0.0-20230729201523-e4a8f131596d h1:D+4y8aN+WvqPW+5hbAB0rioM6rL3+A19aoXwKP3/6Wk= +github.com/AverageMarcus/tailscale-client-go v0.0.0-20230729201523-e4a8f131596d/go.mod h1:Sipqv1kTdwxy38walczihMQQm5uWIHeizasQ3Z3tHNU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/tailscale/hujson v0.0.0-20220506213045-af5ed07155e5 h1:erxeiTyq+nw4Cz5+hLDkOwNF5/9IQWCQPv0gpb3+QHU= +github.com/tailscale/hujson v0.0.0-20220506213045-af5ed07155e5/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..580884b --- /dev/null +++ b/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "log" + "net/http" + "os" + "tailscale-exporter/pkg/metrics" + "tailscale-exporter/pkg/tailscale" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var addr string + +func init() { + port := os.Getenv("PORT") + if port != "" { + addr = ":" + port + } else { + addr = ":8080" + } +} + +func main() { + client, err := tailscale.New() + if err != nil { + log.Fatal(err) + } + + reg := prometheus.NewRegistry() + metrics.Collect(client, reg) + + http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg})) + log.Printf("tailscale-exporter") + log.Printf("Listening on %s", addr) + log.Fatal(http.ListenAndServe(addr, nil)) +} diff --git a/pkg/metrics/devices.go b/pkg/metrics/devices.go new file mode 100644 index 0000000..3bdf351 --- /dev/null +++ b/pkg/metrics/devices.go @@ -0,0 +1,72 @@ +package metrics + +import ( + "fmt" + "tailscale-exporter/pkg/tailscale" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func collectDevices(client *tailscale.Client) []prometheus.Collector { + deviceExpiry := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "tailscale_devices_expiry_time", + Help: "The expiry time of devices authentication", + ConstLabels: prometheus.Labels{ + "tailnet": client.GetTailnet(), + }, + }, + []string{"id", "created", "name"}, + ) + + deviceSecondsRemaining := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "tailscale_devices_expiry_seconds_remaining", + Help: "The number of seconds remaining until a device expires", + ConstLabels: prometheus.Labels{ + "tailnet": client.GetTailnet(), + }, + }, + []string{"id", "created", "name"}, + ) + + deviceUpdateAvailable := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "tailscale_devices_update_available", + Help: "If the device can be updated (1) or is running the latest version of Tailscale (0)", + ConstLabels: prometheus.Labels{ + "tailnet": client.GetTailnet(), + }, + }, + []string{"id", "created", "name", "version"}, + ) + + go func() { + for { + devices, err := client.GetDevices() + if err != nil { + fmt.Println("Failed to get devices: ", err) + } else { + for _, device := range devices { + if !device.KeyExpiryDisabled { + remainingSeconds := time.Until(device.Expires.Time).Seconds() + + deviceExpiry.With(prometheus.Labels{"id": device.ID, "created": device.Created.String(), "name": device.Name}).Set(float64(device.Expires.Unix())) + deviceSecondsRemaining.With(prometheus.Labels{"id": device.ID, "created": device.Created.String(), "name": device.Name}).Set(remainingSeconds) + } + + updateAvailable := 0.0 + if device.UpdateAvailable { + updateAvailable = 1.0 + } + deviceUpdateAvailable.With(prometheus.Labels{"id": device.ID, "created": device.Created.String(), "name": device.Name, "version": device.ClientVersion}).Set(updateAvailable) + } + } + + time.Sleep(60 * time.Second) + } + }() + + return []prometheus.Collector{deviceExpiry, deviceSecondsRemaining, deviceUpdateAvailable} +} diff --git a/pkg/metrics/keys.go b/pkg/metrics/keys.go new file mode 100644 index 0000000..cd587a5 --- /dev/null +++ b/pkg/metrics/keys.go @@ -0,0 +1,56 @@ +package metrics + +import ( + "fmt" + "tailscale-exporter/pkg/tailscale" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func collectKeys(client *tailscale.Client) []prometheus.Collector { + keyExpiry := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "tailscale_keys_expiry_time", + Help: "The expiry time of auth keys", + ConstLabels: prometheus.Labels{ + "tailnet": client.GetTailnet(), + }, + }, + []string{"id", "created", "description", "type"}, + ) + + keySecondsRemaining := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "tailscale_keys_expiry_seconds_remaining", + Help: "The number of seconds remaining until a key expires", + ConstLabels: prometheus.Labels{ + "tailnet": client.GetTailnet(), + }, + }, + []string{"id", "created", "description", "type"}, + ) + + go func() { + for { + keys, err := client.GetKeys() + if err != nil { + fmt.Println("Failed to get keys: ", err) + } else { + for _, key := range keys { + remainingSeconds := time.Until(key.Expires).Seconds() + keyType := "auth_key" + if key.Capabilities == nil { + keyType = "api_access_token" + } + keyExpiry.With(prometheus.Labels{"id": key.ID, "created": key.Created.String(), "description": key.Description, "type": keyType}).Set(float64(key.Expires.Unix())) + keySecondsRemaining.With(prometheus.Labels{"id": key.ID, "created": key.Created.String(), "description": key.Description, "type": keyType}).Set(remainingSeconds) + } + } + + time.Sleep(60 * time.Second) + } + }() + + return []prometheus.Collector{keyExpiry, keySecondsRemaining} +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 0000000..b227a88 --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,19 @@ +package metrics + +import ( + "tailscale-exporter/pkg/tailscale" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" +) + +var defaultCollectors = []prometheus.Collector{ + collectors.NewGoCollector(), + collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), +} + +func Collect(client *tailscale.Client, reg *prometheus.Registry) { + reg.MustRegister(defaultCollectors...) + reg.MustRegister(collectKeys(client)...) + reg.MustRegister(collectDevices(client)...) +} diff --git a/pkg/tailscale/client.go b/pkg/tailscale/client.go new file mode 100644 index 0000000..4acc291 --- /dev/null +++ b/pkg/tailscale/client.go @@ -0,0 +1,64 @@ +package tailscale + +import ( + "context" + "fmt" + "os" + + ts "github.com/tailscale/tailscale-client-go/tailscale" +) + +type Client struct { + tsClient *ts.Client + tailnet string + ctx context.Context +} + +func New() (*Client, error) { + apiKey := os.Getenv("TAILSCALE_API_KEY") + tailnet := os.Getenv("TAILSCALE_TAILNET") + + if apiKey == "" { + return nil, fmt.Errorf("TAILSCALE_API_KEY must be set") + } + if tailnet == "" { + return nil, fmt.Errorf("TAILSCALE_TAILNET must be set") + } + + client, err := ts.NewClient(apiKey, tailnet) + if err != nil { + return nil, err + } + + return &Client{ + tsClient: client, + tailnet: tailnet, + ctx: context.Background(), + }, nil +} + +func (c *Client) GetTailnet() string { + return c.tailnet +} + +func (c *Client) GetKeys() ([]ts.Key, error) { + allKeys := []ts.Key{} + + keys, err := c.tsClient.Keys(c.ctx) + if err != nil { + return nil, err + } else { + for _, k := range keys { + key, err := c.tsClient.GetKey(c.ctx, k.ID) + if err != nil { + return nil, err + } + allKeys = append(allKeys, key) + } + } + return allKeys, nil +} + +func (c *Client) GetDevices() ([]ts.Device, error) { + return c.tsClient.Devices(c.ctx) +}