mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-20 11:58:39 +00:00
Revert "cmd/{k8s-nameserver,k8s-operator},k8s-operator: add a kube nameserver, make operator deploy it (#11017)" (#11669)
Temporarily reverting this PR to avoid releasing half finished featue. This reverts commit 9e2f58f8461b32d5970f2680beda13153196ce46. Signed-off-by: Irbe Krumina <irbe@tailscale.com>
This commit is contained in:
parent
0001237253
commit
231e44e742
8
Makefile
8
Makefile
@ -100,14 +100,6 @@ publishdevoperator: ## Build and publish k8s-operator image to location specifie
|
|||||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
|
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
|
||||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=operator ./build_docker.sh
|
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=operator ./build_docker.sh
|
||||||
|
|
||||||
publishdevnameserver: ## Build and publish k8s-nameserver image to location specified by ${REPO}
|
|
||||||
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
|
|
||||||
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
|
|
||||||
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
|
|
||||||
@test "${REPO}" != "tailscale/k8s-nameserver" || (echo "REPO=... must not be tailscale/k8s-nameserver" && exit 1)
|
|
||||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-nameserver" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-nameserver" && exit 1)
|
|
||||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-nameserver ./build_docker.sh
|
|
||||||
|
|
||||||
help: ## Show this help
|
help: ## Show this help
|
||||||
@echo "\nSpecify a command. The choices are:\n"
|
@echo "\nSpecify a command. The choices are:\n"
|
||||||
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
|
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
|
||||||
|
@ -70,22 +70,6 @@ case "$TARGET" in
|
|||||||
--target="${PLATFORM}" \
|
--target="${PLATFORM}" \
|
||||||
/usr/local/bin/operator
|
/usr/local/bin/operator
|
||||||
;;
|
;;
|
||||||
k8s-nameserver)
|
|
||||||
DEFAULT_REPOS="tailscale/k8s-nameserver"
|
|
||||||
REPOS="${REPOS:-${DEFAULT_REPOS}}"
|
|
||||||
go run github.com/tailscale/mkctr \
|
|
||||||
--gopaths="tailscale.com/cmd/k8s-nameserver:/usr/local/bin/k8s-nameserver" \
|
|
||||||
--ldflags=" \
|
|
||||||
-X tailscale.com/version.longStamp=${VERSION_LONG} \
|
|
||||||
-X tailscale.com/version.shortStamp=${VERSION_SHORT} \
|
|
||||||
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
|
|
||||||
--base="${BASE}" \
|
|
||||||
--tags="${TAGS}" \
|
|
||||||
--repos="${REPOS}" \
|
|
||||||
--push="${PUSH}" \
|
|
||||||
--target="${PLATFORM}" \
|
|
||||||
/usr/local/bin/k8s-nameserver
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "unknown target: $TARGET"
|
echo "unknown target: $TARGET"
|
||||||
exit 1
|
exit 1
|
||||||
|
@ -1,348 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build !plan9
|
|
||||||
|
|
||||||
// k8s-nameserver is a simple nameserver implementation meant to be used with
|
|
||||||
// k8s-operator to allow to resolve magicDNS names associated with tailnet
|
|
||||||
// proxies in cluster.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
operatorutils "tailscale.com/k8s-operator"
|
|
||||||
"tailscale.com/util/dnsname"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// tsNetDomain is the domain that this DNS nameserver has registered a handler for.
|
|
||||||
tsNetDomain = "ts.net"
|
|
||||||
// addr is the the address that the UDP and TCP listeners will listen on.
|
|
||||||
addr = ":1053"
|
|
||||||
|
|
||||||
// The following constants are specific to the nameserver configuration
|
|
||||||
// provided by a mounted Kubernetes Configmap. The Configmap mounted at
|
|
||||||
// /config is the only supported way for configuring this nameserver.
|
|
||||||
defaultDNSConfigDir = "/config"
|
|
||||||
defaultDNSFile = "dns.json"
|
|
||||||
kubeletMountedConfigLn = "..data"
|
|
||||||
)
|
|
||||||
|
|
||||||
// nameserver is a simple nameserver that responds to DNS queries for A records
|
|
||||||
// for ts.net domain names over UDP or TCP. It serves DNS responses from
|
|
||||||
// in-memory IPv4 host records. It is intended to be deployed on Kubernetes with
|
|
||||||
// a ConfigMap mounted at /config that should contain the host records. It
|
|
||||||
// dynamically reconfigures its in-memory mappings as the contents of the
|
|
||||||
// mounted ConfigMap changes.
|
|
||||||
type nameserver struct {
|
|
||||||
// configReader returns the latest desired configuration (host records)
|
|
||||||
// for the nameserver. By default it gets set to a reader that reads
|
|
||||||
// from a Kubernetes ConfigMap mounted at /config, but this can be
|
|
||||||
// overridden in tests.
|
|
||||||
configReader configReaderFunc
|
|
||||||
// configWatcher is a watcher that returns an event when the desired
|
|
||||||
// configuration has changed and the nameserver should update the
|
|
||||||
// in-memory records.
|
|
||||||
configWatcher <-chan string
|
|
||||||
|
|
||||||
mu sync.Mutex // protects following
|
|
||||||
// ip4 are the in-memory hostname -> IP4 mappings that the nameserver
|
|
||||||
// uses to respond to A record queries.
|
|
||||||
ip4 map[dnsname.FQDN][]net.IP
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
// Ensure that we watch the kube Configmap mounted at /config for
|
|
||||||
// nameserver configuration updates and send events when updates happen.
|
|
||||||
c := ensureWatcherForKubeConfigMap(ctx)
|
|
||||||
|
|
||||||
ns := &nameserver{
|
|
||||||
configReader: configMapConfigReader,
|
|
||||||
configWatcher: c,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that in-memory records get set up to date now and will get
|
|
||||||
// reset when the configuration changes.
|
|
||||||
ns.runRecordsReconciler(ctx)
|
|
||||||
|
|
||||||
// Register a DNS server handle for ts.net domain names. Not having a
|
|
||||||
// handle registered for any other domain names is how we enforce that
|
|
||||||
// this nameserver can only be used for ts.net domains - querying any
|
|
||||||
// other domain names returns Rcode Refused.
|
|
||||||
dns.HandleFunc(tsNetDomain, ns.handleFunc())
|
|
||||||
|
|
||||||
// Listen for DNS queries over UDP and TCP.
|
|
||||||
udpSig := make(chan os.Signal)
|
|
||||||
tcpSig := make(chan os.Signal)
|
|
||||||
go listenAndServe("udp", addr, udpSig)
|
|
||||||
go listenAndServe("tcp", addr, tcpSig)
|
|
||||||
sig := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
s := <-sig
|
|
||||||
log.Printf("OS signal (%s) received, shutting down\n", s)
|
|
||||||
cancel() // exit the records reconciler and configmap watcher goroutines
|
|
||||||
udpSig <- s // stop the UDP listener
|
|
||||||
tcpSig <- s // stop the TCP listener
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleFunc is a DNS query handler that can respond to A record queries from
|
|
||||||
// the nameserver's in-memory records.
|
|
||||||
// - If an A record query is received and the
|
|
||||||
// nameserver's in-memory records contain records for the queried domain name,
|
|
||||||
// return a success response.
|
|
||||||
// - If an A record query is received, but the
|
|
||||||
// nameserver's in-memory records do not contain records for the queried domain name,
|
|
||||||
// return NXDOMAIN.
|
|
||||||
// - If an A record query is received, but the queried domain name is not valid, return Format Error.
|
|
||||||
// - If a query is received for any other record type than A, return Not Implemented.
|
|
||||||
func (n *nameserver) handleFunc() func(w dns.ResponseWriter, r *dns.Msg) {
|
|
||||||
h := func(w dns.ResponseWriter, r *dns.Msg) {
|
|
||||||
m := new(dns.Msg)
|
|
||||||
defer func() {
|
|
||||||
w.WriteMsg(m)
|
|
||||||
}()
|
|
||||||
if len(r.Question) < 1 {
|
|
||||||
log.Print("[unexpected] nameserver received a request with no questions\n")
|
|
||||||
m = r.SetRcodeFormatError(r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO (irbekrm): maybe set message compression
|
|
||||||
switch r.Question[0].Qtype {
|
|
||||||
case dns.TypeA:
|
|
||||||
q := r.Question[0].Name
|
|
||||||
fqdn, err := dnsname.ToFQDN(q)
|
|
||||||
if err != nil {
|
|
||||||
m = r.SetRcodeFormatError(r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// The only supported use of this nameserver is as a
|
|
||||||
// single source of truth for MagicDNS names by
|
|
||||||
// non-tailnet Kubernetes workloads.
|
|
||||||
m.Authoritative = true
|
|
||||||
m.RecursionAvailable = false
|
|
||||||
|
|
||||||
ips := n.lookupIP4(fqdn)
|
|
||||||
if ips == nil || len(ips) == 0 {
|
|
||||||
// As we are the authoritative nameserver for MagicDNS
|
|
||||||
// names, if we do not have a record for this MagicDNS
|
|
||||||
// name, it does not exist.
|
|
||||||
m = m.SetRcode(r, dns.RcodeNameError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO (irbekrm): what TTL?
|
|
||||||
for _, ip := range ips {
|
|
||||||
rr := &dns.A{Hdr: dns.RR_Header{Name: q, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0}, A: ip}
|
|
||||||
m.SetRcode(r, dns.RcodeSuccess)
|
|
||||||
m.Answer = append(m.Answer, rr)
|
|
||||||
}
|
|
||||||
case dns.TypeAAAA:
|
|
||||||
// TODO (irbekrm): implement IPv6 support
|
|
||||||
fallthrough
|
|
||||||
default:
|
|
||||||
log.Printf("[unexpected] nameserver received a query for an unsupported record type: %s\n", r.Question[0].String())
|
|
||||||
m.SetRcode(r, dns.RcodeNotImplemented)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// runRecordsReconciler ensures that nameserver's in-memory records are
|
|
||||||
// reset when the provided configuration changes.
|
|
||||||
func (n *nameserver) runRecordsReconciler(ctx context.Context) {
|
|
||||||
log.Print("updating nameserver's records from the provided configuration...\n")
|
|
||||||
if err := n.resetRecords(); err != nil { // ensure records are up to date before the nameserver starts
|
|
||||||
log.Fatalf("error setting nameserver's records: %v\n", err)
|
|
||||||
}
|
|
||||||
log.Print("nameserver's records were updated\n")
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.Printf("context cancelled, exiting records reconciler\n")
|
|
||||||
return
|
|
||||||
case <-n.configWatcher:
|
|
||||||
log.Print("configuration update detected, resetting records\n")
|
|
||||||
if err := n.resetRecords(); err != nil {
|
|
||||||
// TODO (irbekrm): this runs in a
|
|
||||||
// container that will be thrown away,
|
|
||||||
// so this should be ok. But maybe still
|
|
||||||
// need to ensure that the DNS server
|
|
||||||
// terminates connections more
|
|
||||||
// gracefully.
|
|
||||||
log.Fatalf("error resetting records: %v\n", err)
|
|
||||||
}
|
|
||||||
log.Print("nameserver records were reset\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// resetRecords sets the in-memory DNS records of this nameserver from the
|
|
||||||
// provided configuration. It does not check for the diff, so the caller is
|
|
||||||
// expected to ensure that this is only called when reset is needed.
|
|
||||||
func (n *nameserver) resetRecords() error {
|
|
||||||
dnsCfgBytes, err := n.configReader()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error reading nameserver's configuration: %v\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if dnsCfgBytes == nil || len(dnsCfgBytes) < 1 {
|
|
||||||
log.Print("nameserver's configuration is empty, any in-memory records will be unset\n")
|
|
||||||
n.mu.Lock()
|
|
||||||
n.ip4 = make(map[dnsname.FQDN][]net.IP)
|
|
||||||
n.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dnsCfg := &operatorutils.Records{}
|
|
||||||
err = json.Unmarshal(dnsCfgBytes, dnsCfg)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error unmarshalling nameserver configuration: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dnsCfg.Version != operatorutils.Alpha1Version {
|
|
||||||
return fmt.Errorf("unsupported configuration version %s, supported versions are %s\n", dnsCfg.Version, operatorutils.Alpha1Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
ip4 := make(map[dnsname.FQDN][]net.IP)
|
|
||||||
defer func() {
|
|
||||||
n.mu.Lock()
|
|
||||||
defer n.mu.Unlock()
|
|
||||||
n.ip4 = ip4
|
|
||||||
}()
|
|
||||||
|
|
||||||
if dnsCfg.IP4 == nil || len(dnsCfg.IP4) == 0 {
|
|
||||||
log.Print("nameserver's configuration contains no records, any in-memory records will be unset\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for fqdn, ips := range dnsCfg.IP4 {
|
|
||||||
fqdn, err := dnsname.ToFQDN(fqdn)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("invalid nameserver's configuration: %s is not a valid FQDN: %v; skipping this record\n", fqdn, err)
|
|
||||||
continue // one invalid hostname should not break the whole nameserver
|
|
||||||
}
|
|
||||||
for _, ipS := range ips {
|
|
||||||
ip := net.ParseIP(ipS).To4()
|
|
||||||
if ip == nil { // To4 returns nil if IP is not a IPv4 address
|
|
||||||
log.Printf("invalid nameserver's configuration: %v does not appear to be an IPv4 address; skipping this record\n", ipS)
|
|
||||||
continue // one invalid IP address should not break the whole nameserver
|
|
||||||
}
|
|
||||||
ip4[fqdn] = []net.IP{ip}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// listenAndServe starts a DNS server for the provided network and address.
|
|
||||||
func listenAndServe(net, addr string, shutdown chan os.Signal) {
|
|
||||||
s := &dns.Server{Addr: addr, Net: net}
|
|
||||||
go func() {
|
|
||||||
<-shutdown
|
|
||||||
log.Printf("shutting down server for %s\n", net)
|
|
||||||
s.Shutdown()
|
|
||||||
}()
|
|
||||||
log.Printf("listening for %s queries on %s\n", net, addr)
|
|
||||||
if err := s.ListenAndServe(); err != nil {
|
|
||||||
log.Fatalf("error running %s server: %v\n", net, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureWatcherForKubeConfigMap sets up a new file watcher for the ConfigMap
|
|
||||||
// that's expected to be mounted at /config. Returns a channel that receives an
|
|
||||||
// event every time the contents get updated.
|
|
||||||
func ensureWatcherForKubeConfigMap(ctx context.Context) chan string {
|
|
||||||
c := make(chan string)
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error creating a new watcher for the mounted ConfigMap: %v\n", err)
|
|
||||||
}
|
|
||||||
// kubelet mounts configmap to a Pod using a series of symlinks, one of
|
|
||||||
// which is <mount-dir>/..data that Kubernetes recommends consumers to
|
|
||||||
// use if they need to monitor changes
|
|
||||||
// https://github.com/kubernetes/kubernetes/blob/v1.28.1/pkg/volume/util/atomic_writer.go#L39-L61
|
|
||||||
toWatch := filepath.Join(defaultDNSConfigDir, kubeletMountedConfigLn)
|
|
||||||
go func() {
|
|
||||||
defer watcher.Close()
|
|
||||||
log.Printf("starting file watch for %s\n", defaultDNSConfigDir)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.Print("context cancelled, exiting ConfigMap watcher\n")
|
|
||||||
return
|
|
||||||
case event, ok := <-watcher.Events:
|
|
||||||
if !ok {
|
|
||||||
log.Fatal("watcher finished; exiting")
|
|
||||||
}
|
|
||||||
if event.Name == toWatch {
|
|
||||||
msg := fmt.Sprintf("ConfigMap update received: %s\n", event)
|
|
||||||
log.Print(msg)
|
|
||||||
c <- msg
|
|
||||||
}
|
|
||||||
case err, ok := <-watcher.Errors:
|
|
||||||
if !ok {
|
|
||||||
// TODO (irbekrm): this runs in a
|
|
||||||
// container that will be thrown away,
|
|
||||||
// so this should be ok. But maybe still
|
|
||||||
// need to ensure that the DNS server
|
|
||||||
// terminates connections more
|
|
||||||
// gracefully.
|
|
||||||
log.Fatalf("[unexpected] configuration watcher error: errors watcher finished: %v\n", err)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// TODO (irbekrm): this runs in a
|
|
||||||
// container that will be thrown away,
|
|
||||||
// so this should be ok. But maybe still
|
|
||||||
// need to ensure that the DNS server
|
|
||||||
// terminates connections more
|
|
||||||
// gracefully.
|
|
||||||
log.Fatalf("[unexpected] error watching configuration: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if err = watcher.Add(defaultDNSConfigDir); err != nil {
|
|
||||||
log.Fatalf("failed setting up a watcher for the mounted ConfigMap: %v\n", err)
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// configReaderFunc is a function that returns the desired nameserver configuration.
|
|
||||||
type configReaderFunc func() ([]byte, error)
|
|
||||||
|
|
||||||
// configMapConfigReader reads the desired nameserver configuration from a
|
|
||||||
// dns.json file in a ConfigMap mounted at /config.
|
|
||||||
var configMapConfigReader configReaderFunc = func() ([]byte, error) {
|
|
||||||
if contents, err := os.ReadFile(filepath.Join(defaultDNSConfigDir, defaultDNSFile)); err == nil {
|
|
||||||
return contents, nil
|
|
||||||
} else if os.IsNotExist(err) {
|
|
||||||
return nil, nil
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookupIP4 returns any IPv4 addresses for the given FQDN from nameserver's
|
|
||||||
// in-memory records.
|
|
||||||
func (n *nameserver) lookupIP4(fqdn dnsname.FQDN) []net.IP {
|
|
||||||
if n.ip4 == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
n.mu.Lock()
|
|
||||||
defer n.mu.Unlock()
|
|
||||||
f := n.ip4[fqdn]
|
|
||||||
return f
|
|
||||||
}
|
|
@ -1,227 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build !plan9
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
"tailscale.com/util/dnsname"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNameserver(t *testing.T) {
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ip4 map[dnsname.FQDN][]net.IP
|
|
||||||
query *dns.Msg
|
|
||||||
wantResp *dns.Msg
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "A record query, record exists",
|
|
||||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
|
||||||
query: &dns.Msg{
|
|
||||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeA}},
|
|
||||||
MsgHdr: dns.MsgHdr{Id: 1, RecursionDesired: true},
|
|
||||||
},
|
|
||||||
wantResp: &dns.Msg{
|
|
||||||
Answer: []dns.RR{&dns.A{Hdr: dns.RR_Header{
|
|
||||||
Name: "foo.bar.com", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0},
|
|
||||||
A: net.IP{1, 2, 3, 4}}},
|
|
||||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeA}},
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Id: 1,
|
|
||||||
Rcode: dns.RcodeSuccess,
|
|
||||||
RecursionAvailable: false,
|
|
||||||
RecursionDesired: true,
|
|
||||||
Response: true,
|
|
||||||
Opcode: dns.OpcodeQuery,
|
|
||||||
Authoritative: true,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "A record query, record does not exist",
|
|
||||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
|
||||||
query: &dns.Msg{
|
|
||||||
Question: []dns.Question{{Name: "baz.bar.com", Qtype: dns.TypeA}},
|
|
||||||
MsgHdr: dns.MsgHdr{Id: 1},
|
|
||||||
},
|
|
||||||
wantResp: &dns.Msg{
|
|
||||||
Question: []dns.Question{{Name: "baz.bar.com", Qtype: dns.TypeA}},
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Id: 1,
|
|
||||||
Rcode: dns.RcodeNameError,
|
|
||||||
RecursionAvailable: false,
|
|
||||||
Response: true,
|
|
||||||
Opcode: dns.OpcodeQuery,
|
|
||||||
Authoritative: true,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "A record query, but the name is not a valid FQDN",
|
|
||||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
|
||||||
query: &dns.Msg{
|
|
||||||
Question: []dns.Question{{Name: "foo..bar.com", Qtype: dns.TypeA}},
|
|
||||||
MsgHdr: dns.MsgHdr{Id: 1},
|
|
||||||
},
|
|
||||||
wantResp: &dns.Msg{
|
|
||||||
Question: []dns.Question{{Name: "foo..bar.com", Qtype: dns.TypeA}},
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Id: 1,
|
|
||||||
Rcode: dns.RcodeFormatError,
|
|
||||||
Response: true,
|
|
||||||
Opcode: dns.OpcodeQuery,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "AAAA record query",
|
|
||||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
|
||||||
query: &dns.Msg{
|
|
||||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeAAAA}},
|
|
||||||
MsgHdr: dns.MsgHdr{Id: 1},
|
|
||||||
},
|
|
||||||
wantResp: &dns.Msg{
|
|
||||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeAAAA}},
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Id: 1,
|
|
||||||
Rcode: dns.RcodeNotImplemented,
|
|
||||||
Response: true,
|
|
||||||
Opcode: dns.OpcodeQuery,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "AAAA record query",
|
|
||||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
|
||||||
query: &dns.Msg{
|
|
||||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeAAAA}},
|
|
||||||
MsgHdr: dns.MsgHdr{Id: 1},
|
|
||||||
},
|
|
||||||
wantResp: &dns.Msg{
|
|
||||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeAAAA}},
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Id: 1,
|
|
||||||
Rcode: dns.RcodeNotImplemented,
|
|
||||||
Response: true,
|
|
||||||
Opcode: dns.OpcodeQuery,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "CNAME record query",
|
|
||||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
|
||||||
query: &dns.Msg{
|
|
||||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeCNAME}},
|
|
||||||
MsgHdr: dns.MsgHdr{Id: 1},
|
|
||||||
},
|
|
||||||
wantResp: &dns.Msg{
|
|
||||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeCNAME}},
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Id: 1,
|
|
||||||
Rcode: dns.RcodeNotImplemented,
|
|
||||||
Response: true,
|
|
||||||
Opcode: dns.OpcodeQuery,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
ns := &nameserver{
|
|
||||||
ip4: tt.ip4,
|
|
||||||
}
|
|
||||||
handler := ns.handleFunc()
|
|
||||||
fakeRespW := &fakeResponseWriter{}
|
|
||||||
handler(fakeRespW, tt.query)
|
|
||||||
if diff := cmp.Diff(*fakeRespW.msg, *tt.wantResp); diff != "" {
|
|
||||||
t.Fatalf("unexpected response (-got +want): \n%s", diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResetRecords(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
config []byte
|
|
||||||
hasIp4 map[dnsname.FQDN][]net.IP
|
|
||||||
wantsIp4 map[dnsname.FQDN][]net.IP
|
|
||||||
wantsErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "previously empty nameserver.ip4 gets set",
|
|
||||||
config: []byte(`{"version": "v1alpha1", "ip4": {"foo.bar.com": ["1.2.3.4"]}}`),
|
|
||||||
wantsIp4: map[dnsname.FQDN][]net.IP{"foo.bar.com.": {{1, 2, 3, 4}}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nameserver.ip4 gets reset",
|
|
||||||
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
|
|
||||||
config: []byte(`{"version": "v1alpha1", "ip4": {"foo.bar.com": ["1.2.3.4"]}}`),
|
|
||||||
wantsIp4: map[dnsname.FQDN][]net.IP{"foo.bar.com.": {{1, 2, 3, 4}}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "configuration with incompatible version",
|
|
||||||
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
|
|
||||||
config: []byte(`{"version": "v1beta1", "ip4": {"foo.bar.com": ["1.2.3.4"]}}`),
|
|
||||||
wantsIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
|
|
||||||
wantsErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nameserver.ip4 gets reset to empty config when no configuration is provided",
|
|
||||||
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
|
|
||||||
wantsIp4: make(map[dnsname.FQDN][]net.IP),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nameserver.ip4 gets reset to empty config when the provided configuration is empty",
|
|
||||||
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
|
|
||||||
config: []byte(`{"version": "v1alpha1", "ip4": {}}`),
|
|
||||||
wantsIp4: make(map[dnsname.FQDN][]net.IP),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
ns := &nameserver{
|
|
||||||
ip4: tt.hasIp4,
|
|
||||||
configReader: func() ([]byte, error) { return tt.config, nil },
|
|
||||||
}
|
|
||||||
if err := ns.resetRecords(); err == nil == tt.wantsErr {
|
|
||||||
t.Errorf("resetRecords() returned err: %v, wantsErr: %v", err, tt.wantsErr)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(ns.ip4, tt.wantsIp4); diff != "" {
|
|
||||||
t.Fatalf("unexpected nameserver.ip4 contents (-got +want): \n%s", diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fakeResponseWriter is a faked out dns.ResponseWriter that can be used in
|
|
||||||
// tests that need to read the response message that was written.
|
|
||||||
type fakeResponseWriter struct {
|
|
||||||
msg *dns.Msg
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ dns.ResponseWriter = &fakeResponseWriter{}
|
|
||||||
|
|
||||||
func (fr *fakeResponseWriter) WriteMsg(msg *dns.Msg) error {
|
|
||||||
fr.msg = msg
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (fr *fakeResponseWriter) LocalAddr() net.Addr {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (fr *fakeResponseWriter) RemoteAddr() net.Addr {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (fr *fakeResponseWriter) Write([]byte) (int, error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
func (fr *fakeResponseWriter) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (fr *fakeResponseWriter) TsigStatus() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (fr *fakeResponseWriter) TsigTimersOnly(bool) {}
|
|
||||||
func (fr *fakeResponseWriter) Hijack() {}
|
|
@ -24,9 +24,6 @@ rules:
|
|||||||
- apiGroups: ["tailscale.com"]
|
- apiGroups: ["tailscale.com"]
|
||||||
resources: ["connectors", "connectors/status", "proxyclasses", "proxyclasses/status"]
|
resources: ["connectors", "connectors/status", "proxyclasses", "proxyclasses/status"]
|
||||||
verbs: ["get", "list", "watch", "update"]
|
verbs: ["get", "list", "watch", "update"]
|
||||||
- apiGroups: ["tailscale.com"]
|
|
||||||
resources: ["dnsconfigs", "dnsconfigs/status"]
|
|
||||||
verbs: ["get", "list", "watch", "update"]
|
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
kind: ClusterRoleBinding
|
kind: ClusterRoleBinding
|
||||||
@ -48,10 +45,10 @@ metadata:
|
|||||||
namespace: {{ .Release.Namespace }}
|
namespace: {{ .Release.Namespace }}
|
||||||
rules:
|
rules:
|
||||||
- apiGroups: [""]
|
- apiGroups: [""]
|
||||||
resources: ["secrets", "serviceaccounts", "configmaps"]
|
resources: ["secrets"]
|
||||||
verbs: ["*"]
|
verbs: ["*"]
|
||||||
- apiGroups: ["apps"]
|
- apiGroups: ["apps"]
|
||||||
resources: ["statefulsets", "deployments"]
|
resources: ["statefulsets"]
|
||||||
verbs: ["*"]
|
verbs: ["*"]
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
apiVersion: apiextensions.k8s.io/v1
|
|
||||||
kind: CustomResourceDefinition
|
|
||||||
metadata:
|
|
||||||
annotations:
|
|
||||||
controller-gen.kubebuilder.io/version: v0.13.0
|
|
||||||
name: dnsconfigs.tailscale.com
|
|
||||||
spec:
|
|
||||||
group: tailscale.com
|
|
||||||
names:
|
|
||||||
kind: DNSConfig
|
|
||||||
listKind: DNSConfigList
|
|
||||||
plural: dnsconfigs
|
|
||||||
shortNames:
|
|
||||||
- dc
|
|
||||||
singular: dnsconfig
|
|
||||||
scope: Cluster
|
|
||||||
versions:
|
|
||||||
- additionalPrinterColumns:
|
|
||||||
- description: Service IP address of the nameserver
|
|
||||||
jsonPath: .status.nameserverStatus.ip
|
|
||||||
name: NameserverIP
|
|
||||||
type: string
|
|
||||||
name: v1alpha1
|
|
||||||
schema:
|
|
||||||
openAPIV3Schema:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- spec
|
|
||||||
properties:
|
|
||||||
apiVersion:
|
|
||||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
|
||||||
type: string
|
|
||||||
kind:
|
|
||||||
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
|
||||||
type: string
|
|
||||||
metadata:
|
|
||||||
type: object
|
|
||||||
spec:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- nameserver
|
|
||||||
properties:
|
|
||||||
nameserver:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
image:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
repo:
|
|
||||||
type: string
|
|
||||||
tag:
|
|
||||||
type: string
|
|
||||||
status:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
conditions:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
description: ConnectorCondition contains condition information for a Connector.
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
- type
|
|
||||||
properties:
|
|
||||||
lastTransitionTime:
|
|
||||||
description: LastTransitionTime is the timestamp corresponding to the last status change of this condition.
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
message:
|
|
||||||
description: Message is a human readable description of the details of the last transition, complementing reason.
|
|
||||||
type: string
|
|
||||||
observedGeneration:
|
|
||||||
description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Connector.
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
reason:
|
|
||||||
description: Reason is a brief machine readable explanation for the condition's last transition.
|
|
||||||
type: string
|
|
||||||
status:
|
|
||||||
description: Status of the condition, one of ('True', 'False', 'Unknown').
|
|
||||||
type: string
|
|
||||||
type:
|
|
||||||
description: Type of the condition, known values are (`SubnetRouterReady`).
|
|
||||||
type: string
|
|
||||||
x-kubernetes-list-map-keys:
|
|
||||||
- type
|
|
||||||
x-kubernetes-list-type: map
|
|
||||||
nameserverStatus:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
ip:
|
|
||||||
type: string
|
|
||||||
served: true
|
|
||||||
storage: true
|
|
||||||
subresources:
|
|
||||||
status: {}
|
|
@ -1,4 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: dnsconfig
|
|
@ -1,37 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: nameserver
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
revisionHistoryLimit: 5
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: nameserver
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: nameserver
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- imagePullPolicy: IfNotPresent
|
|
||||||
name: nameserver
|
|
||||||
ports:
|
|
||||||
- name: tcp
|
|
||||||
protocol: TCP
|
|
||||||
containerPort: 1053
|
|
||||||
- name: udp
|
|
||||||
protocol: UDP
|
|
||||||
containerPort: 1053
|
|
||||||
volumeMounts:
|
|
||||||
- name: dnsconfig
|
|
||||||
mountPath: /config
|
|
||||||
restartPolicy: Always
|
|
||||||
serviceAccount: nameserver
|
|
||||||
serviceAccountName: nameserver
|
|
||||||
volumes:
|
|
||||||
- name: dnsconfig
|
|
||||||
configMap:
|
|
||||||
name: dnsconfig
|
|
@ -1,6 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: nameserver
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: foo
|
|
@ -1,16 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: nameserver
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: nameserver
|
|
||||||
ports:
|
|
||||||
- name: udp
|
|
||||||
targetPort: 1053
|
|
||||||
port: 53
|
|
||||||
protocol: UDP
|
|
||||||
- name: tcp
|
|
||||||
targetPort: 1053
|
|
||||||
port: 53
|
|
||||||
protocol: TCP
|
|
@ -158,103 +158,6 @@ spec:
|
|||||||
---
|
---
|
||||||
apiVersion: apiextensions.k8s.io/v1
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
kind: CustomResourceDefinition
|
kind: CustomResourceDefinition
|
||||||
metadata:
|
|
||||||
annotations:
|
|
||||||
controller-gen.kubebuilder.io/version: v0.13.0
|
|
||||||
name: dnsconfigs.tailscale.com
|
|
||||||
spec:
|
|
||||||
group: tailscale.com
|
|
||||||
names:
|
|
||||||
kind: DNSConfig
|
|
||||||
listKind: DNSConfigList
|
|
||||||
plural: dnsconfigs
|
|
||||||
shortNames:
|
|
||||||
- dc
|
|
||||||
singular: dnsconfig
|
|
||||||
scope: Cluster
|
|
||||||
versions:
|
|
||||||
- additionalPrinterColumns:
|
|
||||||
- description: Service IP address of the nameserver
|
|
||||||
jsonPath: .status.nameserverStatus.ip
|
|
||||||
name: NameserverIP
|
|
||||||
type: string
|
|
||||||
name: v1alpha1
|
|
||||||
schema:
|
|
||||||
openAPIV3Schema:
|
|
||||||
properties:
|
|
||||||
apiVersion:
|
|
||||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
|
||||||
type: string
|
|
||||||
kind:
|
|
||||||
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
|
||||||
type: string
|
|
||||||
metadata:
|
|
||||||
type: object
|
|
||||||
spec:
|
|
||||||
properties:
|
|
||||||
nameserver:
|
|
||||||
properties:
|
|
||||||
image:
|
|
||||||
properties:
|
|
||||||
repo:
|
|
||||||
type: string
|
|
||||||
tag:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- nameserver
|
|
||||||
type: object
|
|
||||||
status:
|
|
||||||
properties:
|
|
||||||
conditions:
|
|
||||||
items:
|
|
||||||
description: ConnectorCondition contains condition information for a Connector.
|
|
||||||
properties:
|
|
||||||
lastTransitionTime:
|
|
||||||
description: LastTransitionTime is the timestamp corresponding to the last status change of this condition.
|
|
||||||
format: date-time
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
description: Message is a human readable description of the details of the last transition, complementing reason.
|
|
||||||
type: string
|
|
||||||
observedGeneration:
|
|
||||||
description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Connector.
|
|
||||||
format: int64
|
|
||||||
type: integer
|
|
||||||
reason:
|
|
||||||
description: Reason is a brief machine readable explanation for the condition's last transition.
|
|
||||||
type: string
|
|
||||||
status:
|
|
||||||
description: Status of the condition, one of ('True', 'False', 'Unknown').
|
|
||||||
type: string
|
|
||||||
type:
|
|
||||||
description: Type of the condition, known values are (`SubnetRouterReady`).
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
- type
|
|
||||||
type: object
|
|
||||||
type: array
|
|
||||||
x-kubernetes-list-map-keys:
|
|
||||||
- type
|
|
||||||
x-kubernetes-list-type: map
|
|
||||||
nameserverStatus:
|
|
||||||
properties:
|
|
||||||
ip:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- spec
|
|
||||||
type: object
|
|
||||||
served: true
|
|
||||||
storage: true
|
|
||||||
subresources:
|
|
||||||
status: {}
|
|
||||||
---
|
|
||||||
apiVersion: apiextensions.k8s.io/v1
|
|
||||||
kind: CustomResourceDefinition
|
|
||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
controller-gen.kubebuilder.io/version: v0.13.0
|
controller-gen.kubebuilder.io/version: v0.13.0
|
||||||
@ -788,16 +691,6 @@ rules:
|
|||||||
- list
|
- list
|
||||||
- watch
|
- watch
|
||||||
- update
|
- update
|
||||||
- apiGroups:
|
|
||||||
- tailscale.com
|
|
||||||
resources:
|
|
||||||
- dnsconfigs
|
|
||||||
- dnsconfigs/status
|
|
||||||
verbs:
|
|
||||||
- get
|
|
||||||
- list
|
|
||||||
- watch
|
|
||||||
- update
|
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
kind: ClusterRoleBinding
|
kind: ClusterRoleBinding
|
||||||
@ -822,15 +715,12 @@ rules:
|
|||||||
- ""
|
- ""
|
||||||
resources:
|
resources:
|
||||||
- secrets
|
- secrets
|
||||||
- serviceaccounts
|
|
||||||
- configmaps
|
|
||||||
verbs:
|
verbs:
|
||||||
- '*'
|
- '*'
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- apps
|
- apps
|
||||||
resources:
|
resources:
|
||||||
- statefulsets
|
- statefulsets
|
||||||
- deployments
|
|
||||||
verbs:
|
verbs:
|
||||||
- '*'
|
- '*'
|
||||||
---
|
---
|
||||||
|
@ -22,11 +22,9 @@ const (
|
|||||||
operatorDeploymentFilesPath = "cmd/k8s-operator/deploy"
|
operatorDeploymentFilesPath = "cmd/k8s-operator/deploy"
|
||||||
connectorCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_connectors.yaml"
|
connectorCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_connectors.yaml"
|
||||||
proxyClassCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_proxyclasses.yaml"
|
proxyClassCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_proxyclasses.yaml"
|
||||||
dnsConfigCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_dnsconfigs.yaml"
|
|
||||||
helmTemplatesPath = operatorDeploymentFilesPath + "/chart/templates"
|
helmTemplatesPath = operatorDeploymentFilesPath + "/chart/templates"
|
||||||
connectorCRDHelmTemplatePath = helmTemplatesPath + "/connector.yaml"
|
connectorCRDHelmTemplatePath = helmTemplatesPath + "/connector.yaml"
|
||||||
proxyClassCRDHelmTemplatePath = helmTemplatesPath + "/proxyclass.yaml"
|
proxyClassCRDHelmTemplatePath = helmTemplatesPath + "/proxyclass.yaml"
|
||||||
dnsConfigCRDHelmTemplatePath = helmTemplatesPath + "/dnsconfig.yaml"
|
|
||||||
|
|
||||||
helmConditionalStart = "{{ if .Values.installCRDs -}}\n"
|
helmConditionalStart = "{{ if .Values.installCRDs -}}\n"
|
||||||
helmConditionalEnd = "{{- end -}}"
|
helmConditionalEnd = "{{- end -}}"
|
||||||
@ -110,7 +108,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate places tailscale.com CRDs (currently Connector, ProxyClass and DNSConfig) into
|
// generate places tailscale.com CRDs (currently Connector and ProxyClass) into
|
||||||
// the Helm chart templates behind .Values.installCRDs=true condition (true by
|
// the Helm chart templates behind .Values.installCRDs=true condition (true by
|
||||||
// default).
|
// default).
|
||||||
func generate(baseDir string) error {
|
func generate(baseDir string) error {
|
||||||
@ -142,9 +140,6 @@ func generate(baseDir string) error {
|
|||||||
if err := addCRDToHelm(proxyClassCRDPath, proxyClassCRDHelmTemplatePath); err != nil {
|
if err := addCRDToHelm(proxyClassCRDPath, proxyClassCRDHelmTemplatePath); err != nil {
|
||||||
return fmt.Errorf("error adding ProxyClass CRD to Helm templates: %w", err)
|
return fmt.Errorf("error adding ProxyClass CRD to Helm templates: %w", err)
|
||||||
}
|
}
|
||||||
if err := addCRDToHelm(dnsConfigCRDPath, dnsConfigCRDHelmTemplatePath); err != nil {
|
|
||||||
return fmt.Errorf("error adding DNSConfig CRD to Helm templates: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,8 +151,5 @@ func cleanup(baseDir string) error {
|
|||||||
if err := os.Remove(filepath.Join(baseDir, proxyClassCRDHelmTemplatePath)); err != nil && !os.IsNotExist(err) {
|
if err := os.Remove(filepath.Join(baseDir, proxyClassCRDHelmTemplatePath)); err != nil && !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("error cleaning up ProxyClass CRD template: %w", err)
|
return fmt.Errorf("error cleaning up ProxyClass CRD template: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.Remove(filepath.Join(baseDir, dnsConfigCRDHelmTemplatePath)); err != nil && !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("error cleaning up DNSConfig CRD template: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -56,9 +56,6 @@ func Test_generate(t *testing.T) {
|
|||||||
if !strings.Contains(installContentsWithCRD.String(), "name: proxyclasses.tailscale.com") {
|
if !strings.Contains(installContentsWithCRD.String(), "name: proxyclasses.tailscale.com") {
|
||||||
t.Errorf("ProxyClass CRD not found in default chart install")
|
t.Errorf("ProxyClass CRD not found in default chart install")
|
||||||
}
|
}
|
||||||
if !strings.Contains(installContentsWithCRD.String(), "name: dnsconfigs.tailscale.com") {
|
|
||||||
t.Errorf("DNSConfig CRD not found in default chart install")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that CRDs can be excluded from Helm chart install
|
// Test that CRDs can be excluded from Helm chart install
|
||||||
installContentsWithoutCRD := bytes.NewBuffer([]byte{})
|
installContentsWithoutCRD := bytes.NewBuffer([]byte{})
|
||||||
@ -74,7 +71,4 @@ func Test_generate(t *testing.T) {
|
|||||||
if strings.Contains(installContentsWithoutCRD.String(), "name: connectors.tailscale.com") {
|
if strings.Contains(installContentsWithoutCRD.String(), "name: connectors.tailscale.com") {
|
||||||
t.Errorf("ProxyClass CRD found in chart install that should not contain a CRD")
|
t.Errorf("ProxyClass CRD found in chart install that should not contain a CRD")
|
||||||
}
|
}
|
||||||
if strings.Contains(installContentsWithoutCRD.String(), "name: dnsconfigs.tailscale.com") {
|
|
||||||
t.Errorf("DNSConfig CRD found in chart install that should not contain a CRD")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,278 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build !plan9
|
|
||||||
|
|
||||||
// tailscale-operator provides a way to expose services running in a Kubernetes
|
|
||||||
// cluster to your Tailnet and to make Tailscale nodes available to cluster
|
|
||||||
// workloads
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
_ "embed"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
xslices "golang.org/x/exp/slices"
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
"k8s.io/client-go/tools/record"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
||||||
"sigs.k8s.io/yaml"
|
|
||||||
tsoperator "tailscale.com/k8s-operator"
|
|
||||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
|
||||||
"tailscale.com/tstime"
|
|
||||||
"tailscale.com/util/clientmetric"
|
|
||||||
"tailscale.com/util/set"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
reasonNameserverCreationFailed = "NameserverCreationFailed"
|
|
||||||
reasonMultipleDNSConfigsPresent = "MultipleDNSConfigsPresent"
|
|
||||||
|
|
||||||
reasonNameserverCreated = "NameserverCreated"
|
|
||||||
|
|
||||||
messageNameserverCreationFailed = "Failed creating nameserver resources: %v"
|
|
||||||
messageMultipleDNSConfigsPresent = "Multiple DNSConfig resources found in cluster. Please ensure no more than one is present."
|
|
||||||
)
|
|
||||||
|
|
||||||
// NameserverReconciler knows how to create nameserver resources in cluster in
|
|
||||||
// response to users applying DNSConfig.
|
|
||||||
type NameserverReconciler struct {
|
|
||||||
client.Client
|
|
||||||
logger *zap.SugaredLogger
|
|
||||||
recorder record.EventRecorder
|
|
||||||
clock tstime.Clock
|
|
||||||
tsNamespace string
|
|
||||||
|
|
||||||
mu sync.Mutex // protects following
|
|
||||||
managedNameservers set.Slice[types.UID] // one or none
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
gaugeNameserverResources = clientmetric.NewGauge("k8s_nameserver_resources")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *NameserverReconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
|
|
||||||
logger := a.logger.With("dnsConfig", req.Name)
|
|
||||||
logger.Debugf("starting reconcile")
|
|
||||||
defer logger.Debugf("reconcile finished")
|
|
||||||
|
|
||||||
var dnsCfg tsapi.DNSConfig
|
|
||||||
err = a.Get(ctx, req.NamespacedName, &dnsCfg)
|
|
||||||
if apierrors.IsNotFound(err) {
|
|
||||||
// Request object not found, could have been deleted after reconcile request.
|
|
||||||
logger.Debugf("dnsconfig not found, assuming it was deleted")
|
|
||||||
return reconcile.Result{}, nil
|
|
||||||
} else if err != nil {
|
|
||||||
return reconcile.Result{}, fmt.Errorf("failed to get dnsconfig: %w", err)
|
|
||||||
}
|
|
||||||
if !dnsCfg.DeletionTimestamp.IsZero() {
|
|
||||||
ix := xslices.Index(dnsCfg.Finalizers, FinalizerName)
|
|
||||||
if ix < 0 {
|
|
||||||
logger.Debugf("no finalizer, nothing to do")
|
|
||||||
return reconcile.Result{}, nil
|
|
||||||
}
|
|
||||||
logger.Info("Cleaning up DNSConfig resources")
|
|
||||||
if err := a.maybeCleanup(ctx, &dnsCfg, logger); err != nil {
|
|
||||||
logger.Errorf("error cleaning up reconciler resource: %v", err)
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
dnsCfg.Finalizers = append(dnsCfg.Finalizers[:ix], dnsCfg.Finalizers[ix+1:]...)
|
|
||||||
if err := a.Update(ctx, &dnsCfg); err != nil {
|
|
||||||
logger.Errorf("error removing finalizer: %v", err)
|
|
||||||
return reconcile.Result{}, err
|
|
||||||
}
|
|
||||||
logger.Infof("Nameserver resources cleaned up")
|
|
||||||
return reconcile.Result{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
oldCnStatus := dnsCfg.Status.DeepCopy()
|
|
||||||
setStatus := func(dnsCfg *tsapi.DNSConfig, conditionType tsapi.ConnectorConditionType, status metav1.ConditionStatus, reason, message string) (reconcile.Result, error) {
|
|
||||||
tsoperator.SetDNSConfigCondition(dnsCfg, tsapi.NameserverReady, status, reason, message, dnsCfg.Generation, a.clock, logger)
|
|
||||||
if !apiequality.Semantic.DeepEqual(oldCnStatus, dnsCfg.Status) {
|
|
||||||
// An error encountered here should get returned by the Reconcile function.
|
|
||||||
if updateErr := a.Client.Status().Update(ctx, dnsCfg); updateErr != nil {
|
|
||||||
err = errors.Wrap(err, updateErr.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
var dnsCfgs tsapi.DNSConfigList
|
|
||||||
if err := a.List(ctx, &dnsCfgs); err != nil {
|
|
||||||
return res, fmt.Errorf("error listing DNSConfigs: %w", err)
|
|
||||||
}
|
|
||||||
if len(dnsCfgs.Items) > 1 { // enforce DNSConfig to be a singleton
|
|
||||||
msg := "invalid cluster configuration: more than one tailscale.com/dnsconfigs found. Please ensure that no more than one is created."
|
|
||||||
logger.Error(msg)
|
|
||||||
a.recorder.Event(&dnsCfg, corev1.EventTypeWarning, reasonMultipleDNSConfigsPresent, messageMultipleDNSConfigsPresent)
|
|
||||||
setStatus(&dnsCfg, tsapi.NameserverReady, metav1.ConditionFalse, reasonMultipleDNSConfigsPresent, messageMultipleDNSConfigsPresent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !slices.Contains(dnsCfg.Finalizers, FinalizerName) {
|
|
||||||
logger.Infof("ensuring nameserver resources")
|
|
||||||
dnsCfg.Finalizers = append(dnsCfg.Finalizers, FinalizerName)
|
|
||||||
if err := a.Update(ctx, &dnsCfg); err != nil {
|
|
||||||
msg := fmt.Sprintf(messageNameserverCreationFailed, err)
|
|
||||||
logger.Error(msg)
|
|
||||||
return setStatus(&dnsCfg, tsapi.NameserverReady, metav1.ConditionFalse, reasonNameserverCreationFailed, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := a.maybeProvision(ctx, &dnsCfg, logger); err != nil {
|
|
||||||
return reconcile.Result{}, fmt.Errorf("error provisioning nameserver resources: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.mu.Lock()
|
|
||||||
a.managedNameservers.Add(dnsCfg.UID)
|
|
||||||
a.mu.Unlock()
|
|
||||||
gaugeNameserverResources.Set(int64(a.managedNameservers.Len()))
|
|
||||||
|
|
||||||
svc := &corev1.Service{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "nameserver", Namespace: a.tsNamespace},
|
|
||||||
}
|
|
||||||
if err := a.Client.Get(ctx, client.ObjectKeyFromObject(svc), svc); err != nil {
|
|
||||||
return res, fmt.Errorf("error getting Service: %w", err)
|
|
||||||
}
|
|
||||||
if ip := svc.Spec.ClusterIP; ip != "" && ip != "None" {
|
|
||||||
dnsCfg.Status.NameserverStatus = &tsapi.NameserverStatus{
|
|
||||||
IP: ip,
|
|
||||||
}
|
|
||||||
return setStatus(&dnsCfg, tsapi.NameserverReady, metav1.ConditionTrue, reasonNameserverCreated, reasonNameserverCreated)
|
|
||||||
}
|
|
||||||
logger.Info("nameserver Service does not have an IP address allocated, waiting...")
|
|
||||||
return reconcile.Result{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func nameserverResourceLabels(name, namespace string) map[string]string {
|
|
||||||
labels := childResourceLabels(name, namespace, "nameserver")
|
|
||||||
labels["app.kubernetes.io/name"] = "tailscale"
|
|
||||||
labels["app.kubernetes.io/component"] = "nameserver"
|
|
||||||
return labels
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *NameserverReconciler) maybeProvision(ctx context.Context, tsDNSCfg *tsapi.DNSConfig, logger *zap.SugaredLogger) error {
|
|
||||||
labels := nameserverResourceLabels(tsDNSCfg.Name, a.tsNamespace)
|
|
||||||
dCfg := &deployConfig{
|
|
||||||
ownerRefs: []metav1.OwnerReference{*metav1.NewControllerRef(tsDNSCfg, tsapi.SchemeGroupVersion.WithKind("DNSConfig"))},
|
|
||||||
namespace: a.tsNamespace,
|
|
||||||
labels: labels,
|
|
||||||
}
|
|
||||||
if tsDNSCfg.Spec.Nameserver.Image.Repo != "" {
|
|
||||||
dCfg.imageRepo = tsDNSCfg.Spec.Nameserver.Image.Repo
|
|
||||||
}
|
|
||||||
if tsDNSCfg.Spec.Nameserver.Image.Tag != "" {
|
|
||||||
dCfg.imageTag = tsDNSCfg.Spec.Nameserver.Image.Tag
|
|
||||||
}
|
|
||||||
for _, deployable := range []deployable{saDeployable, deployDeployable, svcDeployable, cmDeployable} {
|
|
||||||
if err := deployable.updateObj(ctx, dCfg, a.Client); err != nil {
|
|
||||||
return fmt.Errorf("error reconciling %s: %w", deployable.kind, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// maybeCleanup removes DNSConfig from being tracked. The cluster resources
|
|
||||||
// created, will be automatically garbage collected as they are owned by the
|
|
||||||
// DNSConfig.
|
|
||||||
func (a *NameserverReconciler) maybeCleanup(ctx context.Context, dnsCfg *tsapi.DNSConfig, logger *zap.SugaredLogger) error {
|
|
||||||
a.mu.Lock()
|
|
||||||
a.managedNameservers.Remove(dnsCfg.UID)
|
|
||||||
a.mu.Unlock()
|
|
||||||
gaugeNameserverResources.Set(int64(a.managedNameservers.Len()))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type deployable struct {
|
|
||||||
kind string
|
|
||||||
updateObj func(context.Context, *deployConfig, client.Client) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type deployConfig struct {
|
|
||||||
imageRepo string
|
|
||||||
imageTag string
|
|
||||||
labels map[string]string
|
|
||||||
ownerRefs []metav1.OwnerReference
|
|
||||||
namespace string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
//go:embed deploy/manifests/nameserver/cm.yaml
|
|
||||||
cmYaml []byte
|
|
||||||
//go:embed deploy/manifests/nameserver/deploy.yaml
|
|
||||||
deployYaml []byte
|
|
||||||
//go:embed deploy/manifests/nameserver/sa.yaml
|
|
||||||
saYaml []byte
|
|
||||||
//go:embed deploy/manifests/nameserver/svc.yaml
|
|
||||||
svcYaml []byte
|
|
||||||
|
|
||||||
deployDeployable = deployable{
|
|
||||||
kind: "Deployment",
|
|
||||||
updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
|
|
||||||
d := new(appsv1.Deployment)
|
|
||||||
if err := yaml.Unmarshal(deployYaml, &d); err != nil {
|
|
||||||
return fmt.Errorf("error unmarshalling Deployment yaml: %w", err)
|
|
||||||
}
|
|
||||||
d.Spec.Template.Spec.Containers[0].Image = fmt.Sprintf("%s:%s", cfg.imageRepo, cfg.imageTag)
|
|
||||||
d.ObjectMeta.Namespace = cfg.namespace
|
|
||||||
d.ObjectMeta.Labels = cfg.labels
|
|
||||||
d.ObjectMeta.OwnerReferences = cfg.ownerRefs
|
|
||||||
updateF := func(oldD *appsv1.Deployment) {
|
|
||||||
oldD.Spec = d.Spec
|
|
||||||
}
|
|
||||||
_, err := createOrUpdate[appsv1.Deployment](ctx, kubeClient, cfg.namespace, d, updateF)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
saDeployable = deployable{
|
|
||||||
kind: "ServiceAccount",
|
|
||||||
updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
|
|
||||||
sa := new(corev1.ServiceAccount)
|
|
||||||
if err := yaml.Unmarshal(saYaml, &sa); err != nil {
|
|
||||||
return fmt.Errorf("error unmarshalling ServiceAccount yaml: %w", err)
|
|
||||||
}
|
|
||||||
sa.ObjectMeta.Labels = cfg.labels
|
|
||||||
sa.ObjectMeta.OwnerReferences = cfg.ownerRefs
|
|
||||||
sa.ObjectMeta.Namespace = cfg.namespace
|
|
||||||
_, err := createOrUpdate(ctx, kubeClient, cfg.namespace, sa, func(*corev1.ServiceAccount) {})
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
svcDeployable = deployable{
|
|
||||||
kind: "Service",
|
|
||||||
updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
|
|
||||||
svc := new(corev1.Service)
|
|
||||||
if err := yaml.Unmarshal(svcYaml, &svc); err != nil {
|
|
||||||
return fmt.Errorf("error unmarshalling Service yaml: %w", err)
|
|
||||||
}
|
|
||||||
svc.ObjectMeta.Labels = cfg.labels
|
|
||||||
svc.ObjectMeta.OwnerReferences = cfg.ownerRefs
|
|
||||||
svc.ObjectMeta.Namespace = cfg.namespace
|
|
||||||
_, err := createOrUpdate[corev1.Service](ctx, kubeClient, cfg.namespace, svc, func(*corev1.Service) {})
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
cmDeployable = deployable{
|
|
||||||
kind: "ConfigMap",
|
|
||||||
updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
|
|
||||||
cm := new(corev1.ConfigMap)
|
|
||||||
if err := yaml.Unmarshal(cmYaml, &cm); err != nil {
|
|
||||||
return fmt.Errorf("error unmarshalling ConfigMap yaml: %w", err)
|
|
||||||
}
|
|
||||||
cm.ObjectMeta.Labels = cfg.labels
|
|
||||||
cm.ObjectMeta.OwnerReferences = cfg.ownerRefs
|
|
||||||
cm.ObjectMeta.Namespace = cfg.namespace
|
|
||||||
_, err := createOrUpdate[corev1.ConfigMap](ctx, kubeClient, cfg.namespace, cm, func(cm *corev1.ConfigMap) {})
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,118 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build !plan9
|
|
||||||
|
|
||||||
// tailscale-operator provides a way to expose services running in a Kubernetes
|
|
||||||
// cluster to your Tailnet and to make Tailscale nodes available to cluster
|
|
||||||
// workloads
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
||||||
"sigs.k8s.io/yaml"
|
|
||||||
operatorutils "tailscale.com/k8s-operator"
|
|
||||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
|
||||||
"tailscale.com/tstest"
|
|
||||||
"tailscale.com/util/mak"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNameserverReconciler(t *testing.T) {
|
|
||||||
dnsCfg := &tsapi.DNSConfig{
|
|
||||||
TypeMeta: metav1.TypeMeta{Kind: "DNSConfig", APIVersion: "tailscale.com/v1alpha1"},
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "test",
|
|
||||||
},
|
|
||||||
Spec: tsapi.DNSConfigSpec{
|
|
||||||
Nameserver: &tsapi.Nameserver{
|
|
||||||
Image: &tsapi.Image{
|
|
||||||
Repo: "test",
|
|
||||||
Tag: "v0.0.1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
fc := fake.NewClientBuilder().
|
|
||||||
WithScheme(tsapi.GlobalScheme).
|
|
||||||
WithObjects(dnsCfg).
|
|
||||||
WithStatusSubresource(dnsCfg).
|
|
||||||
Build()
|
|
||||||
zl, err := zap.NewDevelopment()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
cl := tstest.NewClock(tstest.ClockOpts{})
|
|
||||||
nr := &NameserverReconciler{
|
|
||||||
Client: fc,
|
|
||||||
clock: cl,
|
|
||||||
logger: zl.Sugar(),
|
|
||||||
tsNamespace: "tailscale",
|
|
||||||
}
|
|
||||||
expectReconciled(t, nr, "", "test")
|
|
||||||
// Verify that nameserver Deployment has been created and has the expected fields.
|
|
||||||
wantsDeploy := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "nameserver", Namespace: "tailscale"}, TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: appsv1.SchemeGroupVersion.Identifier()}}
|
|
||||||
if err := yaml.Unmarshal(deployYaml, wantsDeploy); err != nil {
|
|
||||||
t.Fatalf("unmarshalling yaml: %v", err)
|
|
||||||
}
|
|
||||||
dnsCfgOwnerRef := metav1.NewControllerRef(dnsCfg, tsapi.SchemeGroupVersion.WithKind("DNSConfig"))
|
|
||||||
wantsDeploy.OwnerReferences = []metav1.OwnerReference{*dnsCfgOwnerRef}
|
|
||||||
wantsDeploy.Spec.Template.Spec.Containers[0].Image = "test:v0.0.1"
|
|
||||||
wantsDeploy.Namespace = "tailscale"
|
|
||||||
labels := nameserverResourceLabels("test", "tailscale")
|
|
||||||
wantsDeploy.ObjectMeta.Labels = labels
|
|
||||||
expectEqual(t, fc, wantsDeploy, nil)
|
|
||||||
|
|
||||||
// Verify that DNSConfig advertizes the nameserver's Service IP address,
|
|
||||||
// has the ready status condition and tailscale finalizer.
|
|
||||||
mustUpdate(t, fc, "tailscale", "nameserver", func(svc *corev1.Service) {
|
|
||||||
svc.Spec.ClusterIP = "1.2.3.4"
|
|
||||||
})
|
|
||||||
expectReconciled(t, nr, "", "test")
|
|
||||||
dnsCfg.Status.NameserverStatus = &tsapi.NameserverStatus{
|
|
||||||
IP: "1.2.3.4",
|
|
||||||
}
|
|
||||||
dnsCfg.Finalizers = []string{FinalizerName}
|
|
||||||
dnsCfg.Status.Conditions = append(dnsCfg.Status.Conditions, tsapi.ConnectorCondition{
|
|
||||||
Type: tsapi.NameserverReady,
|
|
||||||
Status: metav1.ConditionTrue,
|
|
||||||
Reason: reasonNameserverCreated,
|
|
||||||
Message: reasonNameserverCreated,
|
|
||||||
LastTransitionTime: &metav1.Time{Time: cl.Now().Truncate(time.Second)},
|
|
||||||
})
|
|
||||||
expectEqual(t, fc, dnsCfg, nil)
|
|
||||||
|
|
||||||
// // Verify that nameserver image gets updated to match DNSConfig spec.
|
|
||||||
mustUpdate(t, fc, "", "test", func(dnsCfg *tsapi.DNSConfig) {
|
|
||||||
dnsCfg.Spec.Nameserver.Image.Tag = "v0.0.2"
|
|
||||||
})
|
|
||||||
expectReconciled(t, nr, "", "test")
|
|
||||||
wantsDeploy.Spec.Template.Spec.Containers[0].Image = "test:v0.0.2"
|
|
||||||
expectEqual(t, fc, wantsDeploy, nil)
|
|
||||||
|
|
||||||
// Verify that when another actor sets ConfigMap data, it does not get
|
|
||||||
// overwritten by nameserver reconciler.
|
|
||||||
dnsRecords := &operatorutils.Records{Version: "v1alpha1", IP4: map[string][]string{"foo.ts.net": {"1.2.3.4"}}}
|
|
||||||
bs, err := json.Marshal(dnsRecords)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error marshalling ConfigMap contents: %v", err)
|
|
||||||
}
|
|
||||||
mustUpdate(t, fc, "tailscale", "dnsconfig", func(cm *corev1.ConfigMap) {
|
|
||||||
mak.Set(&cm.Data, "dns.json", string(bs))
|
|
||||||
})
|
|
||||||
expectReconciled(t, nr, "", "test")
|
|
||||||
wantCm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "dnsconfig",
|
|
||||||
Namespace: "tailscale", Labels: labels, OwnerReferences: []metav1.OwnerReference{*dnsCfgOwnerRef}},
|
|
||||||
TypeMeta: metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"},
|
|
||||||
Data: map[string]string{"dns.json": string(bs)},
|
|
||||||
}
|
|
||||||
expectEqual(t, fc, wantCm, nil)
|
|
||||||
}
|
|
@ -223,11 +223,8 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
|||||||
// resources that we GET via the controller manager's client.
|
// resources that we GET via the controller manager's client.
|
||||||
Cache: cache.Options{
|
Cache: cache.Options{
|
||||||
ByObject: map[client.Object]cache.ByObject{
|
ByObject: map[client.Object]cache.ByObject{
|
||||||
&corev1.Secret{}: nsFilter,
|
&corev1.Secret{}: nsFilter,
|
||||||
&corev1.ServiceAccount{}: nsFilter,
|
&appsv1.StatefulSet{}: nsFilter,
|
||||||
&corev1.ConfigMap{}: nsFilter,
|
|
||||||
&appsv1.StatefulSet{}: nsFilter,
|
|
||||||
&appsv1.Deployment{}: nsFilter,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Scheme: tsapi.GlobalScheme,
|
Scheme: tsapi.GlobalScheme,
|
||||||
@ -311,28 +308,7 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
|||||||
clock: tstime.DefaultClock{},
|
clock: tstime.DefaultClock{},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
startlog.Fatalf("could not create connector reconciler: %v", err)
|
startlog.Fatal("could not create connector reconciler: %v", err)
|
||||||
}
|
|
||||||
// TODO (irbekrm): switch to metadata-only watches for resources whose
|
|
||||||
// spec we don't need to inspect to reduce memory consumption
|
|
||||||
// https://github.com/kubernetes-sigs/controller-runtime/issues/1159
|
|
||||||
nameserverFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("nameserver"))
|
|
||||||
err = builder.ControllerManagedBy(mgr).
|
|
||||||
For(&tsapi.DNSConfig{}).
|
|
||||||
Watches(&appsv1.Deployment{}, nameserverFilter).
|
|
||||||
Watches(&corev1.ConfigMap{}, nameserverFilter).
|
|
||||||
Watches(&corev1.Service{}, nameserverFilter).
|
|
||||||
Watches(&corev1.ServiceAccount{}, nameserverFilter).
|
|
||||||
Complete(&NameserverReconciler{
|
|
||||||
recorder: eventRecorder,
|
|
||||||
tsNamespace: tsNamespace,
|
|
||||||
|
|
||||||
Client: mgr.GetClient(),
|
|
||||||
logger: zlog.Named("nameserver-reconciler"),
|
|
||||||
clock: tstime.DefaultClock{},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
startlog.Fatalf("could not create nameserver reconciler: %v", err)
|
|
||||||
}
|
}
|
||||||
err = builder.ControllerManagedBy(mgr).
|
err = builder.ControllerManagedBy(mgr).
|
||||||
For(&tsapi.ProxyClass{}).
|
For(&tsapi.ProxyClass{}).
|
||||||
|
@ -1194,6 +1194,7 @@ func TestTailscaledConfigfileHash(t *testing.T) {
|
|||||||
expectReconciled(t, sr, "default", "test")
|
expectReconciled(t, sr, "default", "test")
|
||||||
expectEqual(t, fc, expectedSTS(t, fc, o), nil)
|
expectEqual(t, fc, expectedSTS(t, fc, o), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_isMagicDNSName(t *testing.T) {
|
func Test_isMagicDNSName(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
in string
|
in string
|
||||||
|
@ -10,8 +10,6 @@ Resource Types:
|
|||||||
|
|
||||||
- [Connector](#connector)
|
- [Connector](#connector)
|
||||||
|
|
||||||
- [DNSConfig](#dnsconfig)
|
|
||||||
|
|
||||||
- [ProxyClass](#proxyclass)
|
- [ProxyClass](#proxyclass)
|
||||||
|
|
||||||
|
|
||||||
@ -261,274 +259,6 @@ ConnectorCondition contains condition information for a Connector.
|
|||||||
</tr></tbody>
|
</tr></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## DNSConfig
|
|
||||||
<sup><sup>[↩ Parent](#tailscalecomv1alpha1 )</sup></sup>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Required</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody><tr>
|
|
||||||
<td><b>apiVersion</b></td>
|
|
||||||
<td>string</td>
|
|
||||||
<td>tailscale.com/v1alpha1</td>
|
|
||||||
<td>true</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><b>kind</b></td>
|
|
||||||
<td>string</td>
|
|
||||||
<td>DNSConfig</td>
|
|
||||||
<td>true</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><b><a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#objectmeta-v1-meta">metadata</a></b></td>
|
|
||||||
<td>object</td>
|
|
||||||
<td>Refer to the Kubernetes API documentation for the fields of the `metadata` field.</td>
|
|
||||||
<td>true</td>
|
|
||||||
</tr><tr>
|
|
||||||
<td><b><a href="#dnsconfigspec">spec</a></b></td>
|
|
||||||
<td>object</td>
|
|
||||||
<td>
|
|
||||||
<br/>
|
|
||||||
</td>
|
|
||||||
<td>true</td>
|
|
||||||
</tr><tr>
|
|
||||||
<td><b><a href="#dnsconfigstatus">status</a></b></td>
|
|
||||||
<td>object</td>
|
|
||||||
<td>
|
|
||||||
<br/>
|
|
||||||
</td>
|
|
||||||
<td>false</td>
|
|
||||||
</tr></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
### DNSConfig.spec
|
|
||||||
<sup><sup>[↩ Parent](#dnsconfig)</sup></sup>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Required</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody><tr>
|
|
||||||
<td><b><a href="#dnsconfigspecnameserver">nameserver</a></b></td>
|
|
||||||
<td>object</td>
|
|
||||||
<td>
|
|
||||||
<br/>
|
|
||||||
</td>
|
|
||||||
<td>true</td>
|
|
||||||
</tr></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
### DNSConfig.spec.nameserver
|
|
||||||
<sup><sup>[↩ Parent](#dnsconfigspec)</sup></sup>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Required</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody><tr>
|
|
||||||
<td><b><a href="#dnsconfigspecnameserverimage">image</a></b></td>
|
|
||||||
<td>object</td>
|
|
||||||
<td>
|
|
||||||
<br/>
|
|
||||||
</td>
|
|
||||||
<td>false</td>
|
|
||||||
</tr></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
### DNSConfig.spec.nameserver.image
|
|
||||||
<sup><sup>[↩ Parent](#dnsconfigspecnameserver)</sup></sup>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Required</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody><tr>
|
|
||||||
<td><b>repo</b></td>
|
|
||||||
<td>string</td>
|
|
||||||
<td>
|
|
||||||
<br/>
|
|
||||||
</td>
|
|
||||||
<td>false</td>
|
|
||||||
</tr><tr>
|
|
||||||
<td><b>tag</b></td>
|
|
||||||
<td>string</td>
|
|
||||||
<td>
|
|
||||||
<br/>
|
|
||||||
</td>
|
|
||||||
<td>false</td>
|
|
||||||
</tr></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
### DNSConfig.status
|
|
||||||
<sup><sup>[↩ Parent](#dnsconfig)</sup></sup>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Required</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody><tr>
|
|
||||||
<td><b><a href="#dnsconfigstatusconditionsindex">conditions</a></b></td>
|
|
||||||
<td>[]object</td>
|
|
||||||
<td>
|
|
||||||
<br/>
|
|
||||||
</td>
|
|
||||||
<td>false</td>
|
|
||||||
</tr><tr>
|
|
||||||
<td><b><a href="#dnsconfigstatusnameserverstatus">nameserverStatus</a></b></td>
|
|
||||||
<td>object</td>
|
|
||||||
<td>
|
|
||||||
<br/>
|
|
||||||
</td>
|
|
||||||
<td>false</td>
|
|
||||||
</tr></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
### DNSConfig.status.conditions[index]
|
|
||||||
<sup><sup>[↩ Parent](#dnsconfigstatus)</sup></sup>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ConnectorCondition contains condition information for a Connector.
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Required</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody><tr>
|
|
||||||
<td><b>status</b></td>
|
|
||||||
<td>string</td>
|
|
||||||
<td>
|
|
||||||
Status of the condition, one of ('True', 'False', 'Unknown').<br/>
|
|
||||||
</td>
|
|
||||||
<td>true</td>
|
|
||||||
</tr><tr>
|
|
||||||
<td><b>type</b></td>
|
|
||||||
<td>string</td>
|
|
||||||
<td>
|
|
||||||
Type of the condition, known values are (`SubnetRouterReady`).<br/>
|
|
||||||
</td>
|
|
||||||
<td>true</td>
|
|
||||||
</tr><tr>
|
|
||||||
<td><b>lastTransitionTime</b></td>
|
|
||||||
<td>string</td>
|
|
||||||
<td>
|
|
||||||
LastTransitionTime is the timestamp corresponding to the last status change of this condition.<br/>
|
|
||||||
<br/>
|
|
||||||
<i>Format</i>: date-time<br/>
|
|
||||||
</td>
|
|
||||||
<td>false</td>
|
|
||||||
</tr><tr>
|
|
||||||
<td><b>message</b></td>
|
|
||||||
<td>string</td>
|
|
||||||
<td>
|
|
||||||
Message is a human readable description of the details of the last transition, complementing reason.<br/>
|
|
||||||
</td>
|
|
||||||
<td>false</td>
|
|
||||||
</tr><tr>
|
|
||||||
<td><b>observedGeneration</b></td>
|
|
||||||
<td>integer</td>
|
|
||||||
<td>
|
|
||||||
If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Connector.<br/>
|
|
||||||
<br/>
|
|
||||||
<i>Format</i>: int64<br/>
|
|
||||||
</td>
|
|
||||||
<td>false</td>
|
|
||||||
</tr><tr>
|
|
||||||
<td><b>reason</b></td>
|
|
||||||
<td>string</td>
|
|
||||||
<td>
|
|
||||||
Reason is a brief machine readable explanation for the condition's last transition.<br/>
|
|
||||||
</td>
|
|
||||||
<td>false</td>
|
|
||||||
</tr></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
### DNSConfig.status.nameserverStatus
|
|
||||||
<sup><sup>[↩ Parent](#dnsconfigstatus)</sup></sup>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Required</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody><tr>
|
|
||||||
<td><b>ip</b></td>
|
|
||||||
<td>string</td>
|
|
||||||
<td>
|
|
||||||
<br/>
|
|
||||||
</td>
|
|
||||||
<td>false</td>
|
|
||||||
</tr></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## ProxyClass
|
## ProxyClass
|
||||||
<sup><sup>[↩ Parent](#tailscalecomv1alpha1 )</sup></sup>
|
<sup><sup>[↩ Parent](#tailscalecomv1alpha1 )</sup></sup>
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ func init() {
|
|||||||
|
|
||||||
// Adds the list of known types to api.Scheme.
|
// Adds the list of known types to api.Scheme.
|
||||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||||
scheme.AddKnownTypes(SchemeGroupVersion, &Connector{}, &ConnectorList{}, &ProxyClass{}, &ProxyClassList{}, &DNSConfig{}, &DNSConfigList{})
|
scheme.AddKnownTypes(SchemeGroupVersion, &Connector{}, &ConnectorList{}, &ProxyClass{}, &ProxyClassList{})
|
||||||
|
|
||||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||||
return nil
|
return nil
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build !plan9
|
|
||||||
|
|
||||||
package v1alpha1
|
|
||||||
|
|
||||||
import (
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Code comments on these types should be treated as user facing documentation-
|
|
||||||
// they will appear on the DNSConfig CRD i.e if someone runs kubectl explain dnsconfig.
|
|
||||||
|
|
||||||
var DNSConfigKind = "DNSConfig"
|
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
|
||||||
// +kubebuilder:subresource:status
|
|
||||||
// +kubebuilder:resource:scope=Cluster,shortName=dc
|
|
||||||
// +kubebuilder:printcolumn:name="NameserverIP",type="string",JSONPath=`.status.nameserverStatus.ip`,description="Service IP address of the nameserver"
|
|
||||||
|
|
||||||
type DNSConfig struct {
|
|
||||||
metav1.TypeMeta `json:",inline"`
|
|
||||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
|
||||||
|
|
||||||
Spec DNSConfigSpec `json:"spec"`
|
|
||||||
|
|
||||||
// +optional
|
|
||||||
Status DNSConfigStatus `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
|
||||||
|
|
||||||
type DNSConfigList struct {
|
|
||||||
metav1.TypeMeta `json:",inline"`
|
|
||||||
metav1.ListMeta `json:"metadata"`
|
|
||||||
|
|
||||||
Items []DNSConfig `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSConfigSpec struct {
|
|
||||||
Nameserver *Nameserver `json:"nameserver"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Nameserver struct {
|
|
||||||
// +optional
|
|
||||||
Image *Image `json:"image,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Image struct {
|
|
||||||
// +optional
|
|
||||||
Repo string `json:"repo,omitempty"`
|
|
||||||
// +optional
|
|
||||||
Tag string `json:"tag,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSConfigStatus struct {
|
|
||||||
// +listType=map
|
|
||||||
// +listMapKey=type
|
|
||||||
// +optional
|
|
||||||
Conditions []ConnectorCondition `json:"conditions"`
|
|
||||||
// +optional
|
|
||||||
NameserverStatus *NameserverStatus `json:"nameserverStatus"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NameserverStatus struct {
|
|
||||||
// +optional
|
|
||||||
IP string `json:"ip"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const NameserverReady ConnectorConditionType = `NameserverReady`
|
|
@ -158,162 +158,6 @@ func (in *Container) DeepCopy() *Container {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
|
||||||
func (in *DNSConfig) DeepCopyInto(out *DNSConfig) {
|
|
||||||
*out = *in
|
|
||||||
out.TypeMeta = in.TypeMeta
|
|
||||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
|
||||||
in.Spec.DeepCopyInto(&out.Spec)
|
|
||||||
in.Status.DeepCopyInto(&out.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSConfig.
|
|
||||||
func (in *DNSConfig) DeepCopy() *DNSConfig {
|
|
||||||
if in == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := new(DNSConfig)
|
|
||||||
in.DeepCopyInto(out)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
|
||||||
func (in *DNSConfig) DeepCopyObject() runtime.Object {
|
|
||||||
if c := in.DeepCopy(); c != nil {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
|
||||||
func (in *DNSConfigList) DeepCopyInto(out *DNSConfigList) {
|
|
||||||
*out = *in
|
|
||||||
out.TypeMeta = in.TypeMeta
|
|
||||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
|
||||||
if in.Items != nil {
|
|
||||||
in, out := &in.Items, &out.Items
|
|
||||||
*out = make([]DNSConfig, len(*in))
|
|
||||||
for i := range *in {
|
|
||||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSConfigList.
|
|
||||||
func (in *DNSConfigList) DeepCopy() *DNSConfigList {
|
|
||||||
if in == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := new(DNSConfigList)
|
|
||||||
in.DeepCopyInto(out)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
|
||||||
func (in *DNSConfigList) DeepCopyObject() runtime.Object {
|
|
||||||
if c := in.DeepCopy(); c != nil {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
|
||||||
func (in *DNSConfigSpec) DeepCopyInto(out *DNSConfigSpec) {
|
|
||||||
*out = *in
|
|
||||||
if in.Nameserver != nil {
|
|
||||||
in, out := &in.Nameserver, &out.Nameserver
|
|
||||||
*out = new(Nameserver)
|
|
||||||
(*in).DeepCopyInto(*out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSConfigSpec.
|
|
||||||
func (in *DNSConfigSpec) DeepCopy() *DNSConfigSpec {
|
|
||||||
if in == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := new(DNSConfigSpec)
|
|
||||||
in.DeepCopyInto(out)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
|
||||||
func (in *DNSConfigStatus) DeepCopyInto(out *DNSConfigStatus) {
|
|
||||||
*out = *in
|
|
||||||
if in.Conditions != nil {
|
|
||||||
in, out := &in.Conditions, &out.Conditions
|
|
||||||
*out = make([]ConnectorCondition, len(*in))
|
|
||||||
for i := range *in {
|
|
||||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if in.NameserverStatus != nil {
|
|
||||||
in, out := &in.NameserverStatus, &out.NameserverStatus
|
|
||||||
*out = new(NameserverStatus)
|
|
||||||
**out = **in
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSConfigStatus.
|
|
||||||
func (in *DNSConfigStatus) DeepCopy() *DNSConfigStatus {
|
|
||||||
if in == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := new(DNSConfigStatus)
|
|
||||||
in.DeepCopyInto(out)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
|
||||||
func (in *Image) DeepCopyInto(out *Image) {
|
|
||||||
*out = *in
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Image.
|
|
||||||
func (in *Image) DeepCopy() *Image {
|
|
||||||
if in == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := new(Image)
|
|
||||||
in.DeepCopyInto(out)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
|
||||||
func (in *Nameserver) DeepCopyInto(out *Nameserver) {
|
|
||||||
*out = *in
|
|
||||||
if in.Image != nil {
|
|
||||||
in, out := &in.Image, &out.Image
|
|
||||||
*out = new(Image)
|
|
||||||
**out = **in
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Nameserver.
|
|
||||||
func (in *Nameserver) DeepCopy() *Nameserver {
|
|
||||||
if in == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := new(Nameserver)
|
|
||||||
in.DeepCopyInto(out)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
|
||||||
func (in *NameserverStatus) DeepCopyInto(out *NameserverStatus) {
|
|
||||||
*out = *in
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NameserverStatus.
|
|
||||||
func (in *NameserverStatus) DeepCopy() *NameserverStatus {
|
|
||||||
if in == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := new(NameserverStatus)
|
|
||||||
in.DeepCopyInto(out)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *Pod) DeepCopyInto(out *Pod) {
|
func (in *Pod) DeepCopyInto(out *Pod) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -24,7 +24,7 @@ func SetConnectorCondition(cn *tsapi.Connector, conditionType tsapi.ConnectorCon
|
|||||||
cn.Status.Conditions = conds
|
cn.Status.Conditions = conds
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveConnectorCondition will remove condition of the given type if it exists.
|
// RemoveConnectorCondition will remove condition of the given type.
|
||||||
func RemoveConnectorCondition(conn *tsapi.Connector, conditionType tsapi.ConnectorConditionType) {
|
func RemoveConnectorCondition(conn *tsapi.Connector, conditionType tsapi.ConnectorConditionType) {
|
||||||
conn.Status.Conditions = slices.DeleteFunc(conn.Status.Conditions, func(cond tsapi.ConnectorCondition) bool {
|
conn.Status.Conditions = slices.DeleteFunc(conn.Status.Conditions, func(cond tsapi.ConnectorCondition) bool {
|
||||||
return cond.Type == conditionType
|
return cond.Type == conditionType
|
||||||
@ -39,14 +39,6 @@ func SetProxyClassCondition(pc *tsapi.ProxyClass, conditionType tsapi.ConnectorC
|
|||||||
pc.Status.Conditions = conds
|
pc.Status.Conditions = conds
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDNSConfigCondition ensures that DNSConfig status has a condition with the
|
|
||||||
// given attributes. LastTransitionTime gets set every time condition's status
|
|
||||||
// changes
|
|
||||||
func SetDNSConfigCondition(dnsCfg *tsapi.DNSConfig, conditionType tsapi.ConnectorConditionType, status metav1.ConditionStatus, reason, message string, gen int64, clock tstime.Clock, logger *zap.SugaredLogger) {
|
|
||||||
conds := updateCondition(dnsCfg.Status.Conditions, conditionType, status, reason, message, gen, clock, logger)
|
|
||||||
dnsCfg.Status.Conditions = conds
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateCondition(conds []tsapi.ConnectorCondition, conditionType tsapi.ConnectorConditionType, status metav1.ConditionStatus, reason, message string, gen int64, clock tstime.Clock, logger *zap.SugaredLogger) []tsapi.ConnectorCondition {
|
func updateCondition(conds []tsapi.ConnectorCondition, conditionType tsapi.ConnectorConditionType, status metav1.ConditionStatus, reason, message string, gen int64, clock tstime.Clock, logger *zap.SugaredLogger) []tsapi.ConnectorCondition {
|
||||||
newCondition := tsapi.ConnectorCondition{
|
newCondition := tsapi.ConnectorCondition{
|
||||||
Type: conditionType,
|
Type: conditionType,
|
||||||
@ -69,9 +61,8 @@ func updateCondition(conds []tsapi.ConnectorCondition, conditionType tsapi.Conne
|
|||||||
}
|
}
|
||||||
|
|
||||||
cond := conds[idx] // update the existing condition
|
cond := conds[idx] // update the existing condition
|
||||||
|
// If this update doesn't contain a state transition, we don't update
|
||||||
// If this update doesn't contain a state transition, don't update last
|
// the conditions LastTransitionTime to Now().
|
||||||
// transition time.
|
|
||||||
if cond.Status == status {
|
if cond.Status == status {
|
||||||
newCondition.LastTransitionTime = cond.LastTransitionTime
|
newCondition.LastTransitionTime = cond.LastTransitionTime
|
||||||
} else {
|
} else {
|
||||||
@ -91,14 +82,3 @@ func ProxyClassIsReady(pc *tsapi.ProxyClass) bool {
|
|||||||
cond := pc.Status.Conditions[idx]
|
cond := pc.Status.Conditions[idx]
|
||||||
return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == pc.Generation
|
return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == pc.Generation
|
||||||
}
|
}
|
||||||
|
|
||||||
func DNSCfgIsReady(cfg *tsapi.DNSConfig) bool {
|
|
||||||
idx := xslices.IndexFunc(cfg.Status.Conditions, func(cond tsapi.ConnectorCondition) bool {
|
|
||||||
return cond.Type == tsapi.NameserverReady
|
|
||||||
})
|
|
||||||
if idx == -1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
cond := cfg.Status.Conditions[idx]
|
|
||||||
return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == cfg.Generation
|
|
||||||
}
|
|
||||||
|
@ -98,4 +98,5 @@ func TestSetConnectorCondition(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build !plan9
|
|
||||||
|
|
||||||
package kube
|
|
||||||
|
|
||||||
const Alpha1Version = "v1alpha1"
|
|
||||||
|
|
||||||
type Records struct {
|
|
||||||
// Version is the version of this Records configuration. Version is
|
|
||||||
// intended to be used by ./cmd/k8s-nameserver to determine whether it
|
|
||||||
// can read this records configuration.
|
|
||||||
Version string `json:"version"`
|
|
||||||
// IP4 contains a mapping of DNS names to IPv4 address(es).
|
|
||||||
IP4 map[string][]string `json:"ip4"`
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user