diff --git a/cmd/k8s-operator/connector.go b/cmd/k8s-operator/connector.go index 26ee0b7c6..770c5e4fc 100644 --- a/cmd/k8s-operator/connector.go +++ b/cmd/k8s-operator/connector.go @@ -108,7 +108,7 @@ func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Reque } oldCnStatus := cn.Status.DeepCopy() - setStatus := func(cn *tsapi.Connector, conditionType tsapi.ConnectorConditionType, status metav1.ConditionStatus, reason, message string) (reconcile.Result, error) { + setStatus := func(cn *tsapi.Connector, _ tsapi.ConnectorConditionType, status metav1.ConditionStatus, reason, message string) (reconcile.Result, error) { tsoperator.SetConnectorCondition(cn, tsapi.ConnectorReady, status, reason, message, cn.Generation, a.clock, logger) if !apiequality.Semantic.DeepEqual(oldCnStatus, cn.Status) { // An error encountered here should get returned by the Reconcile function. @@ -211,7 +211,27 @@ func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logge gaugeConnectorResources.Set(int64(connectors.Len())) _, err := a.ssr.Provision(ctx, logger, sts) - return err + if err != nil { + return err + } + + _, tsHost, ips, err := a.ssr.DeviceInfo(ctx, crl) + if err != nil { + return err + } + + if tsHost == "" { + logger.Debugf("no Tailscale hostname known yet, waiting for connector pod to finish auth") + // No hostname yet. Wait for the connector pod to auth. + cn.Status.TailnetIPs = nil + cn.Status.Hostname = "" + return nil + } + + cn.Status.TailnetIPs = ips + cn.Status.Hostname = tsHost + + return nil } func (a *ConnectorReconciler) maybeCleanupConnector(ctx context.Context, logger *zap.SugaredLogger, cn *tsapi.Connector) (bool, error) { diff --git a/cmd/k8s-operator/connector_test.go b/cmd/k8s-operator/connector_test.go index 07f0e2d97..a2a6e0be9 100644 --- a/cmd/k8s-operator/connector_test.go +++ b/cmd/k8s-operator/connector_test.go @@ -17,6 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" tsapi "tailscale.com/k8s-operator/apis/v1alpha1" "tailscale.com/tstest" + "tailscale.com/util/mak" ) func TestConnector(t *testing.T) { @@ -29,7 +30,7 @@ func TestConnector(t *testing.T) { }, TypeMeta: metav1.TypeMeta{ Kind: tsapi.ConnectorKind, - APIVersion: "tailscale.io/v1alpha1", + APIVersion: "tailscale.com/v1alpha1", }, Spec: tsapi.ConnectorSpec{ SubnetRouter: &tsapi.SubnetRouter{ @@ -77,6 +78,23 @@ func TestConnector(t *testing.T) { expectEqual(t, fc, expectedSecret(t, opts), nil) expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation) + // Connector status should get updated with the IP/hostname info when available. + const hostname = "foo.tailnetxyz.ts.net" + mustUpdate(t, fc, "operator-ns", opts.secretName, func(secret *corev1.Secret) { + mak.Set(&secret.Data, "device_id", []byte("1234")) + mak.Set(&secret.Data, "device_fqdn", []byte(hostname)) + mak.Set(&secret.Data, "device_ips", []byte(`["127.0.0.1", "::1"]`)) + }) + expectReconciled(t, cr, "", "test") + cn.Finalizers = append(cn.Finalizers, "tailscale.com/finalizer") + cn.Status.IsExitNode = cn.Spec.ExitNode + cn.Status.SubnetRoutes = cn.Spec.SubnetRouter.AdvertiseRoutes.Stringify() + cn.Status.Hostname = hostname + cn.Status.TailnetIPs = []string{"127.0.0.1", "::1"} + expectEqual(t, fc, cn, func(o *tsapi.Connector) { + o.Status.Conditions = nil + }) + // Add another route to be advertised. mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) { conn.Spec.SubnetRouter.AdvertiseRoutes = []tsapi.Route{"10.40.0.0/14", "10.44.0.0/20"} diff --git a/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml b/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml index 7811f22f7..2a5d8039c 100644 --- a/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml +++ b/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml @@ -117,12 +117,20 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + hostname: + description: Hostname is the fully qualified domain name of the Connector node. If MagicDNS is enabled in your tailnet, it is the MagicDNS name of the node. + type: string isExitNode: description: IsExitNode is set to true if the Connector acts as an exit node. type: boolean subnetRoutes: description: SubnetRoutes are the routes currently exposed to tailnet via this Connector instance. type: string + tailnetIPs: + description: TailnetIPs is the set of tailnet IP addresses (both IPv4 and IPv6) assigned to the Connector node. + type: array + items: + type: string served: true storage: true subresources: diff --git a/cmd/k8s-operator/deploy/manifests/operator.yaml b/cmd/k8s-operator/deploy/manifests/operator.yaml index abbc6d242..3f5fe4951 100644 --- a/cmd/k8s-operator/deploy/manifests/operator.yaml +++ b/cmd/k8s-operator/deploy/manifests/operator.yaml @@ -142,12 +142,20 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + hostname: + description: Hostname is the fully qualified domain name of the Connector node. If MagicDNS is enabled in your tailnet, it is the MagicDNS name of the node. + type: string isExitNode: description: IsExitNode is set to true if the Connector acts as an exit node. type: boolean subnetRoutes: description: SubnetRoutes are the routes currently exposed to tailnet via this Connector instance. type: string + tailnetIPs: + description: TailnetIPs is the set of tailnet IP addresses (both IPv4 and IPv6) assigned to the Connector node. + items: + type: string + type: array type: object required: - spec diff --git a/k8s-operator/api.md b/k8s-operator/api.md index 9ec4641b2..f15bd3577 100644 --- a/k8s-operator/api.md +++ b/k8s-operator/api.md @@ -178,6 +178,13 @@ ConnectorStatus describes the status of the Connector. This is set and managed b List of status conditions to indicate the status of the Connector. Known condition types are `ConnectorReady`.
false + + hostname + string + + Hostname is the fully qualified domain name of the Connector node. If MagicDNS is enabled in your tailnet, it is the MagicDNS name of the node.
+ + false isExitNode boolean @@ -192,6 +199,13 @@ ConnectorStatus describes the status of the Connector. This is set and managed b SubnetRoutes are the routes currently exposed to tailnet via this Connector instance.
false + + tailnetIPs + []string + + TailnetIPs is the set of tailnet IP addresses (both IPv4 and IPv6) assigned to the Connector node.
+ + false diff --git a/k8s-operator/apis/v1alpha1/types_connector.go b/k8s-operator/apis/v1alpha1/types_connector.go index 26c8d6473..351bf6ea8 100644 --- a/k8s-operator/apis/v1alpha1/types_connector.go +++ b/k8s-operator/apis/v1alpha1/types_connector.go @@ -156,6 +156,15 @@ type ConnectorStatus struct { // IsExitNode is set to true if the Connector acts as an exit node. // +optional IsExitNode bool `json:"isExitNode"` + // TailnetIPs is the set of tailnet IP addresses (both IPv4 and IPv6) + // assigned to the Connector node. + // +optional + TailnetIPs []string `json:"tailnetIPs,omitempty"` + // Hostname is the fully qualified domain name of the Connector node. + // If MagicDNS is enabled in your tailnet, it is the MagicDNS name of the + // node. + // +optional + Hostname string `json:"hostname,omitempty"` } // ConnectorCondition contains condition information for a Connector. diff --git a/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go b/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go index 8c88b1aab..f79d7de88 100644 --- a/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go +++ b/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go @@ -125,6 +125,11 @@ func (in *ConnectorStatus) DeepCopyInto(out *ConnectorStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.TailnetIPs != nil { + in, out := &in.TailnetIPs, &out.TailnetIPs + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectorStatus.