diff --git a/Makefile b/Makefile index b77f631d6..1f9427ceb 100644 --- a/Makefile +++ b/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) 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 @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}' diff --git a/build_docker.sh b/build_docker.sh index c5b81f2ac..8c134fbc5 100755 --- a/build_docker.sh +++ b/build_docker.sh @@ -70,22 +70,6 @@ case "$TARGET" in --target="${PLATFORM}" \ /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" exit 1 diff --git a/cmd/k8s-nameserver/main.go b/cmd/k8s-nameserver/main.go deleted file mode 100644 index 871c192a3..000000000 --- a/cmd/k8s-nameserver/main.go +++ /dev/null @@ -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 /..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 -} diff --git a/cmd/k8s-nameserver/main_test.go b/cmd/k8s-nameserver/main_test.go deleted file mode 100644 index 2c0367e6e..000000000 --- a/cmd/k8s-nameserver/main_test.go +++ /dev/null @@ -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() {} diff --git a/cmd/k8s-operator/deploy/chart/templates/operator-rbac.yaml b/cmd/k8s-operator/deploy/chart/templates/operator-rbac.yaml index 0dc61f4f4..ff518e40c 100644 --- a/cmd/k8s-operator/deploy/chart/templates/operator-rbac.yaml +++ b/cmd/k8s-operator/deploy/chart/templates/operator-rbac.yaml @@ -24,9 +24,6 @@ rules: - apiGroups: ["tailscale.com"] resources: ["connectors", "connectors/status", "proxyclasses", "proxyclasses/status"] verbs: ["get", "list", "watch", "update"] -- apiGroups: ["tailscale.com"] - resources: ["dnsconfigs", "dnsconfigs/status"] - verbs: ["get", "list", "watch", "update"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -48,10 +45,10 @@ metadata: namespace: {{ .Release.Namespace }} rules: - apiGroups: [""] - resources: ["secrets", "serviceaccounts", "configmaps"] + resources: ["secrets"] verbs: ["*"] - apiGroups: ["apps"] - resources: ["statefulsets", "deployments"] + resources: ["statefulsets"] verbs: ["*"] --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/cmd/k8s-operator/deploy/crds/tailscale.com_dnsconfigs.yaml b/cmd/k8s-operator/deploy/crds/tailscale.com_dnsconfigs.yaml deleted file mode 100644 index ba4a66c98..000000000 --- a/cmd/k8s-operator/deploy/crds/tailscale.com_dnsconfigs.yaml +++ /dev/null @@ -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: {} diff --git a/cmd/k8s-operator/deploy/manifests/nameserver/cm.yaml b/cmd/k8s-operator/deploy/manifests/nameserver/cm.yaml deleted file mode 100644 index febabb374..000000000 --- a/cmd/k8s-operator/deploy/manifests/nameserver/cm.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: dnsconfig diff --git a/cmd/k8s-operator/deploy/manifests/nameserver/deploy.yaml b/cmd/k8s-operator/deploy/manifests/nameserver/deploy.yaml deleted file mode 100644 index 735f59b79..000000000 --- a/cmd/k8s-operator/deploy/manifests/nameserver/deploy.yaml +++ /dev/null @@ -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 diff --git a/cmd/k8s-operator/deploy/manifests/nameserver/sa.yaml b/cmd/k8s-operator/deploy/manifests/nameserver/sa.yaml deleted file mode 100644 index b5a87b762..000000000 --- a/cmd/k8s-operator/deploy/manifests/nameserver/sa.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: nameserver -imagePullSecrets: -- name: foo diff --git a/cmd/k8s-operator/deploy/manifests/nameserver/svc.yaml b/cmd/k8s-operator/deploy/manifests/nameserver/svc.yaml deleted file mode 100644 index 08a90c176..000000000 --- a/cmd/k8s-operator/deploy/manifests/nameserver/svc.yaml +++ /dev/null @@ -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 diff --git a/cmd/k8s-operator/deploy/manifests/operator.yaml b/cmd/k8s-operator/deploy/manifests/operator.yaml index c83a1165e..62e444f82 100644 --- a/cmd/k8s-operator/deploy/manifests/operator.yaml +++ b/cmd/k8s-operator/deploy/manifests/operator.yaml @@ -158,103 +158,6 @@ spec: --- 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: - 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: annotations: controller-gen.kubebuilder.io/version: v0.13.0 @@ -788,16 +691,6 @@ rules: - list - watch - update - - apiGroups: - - tailscale.com - resources: - - dnsconfigs - - dnsconfigs/status - verbs: - - get - - list - - watch - - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -822,15 +715,12 @@ rules: - "" resources: - secrets - - serviceaccounts - - configmaps verbs: - '*' - apiGroups: - apps resources: - statefulsets - - deployments verbs: - '*' --- diff --git a/cmd/k8s-operator/generate/main.go b/cmd/k8s-operator/generate/main.go index 64fb7d991..7f2b18bf9 100644 --- a/cmd/k8s-operator/generate/main.go +++ b/cmd/k8s-operator/generate/main.go @@ -22,11 +22,9 @@ const ( operatorDeploymentFilesPath = "cmd/k8s-operator/deploy" connectorCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_connectors.yaml" proxyClassCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_proxyclasses.yaml" - dnsConfigCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_dnsconfigs.yaml" helmTemplatesPath = operatorDeploymentFilesPath + "/chart/templates" connectorCRDHelmTemplatePath = helmTemplatesPath + "/connector.yaml" proxyClassCRDHelmTemplatePath = helmTemplatesPath + "/proxyclass.yaml" - dnsConfigCRDHelmTemplatePath = helmTemplatesPath + "/dnsconfig.yaml" helmConditionalStart = "{{ if .Values.installCRDs -}}\n" 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 // default). func generate(baseDir string) error { @@ -142,9 +140,6 @@ func generate(baseDir string) error { if err := addCRDToHelm(proxyClassCRDPath, proxyClassCRDHelmTemplatePath); err != nil { 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 } @@ -156,8 +151,5 @@ func cleanup(baseDir string) error { if err := os.Remove(filepath.Join(baseDir, proxyClassCRDHelmTemplatePath)); err != nil && !os.IsNotExist(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 } diff --git a/cmd/k8s-operator/generate/main_test.go b/cmd/k8s-operator/generate/main_test.go index febef6789..7e309c1fd 100644 --- a/cmd/k8s-operator/generate/main_test.go +++ b/cmd/k8s-operator/generate/main_test.go @@ -56,9 +56,6 @@ func Test_generate(t *testing.T) { if !strings.Contains(installContentsWithCRD.String(), "name: proxyclasses.tailscale.com") { 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 installContentsWithoutCRD := bytes.NewBuffer([]byte{}) @@ -74,7 +71,4 @@ func Test_generate(t *testing.T) { if strings.Contains(installContentsWithoutCRD.String(), "name: connectors.tailscale.com") { 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") - } } diff --git a/cmd/k8s-operator/nameserver.go b/cmd/k8s-operator/nameserver.go deleted file mode 100644 index 292038aa0..000000000 --- a/cmd/k8s-operator/nameserver.go +++ /dev/null @@ -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 - }, - } -) diff --git a/cmd/k8s-operator/nameserver_test.go b/cmd/k8s-operator/nameserver_test.go deleted file mode 100644 index 89f2b25d4..000000000 --- a/cmd/k8s-operator/nameserver_test.go +++ /dev/null @@ -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) -} diff --git a/cmd/k8s-operator/operator.go b/cmd/k8s-operator/operator.go index 4af00e5c0..6993b20fb 100644 --- a/cmd/k8s-operator/operator.go +++ b/cmd/k8s-operator/operator.go @@ -223,11 +223,8 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string // resources that we GET via the controller manager's client. Cache: cache.Options{ ByObject: map[client.Object]cache.ByObject{ - &corev1.Secret{}: nsFilter, - &corev1.ServiceAccount{}: nsFilter, - &corev1.ConfigMap{}: nsFilter, - &appsv1.StatefulSet{}: nsFilter, - &appsv1.Deployment{}: nsFilter, + &corev1.Secret{}: nsFilter, + &appsv1.StatefulSet{}: nsFilter, }, }, Scheme: tsapi.GlobalScheme, @@ -311,28 +308,7 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string clock: tstime.DefaultClock{}, }) if err != nil { - startlog.Fatalf("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) + startlog.Fatal("could not create connector reconciler: %v", err) } err = builder.ControllerManagedBy(mgr). For(&tsapi.ProxyClass{}). diff --git a/cmd/k8s-operator/operator_test.go b/cmd/k8s-operator/operator_test.go index d9ff51bf6..7188ce65b 100644 --- a/cmd/k8s-operator/operator_test.go +++ b/cmd/k8s-operator/operator_test.go @@ -1194,6 +1194,7 @@ func TestTailscaledConfigfileHash(t *testing.T) { expectReconciled(t, sr, "default", "test") expectEqual(t, fc, expectedSTS(t, fc, o), nil) } + func Test_isMagicDNSName(t *testing.T) { tests := []struct { in string diff --git a/k8s-operator/api.md b/k8s-operator/api.md index 21daf5f3b..e2cd47c25 100644 --- a/k8s-operator/api.md +++ b/k8s-operator/api.md @@ -10,8 +10,6 @@ Resource Types: - [Connector](#connector) -- [DNSConfig](#dnsconfig) - - [ProxyClass](#proxyclass) @@ -261,274 +259,6 @@ ConnectorCondition contains condition information for a Connector. -## DNSConfig -[↩ Parent](#tailscalecomv1alpha1 ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
apiVersionstringtailscale.com/v1alpha1true
kindstringDNSConfigtrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject -
-
true
statusobject -
-
false
- - -### DNSConfig.spec -[↩ Parent](#dnsconfig) - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
nameserverobject -
-
true
- - -### DNSConfig.spec.nameserver -[↩ Parent](#dnsconfigspec) - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
imageobject -
-
false
- - -### DNSConfig.spec.nameserver.image -[↩ Parent](#dnsconfigspecnameserver) - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
repostring -
-
false
tagstring -
-
false
- - -### DNSConfig.status -[↩ Parent](#dnsconfig) - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
conditions[]object -
-
false
nameserverStatusobject -
-
false
- - -### DNSConfig.status.conditions[index] -[↩ Parent](#dnsconfigstatus) - - - -ConnectorCondition contains condition information for a Connector. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
statusstring - Status of the condition, one of ('True', 'False', 'Unknown').
-
true
typestring - Type of the condition, known values are (`SubnetRouterReady`).
-
true
lastTransitionTimestring - LastTransitionTime is the timestamp corresponding to the last status change of this condition.
-
- Format: date-time
-
false
messagestring - Message is a human readable description of the details of the last transition, complementing reason.
-
false
observedGenerationinteger - 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
-
false
reasonstring - Reason is a brief machine readable explanation for the condition's last transition.
-
false
- - -### DNSConfig.status.nameserverStatus -[↩ Parent](#dnsconfigstatus) - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
ipstring -
-
false
- ## ProxyClass [↩ Parent](#tailscalecomv1alpha1 ) diff --git a/k8s-operator/apis/v1alpha1/register.go b/k8s-operator/apis/v1alpha1/register.go index 8c888ff05..4e6bfda64 100644 --- a/k8s-operator/apis/v1alpha1/register.go +++ b/k8s-operator/apis/v1alpha1/register.go @@ -49,7 +49,7 @@ func init() { // Adds the list of known types to api.Scheme. 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) return nil diff --git a/k8s-operator/apis/v1alpha1/types_tsdnsconfig.go b/k8s-operator/apis/v1alpha1/types_tsdnsconfig.go deleted file mode 100644 index 7104d3c13..000000000 --- a/k8s-operator/apis/v1alpha1/types_tsdnsconfig.go +++ /dev/null @@ -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` diff --git a/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go b/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go index f6cabe184..efd202eee 100644 --- a/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go +++ b/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go @@ -158,162 +158,6 @@ func (in *Container) DeepCopy() *Container { 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. func (in *Pod) DeepCopyInto(out *Pod) { *out = *in diff --git a/k8s-operator/conditions.go b/k8s-operator/conditions.go index cdcdbe770..146487fb8 100644 --- a/k8s-operator/conditions.go +++ b/k8s-operator/conditions.go @@ -24,7 +24,7 @@ func SetConnectorCondition(cn *tsapi.Connector, conditionType tsapi.ConnectorCon 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) { conn.Status.Conditions = slices.DeleteFunc(conn.Status.Conditions, func(cond tsapi.ConnectorCondition) bool { return cond.Type == conditionType @@ -39,14 +39,6 @@ func SetProxyClassCondition(pc *tsapi.ProxyClass, conditionType tsapi.ConnectorC 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 { newCondition := tsapi.ConnectorCondition{ Type: conditionType, @@ -69,9 +61,8 @@ func updateCondition(conds []tsapi.ConnectorCondition, conditionType tsapi.Conne } cond := conds[idx] // update the existing condition - - // If this update doesn't contain a state transition, don't update last - // transition time. + // If this update doesn't contain a state transition, we don't update + // the conditions LastTransitionTime to Now(). if cond.Status == status { newCondition.LastTransitionTime = cond.LastTransitionTime } else { @@ -91,14 +82,3 @@ func ProxyClassIsReady(pc *tsapi.ProxyClass) bool { cond := pc.Status.Conditions[idx] 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 -} diff --git a/k8s-operator/conditions_test.go b/k8s-operator/conditions_test.go index 43fea9cb7..624891d75 100644 --- a/k8s-operator/conditions_test.go +++ b/k8s-operator/conditions_test.go @@ -98,4 +98,5 @@ func TestSetConnectorCondition(t *testing.T) { }, }, }) + } diff --git a/k8s-operator/tsdns.go b/k8s-operator/tsdns.go deleted file mode 100644 index 79e77a561..000000000 --- a/k8s-operator/tsdns.go +++ /dev/null @@ -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"` -}