cmd/k8s-operator: put Tailscale IPs in Service ingress status

Updates #502

Signed-off-by: Mike Beaumont <mjboamail@gmail.com>
pull/9083/head
Mike Beaumont 1 year ago committed by Maisem Ali
parent ce4bf41dcf
commit 3451b89e5f

@ -7,9 +7,11 @@ package main
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/netip"
"os" "os"
"tailscale.com/kube" "tailscale.com/kube"
@ -32,7 +34,7 @@ func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error)
// storeDeviceInfo writes deviceID into the "device_id" data field of the kube // storeDeviceInfo writes deviceID into the "device_id" data field of the kube
// secret secretName. // secret secretName.
func storeDeviceInfo(ctx context.Context, secretName string, deviceID tailcfg.StableNodeID, fqdn string) error { func storeDeviceInfo(ctx context.Context, secretName string, deviceID tailcfg.StableNodeID, fqdn string, addresses []netip.Prefix) error {
// First check if the secret exists at all. Even if running on // First check if the secret exists at all. Even if running on
// kubernetes, we do not necessarily store state in a k8s secret. // kubernetes, we do not necessarily store state in a k8s secret.
if _, err := kc.GetSecret(ctx, secretName); err != nil { if _, err := kc.GetSecret(ctx, secretName); err != nil {
@ -46,10 +48,20 @@ func storeDeviceInfo(ctx context.Context, secretName string, deviceID tailcfg.St
return err return err
} }
var ips []string
for _, addr := range addresses {
ips = append(ips, addr.Addr().String())
}
deviceIPs, err := json.Marshal(ips)
if err != nil {
return err
}
m := &kube.Secret{ m := &kube.Secret{
Data: map[string][]byte{ Data: map[string][]byte{
"device_id": []byte(deviceID), "device_id": []byte(deviceID),
"device_fqdn": []byte(fqdn), "device_fqdn": []byte(fqdn),
"device_ips": deviceIPs,
}, },
} }
return kc.StrategicMergePatchSecret(ctx, secretName, m, "tailscale-container") return kc.StrategicMergePatchSecret(ctx, secretName, m, "tailscale-container")

@ -314,7 +314,7 @@ authLoop:
} }
deviceInfo := []any{n.NetMap.SelfNode.StableID(), n.NetMap.SelfNode.Name()} deviceInfo := []any{n.NetMap.SelfNode.StableID(), n.NetMap.SelfNode.Name()}
if cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != "" && deephash.Update(&currentDeviceInfo, &deviceInfo) { if cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != "" && deephash.Update(&currentDeviceInfo, &deviceInfo) {
if err := storeDeviceInfo(ctx, cfg.KubeSecret, n.NetMap.SelfNode.StableID(), n.NetMap.SelfNode.Name()); err != nil { if err := storeDeviceInfo(ctx, cfg.KubeSecret, n.NetMap.SelfNode.StableID(), n.NetMap.SelfNode.Name(), n.NetMap.SelfNode.Addresses().AsSlice()); err != nil {
log.Fatalf("storing device ID in kube secret: %v", err) log.Fatalf("storing device ID in kube secret: %v", err)
} }
} }

@ -113,10 +113,10 @@ func TestContainerBoot(t *testing.T) {
State: ptr.To(ipn.Running), State: ptr.To(ipn.Running),
NetMap: &netmap.NetworkMap{ NetMap: &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{ SelfNode: (&tailcfg.Node{
StableID: tailcfg.StableNodeID("myID"), StableID: tailcfg.StableNodeID("myID"),
Name: "test-node.test.ts.net", Name: "test-node.test.ts.net",
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
}).View(), }).View(),
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
}, },
} }
tests := []struct { tests := []struct {
@ -359,6 +359,7 @@ func TestContainerBoot(t *testing.T) {
"authkey": "tskey-key", "authkey": "tskey-key",
"device_fqdn": "test-node.test.ts.net", "device_fqdn": "test-node.test.ts.net",
"device_id": "myID", "device_id": "myID",
"device_ips": `["100.64.0.1"]`,
}, },
}, },
}, },
@ -447,6 +448,7 @@ func TestContainerBoot(t *testing.T) {
WantKubeSecret: map[string]string{ WantKubeSecret: map[string]string{
"device_fqdn": "test-node.test.ts.net", "device_fqdn": "test-node.test.ts.net",
"device_id": "myID", "device_id": "myID",
"device_ips": `["100.64.0.1"]`,
}, },
}, },
}, },
@ -476,6 +478,7 @@ func TestContainerBoot(t *testing.T) {
"authkey": "tskey-key", "authkey": "tskey-key",
"device_fqdn": "test-node.test.ts.net", "device_fqdn": "test-node.test.ts.net",
"device_id": "myID", "device_id": "myID",
"device_ips": `["100.64.0.1"]`,
}, },
}, },
{ {
@ -483,16 +486,17 @@ func TestContainerBoot(t *testing.T) {
State: ptr.To(ipn.Running), State: ptr.To(ipn.Running),
NetMap: &netmap.NetworkMap{ NetMap: &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{ SelfNode: (&tailcfg.Node{
StableID: tailcfg.StableNodeID("newID"), StableID: tailcfg.StableNodeID("newID"),
Name: "new-name.test.ts.net", Name: "new-name.test.ts.net",
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
}).View(), }).View(),
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
}, },
}, },
WantKubeSecret: map[string]string{ WantKubeSecret: map[string]string{
"authkey": "tskey-key", "authkey": "tskey-key",
"device_fqdn": "new-name.test.ts.net", "device_fqdn": "new-name.test.ts.net",
"device_id": "newID", "device_id": "newID",
"device_ips": `["100.64.0.1"]`,
}, },
}, },
}, },

@ -194,7 +194,7 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
return fmt.Errorf("failed to provision: %w", err) return fmt.Errorf("failed to provision: %w", err)
} }
_, tsHost, err := a.ssr.DeviceInfo(ctx, crl) _, tsHost, _, err := a.ssr.DeviceInfo(ctx, crl)
if err != nil { if err != nil {
return fmt.Errorf("failed to get device ID: %w", err) return fmt.Errorf("failed to get device ID: %w", err)
} }

@ -80,6 +80,7 @@ func TestLoadBalancerClass(t *testing.T) {
} }
s.Data["device_id"] = []byte("ts-id-1234") s.Data["device_id"] = []byte("ts-id-1234")
s.Data["device_fqdn"] = []byte("tailscale.device.name.") s.Data["device_fqdn"] = []byte("tailscale.device.name.")
s.Data["device_ips"] = []byte(`["100.99.98.97", "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf"]`)
}) })
expectReconciled(t, sr, "default", "test") expectReconciled(t, sr, "default", "test")
want := &corev1.Service{ want := &corev1.Service{
@ -104,6 +105,12 @@ func TestLoadBalancerClass(t *testing.T) {
{ {
Hostname: "tailscale.device.name", Hostname: "tailscale.device.name",
}, },
{
IP: "100.99.98.97",
},
{
IP: "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf",
},
}, },
}, },
}, },
@ -306,6 +313,7 @@ func TestAnnotationIntoLB(t *testing.T) {
} }
s.Data["device_id"] = []byte("ts-id-1234") s.Data["device_id"] = []byte("ts-id-1234")
s.Data["device_fqdn"] = []byte("tailscale.device.name.") s.Data["device_fqdn"] = []byte("tailscale.device.name.")
s.Data["device_ips"] = []byte(`["100.99.98.97", "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf"]`)
}) })
expectReconciled(t, sr, "default", "test") expectReconciled(t, sr, "default", "test")
want := &corev1.Service{ want := &corev1.Service{
@ -364,6 +372,12 @@ func TestAnnotationIntoLB(t *testing.T) {
{ {
Hostname: "tailscale.device.name", Hostname: "tailscale.device.name",
}, },
{
IP: "100.99.98.97",
},
{
IP: "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf",
},
}, },
}, },
}, },
@ -425,6 +439,7 @@ func TestLBIntoAnnotation(t *testing.T) {
} }
s.Data["device_id"] = []byte("ts-id-1234") s.Data["device_id"] = []byte("ts-id-1234")
s.Data["device_fqdn"] = []byte("tailscale.device.name.") s.Data["device_fqdn"] = []byte("tailscale.device.name.")
s.Data["device_ips"] = []byte(`["100.99.98.97", "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf"]`)
}) })
expectReconciled(t, sr, "default", "test") expectReconciled(t, sr, "default", "test")
want := &corev1.Service{ want := &corev1.Service{
@ -449,6 +464,12 @@ func TestLBIntoAnnotation(t *testing.T) {
{ {
Hostname: "tailscale.device.name", Hostname: "tailscale.device.name",
}, },
{
IP: "100.99.98.97",
},
{
IP: "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf",
},
}, },
}, },
}, },

@ -122,7 +122,7 @@ func (a *tailscaleSTSReconciler) Cleanup(ctx context.Context, logger *zap.Sugare
return false, nil return false, nil
} }
id, _, err := a.DeviceInfo(ctx, labels) id, _, _, err := a.DeviceInfo(ctx, labels)
if err != nil { if err != nil {
return false, fmt.Errorf("getting device info: %w", err) return false, fmt.Errorf("getting device info: %w", err)
} }
@ -232,25 +232,31 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
// DeviceInfo returns the device ID and hostname for the Tailscale device // DeviceInfo returns the device ID and hostname for the Tailscale device
// associated with the given labels. // associated with the given labels.
func (a *tailscaleSTSReconciler) DeviceInfo(ctx context.Context, childLabels map[string]string) (id tailcfg.StableNodeID, hostname string, err error) { func (a *tailscaleSTSReconciler) DeviceInfo(ctx context.Context, childLabels map[string]string) (id tailcfg.StableNodeID, hostname string, ips []string, err error) {
sec, err := getSingleObject[corev1.Secret](ctx, a.Client, a.operatorNamespace, childLabels) sec, err := getSingleObject[corev1.Secret](ctx, a.Client, a.operatorNamespace, childLabels)
if err != nil { if err != nil {
return "", "", err return "", "", nil, err
} }
if sec == nil { if sec == nil {
return "", "", nil return "", "", nil, nil
} }
id = tailcfg.StableNodeID(sec.Data["device_id"]) id = tailcfg.StableNodeID(sec.Data["device_id"])
if id == "" { if id == "" {
return "", "", nil return "", "", nil, nil
} }
// Kubernetes chokes on well-formed FQDNs with the trailing dot, so we have // Kubernetes chokes on well-formed FQDNs with the trailing dot, so we have
// to remove it. // to remove it.
hostname = strings.TrimSuffix(string(sec.Data["device_fqdn"]), ".") hostname = strings.TrimSuffix(string(sec.Data["device_fqdn"]), ".")
if hostname == "" { if hostname == "" {
return "", "", nil return "", "", nil, nil
} }
return id, hostname, nil if rawDeviceIPs, ok := sec.Data["device_ips"]; ok {
if err := json.Unmarshal(rawDeviceIPs, &ips); err != nil {
return "", "", nil, err
}
}
return id, hostname, ips, nil
} }
func (a *tailscaleSTSReconciler) newAuthKey(ctx context.Context, tags []string) (string, error) { func (a *tailscaleSTSReconciler) newAuthKey(ctx context.Context, tags []string) (string, error) {

@ -139,7 +139,7 @@ func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
return nil return nil
} }
_, tsHost, err := a.ssr.DeviceInfo(ctx, crl) _, tsHost, tsIPs, err := a.ssr.DeviceInfo(ctx, crl)
if err != nil { if err != nil {
return fmt.Errorf("failed to get device ID: %w", err) return fmt.Errorf("failed to get device ID: %w", err)
} }
@ -153,12 +153,14 @@ func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
return nil return nil
} }
logger.Debugf("setting ingress hostname to %q", tsHost) logger.Debugf("setting ingress to %q, %s", tsHost, strings.Join(tsIPs, ", "))
svc.Status.LoadBalancer.Ingress = []corev1.LoadBalancerIngress{ ingress := []corev1.LoadBalancerIngress{
{ {Hostname: tsHost},
Hostname: tsHost,
},
} }
for _, ip := range tsIPs {
ingress = append(ingress, corev1.LoadBalancerIngress{IP: ip})
}
svc.Status.LoadBalancer.Ingress = ingress
if err := a.Status().Update(ctx, svc); err != nil { if err := a.Status().Update(ctx, svc); err != nil {
return fmt.Errorf("failed to update service status: %w", err) return fmt.Errorf("failed to update service status: %w", err)
} }

Loading…
Cancel
Save