From 9535e3b99bfc9044eac8b8e03f4f639132245149 Mon Sep 17 00:00:00 2001 From: David Bond Date: Fri, 24 Jul 2026 14:01:41 +0100 Subject: [PATCH] cmd/k8s-operator: include peer relay CRD in generation (#20593) This commit modifies the generation command for the kubernetes operator to include the CRD for peer relays in the helm chart and static manifests Updates: #fixup Signed-off-by: David Bond --- .../deploy/chart/templates/.gitignore | 1 + .../deploy/manifests/operator.yaml | 265 ++++++++++++++++++ cmd/k8s-operator/generate/main.go | 4 + 3 files changed, 270 insertions(+) diff --git a/cmd/k8s-operator/deploy/chart/templates/.gitignore b/cmd/k8s-operator/deploy/chart/templates/.gitignore index 185ea9e2b..ccdea8702 100644 --- a/cmd/k8s-operator/deploy/chart/templates/.gitignore +++ b/cmd/k8s-operator/deploy/chart/templates/.gitignore @@ -10,3 +10,4 @@ /recorder.yaml /tailnet.yaml /proxygrouppolicy.yaml +/peerrelay.yaml diff --git a/cmd/k8s-operator/deploy/manifests/operator.yaml b/cmd/k8s-operator/deploy/manifests/operator.yaml index 32d9f0896..1dfbf7126 100644 --- a/cmd/k8s-operator/deploy/manifests/operator.yaml +++ b/cmd/k8s-operator/deploy/manifests/operator.yaml @@ -1463,6 +1463,271 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.0 + name: peerrelays.tailscale.com +spec: + group: tailscale.com + names: + kind: PeerRelay + listKind: PeerRelayList + plural: peerrelays + shortNames: + - pr + singular: peerrelay + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Status of the deployed PeerRelay resources. + jsonPath: .status.conditions[?(@.type == "PeerRelayReady")].reason + name: Status + type: string + - description: Public addresses the peer relay replicas are reachable on. + jsonPath: .status.endpoints[*].address + name: Endpoints + 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: + description: |- + Spec describes the desired state of the PeerRelay. + More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + aws: + description: |- + AWS contains configuration for pinning each replica to a specific AWS Elastic IP and subnet. Only meaningful + when running on EKS with the AWS Load Balancer Controller. When set, the per-replica values override any + aws-load-balancer-eip-allocations or aws-load-balancer-subnets values supplied via spec.service.annotations. + properties: + elasticIPs: + description: |- + ElasticIPs pins each replica to a specific AWS EIP allocation and subnet. Only meaningful when Network Load + Balancers are provisioned by the AWS Load Balancer Controller. ElasticIPs supplies one allocation-subnet pair + per replica: replica N uses ElasticIPs[N]. The list must be at least as long as spec.replicas so every replica + has a distinct EIP; extra entries are permitted so that scale-up doesn't immediately trip validation. + + When set, the reconciler stamps + service.beta.kubernetes.io/aws-load-balancer-eip-allocations and + service.beta.kubernetes.io/aws-load-balancer-subnets on each per-replica Service, overriding any values in + spec.service.annotations. + items: + description: PeerRelayAWSElasticIP pairs an EIP allocation with the subnet in the same AZ. + properties: + allocationID: + description: |- + AllocationID is the AWS EIP allocation ID (e.g. eipalloc-0123abcd) whose public IP this replica is reachable + on. Stamped as service.beta.kubernetes.io/aws-load-balancer-eip-allocations on the replica's Service. + pattern: ^eipalloc-[0-9a-f]+$ + type: string + subnetID: + description: |- + SubnetID is the AWS subnet in the same availability zone as AllocationID (e.g. subnet-0123abcd). Stamped as + service.beta.kubernetes.io/aws-load-balancer-subnets on the replica's Service so the NLB is provisioned in + the same AZ as the EIP. + pattern: ^subnet-[0-9a-f]+$ + type: string + required: + - allocationID + - subnetID + type: object + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - elasticIPs + type: object + hostnamePrefix: + description: |- + HostnamePrefix specifies the hostname prefix for each + replica. Each device will have the integer number + from its StatefulSet pod appended to this prefix to form the full hostname. + HostnamePrefix can contain lower case letters, numbers and dashes, it + must not start with a dash and must be between 1 and 62 characters long. + pattern: ^[a-z0-9][a-z0-9-]{0,61}$ + type: string + proxyClass: + description: |- + ProxyClass is the name of the ProxyClass custom resource that + contains configuration options that should be applied to the + resources created for this PeerRelay. If unset, the operator will + create resources with the default configuration. + type: string + replicas: + default: 1 + description: |- + Replicas specifies how many devices to create. Set this to enable + high availability for peer relays. + https://tailscale.com/kb/1115/high-availability. Defaults to 1. + format: int32 + minimum: 0 + type: integer + service: + description: Service contains configuration values to modify the LoadBalancer service used to expose the peer relay. + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations to apply to the LoadBalancer service. Any annotations that conflict with those used by known + cloud providers to ensure IP addresses rather than DNS names are ignored. + type: object + type: object + tags: + description: |- + Tags that the Tailscale node will be tagged with. + Defaults to [tag:k8s]. + To autoapprove the device defined by a PeerRelay, + you can configure Tailscale ACLs to give these tags the necessary + permissions. + See https://tailscale.com/kb/1337/acl-syntax#autoapprovers. + If you specify custom tags here, you must also make the operator an owner of these tags. + See https://tailscale.com/kb/1236/kubernetes-operator/#setting-up-the-kubernetes-operator. + Tags cannot be changed once a PeerRelay node has been created. + Tag values must be in form ^tag:[a-zA-Z][a-zA-Z0-9-]*$. + items: + pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$ + type: string + type: array + tailnet: + description: |- + Tailnet specifies the tailnet this PeerRelay should join. If blank, the default tailnet is used. When set, this + name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. + type: string + x-kubernetes-validations: + - message: PeerRelay tailnet is immutable + rule: self == oldSelf + type: object + x-kubernetes-validations: + - message: spec.aws.elasticIPs must contain at least one entry per replica + rule: '!has(self.aws) || !has(self.aws.elasticIPs) || self.aws.elasticIPs.size() >= self.replicas' + status: + description: |- + Status describes the status of the PeerRelay. This is set + and managed by the Tailscale operator. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + endpoints: + description: |- + Endpoints lists the public address:port pairs each peer relay replica is reachable on. There is one entry + per replica whose LoadBalancer Service has been assigned a public address; entries appear as the underlying + cloud provisions each Service. + items: + properties: + address: + description: |- + Address is the public IP or hostname the cloud has allocated for this replica's LoadBalancer Service. + Peers reach this relay by connecting to Address:Port over UDP. + type: string + port: + description: Port is the UDP port the peer relay listens on. + format: int32 + type: integer + replica: + description: Replica is the zero-based index of the peer relay replica this endpoint targets. + format: int32 + type: integer + required: + - address + - port + - replica + type: object + type: array + x-kubernetes-list-map-keys: + - replica + x-kubernetes-list-type: map + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.17.0 diff --git a/cmd/k8s-operator/generate/main.go b/cmd/k8s-operator/generate/main.go index 840812ea3..b3e44c117 100644 --- a/cmd/k8s-operator/generate/main.go +++ b/cmd/k8s-operator/generate/main.go @@ -28,6 +28,7 @@ const ( proxyGroupCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_proxygroups.yaml" tailnetCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_tailnets.yaml" proxyGroupPolicyCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_proxygrouppolicies.yaml" + peerRelayCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_peerrelays.yaml" helmTemplatesPath = operatorDeploymentFilesPath + "/chart/templates" connectorCRDHelmTemplatePath = helmTemplatesPath + "/connector.yaml" proxyClassCRDHelmTemplatePath = helmTemplatesPath + "/proxyclass.yaml" @@ -36,6 +37,7 @@ const ( proxyGroupCRDHelmTemplatePath = helmTemplatesPath + "/proxygroup.yaml" tailnetCRDHelmTemplatePath = helmTemplatesPath + "/tailnet.yaml" proxyGroupPolicyCRDHelmTemplatePath = helmTemplatesPath + "/proxygrouppolicy.yaml" + peerRelayCRDHelmTemplatePath = helmTemplatesPath + "/peerrelay.yaml" helmConditionalStart = "{{ if .Values.installCRDs -}}\n" helmConditionalEnd = "{{- end -}}" @@ -160,6 +162,7 @@ func generate(baseDir string) error { {proxyGroupCRDPath, proxyGroupCRDHelmTemplatePath}, {tailnetCRDPath, tailnetCRDHelmTemplatePath}, {proxyGroupPolicyCRDPath, proxyGroupPolicyCRDHelmTemplatePath}, + {peerRelayCRDPath, peerRelayCRDHelmTemplatePath}, } { if err := addCRDToHelm(crd.crdPath, crd.templatePath); err != nil { return fmt.Errorf("error adding %s CRD to Helm templates: %w", crd.crdPath, err) @@ -178,6 +181,7 @@ func cleanup(baseDir string) error { proxyGroupCRDHelmTemplatePath, tailnetCRDHelmTemplatePath, proxyGroupPolicyCRDHelmTemplatePath, + peerRelayCRDHelmTemplatePath, } { if err := os.Remove(filepath.Join(baseDir, path)); err != nil && !os.IsNotExist(err) { return fmt.Errorf("error cleaning up %s: %w", path, err)