k8s-operator,cmd/k8s-operator: define ProxyGroupPolicy CRD (#18614)

This commit adds a new custom resource definition to the kubernetes
operator named `ProxyGroupPolicy`. This resource is namespace scoped
and is used as an allow list for which `ProxyGroup` resources can be
used within its namespace.

The `spec` contains two fields, `ingress` and `egress`. These should
contain the names of `ProxyGroup` resources to denote which can be
used as values in the `tailscale.com/proxy-group` annotation within
`Service` and `Ingress` resources.

The intention is for these policies to be merged within a namespace and
produce a `ValidatingAdmissionPolicy` and `ValidatingAdmissionPolicyBinding`
for both ingress and egress that prevents users from using names of
`ProxyGroup` resources in those annotations.

Closes: https://github.com/tailscale/corp/issues/36829

Signed-off-by: David Bond <davidsbond93@gmail.com>
This commit is contained in:
David Bond
2026-02-13 16:04:34 +00:00
committed by GitHub
parent d468870310
commit a341eea00b
5 changed files with 391 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.0
name: proxygrouppolicies.tailscale.com
spec:
group: tailscale.com
names:
kind: ProxyGroupPolicy
listKind: ProxyGroupPolicyList
plural: proxygrouppolicies
shortNames:
- pgp
singular: proxygrouppolicy
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- description: Status of the deployed ProxyGroupPolicy resources.
jsonPath: .status.conditions[?(@.type == "ProxyGroupPolicyReady")].reason
name: Status
type: string
name: v1alpha1
schema:
openAPIV3Schema:
type: object
required:
- metadata
- 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:
description: |-
Spec describes the desired state of the ProxyGroupPolicy.
More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
type: object
properties:
egress:
description: |-
Names of ProxyGroup resources that can be used by Service resources within this namespace. An empty list
denotes that no egress via ProxyGroups is allowed within this namespace.
type: array
items:
type: string
ingress:
description: |-
Names of ProxyGroup resources that can be used by Ingress resources within this namespace. An empty list
denotes that no ingress via ProxyGroups is allowed within this namespace.
type: array
items:
type: string
status:
description: |-
Status describes the status of the ProxyGroupPolicy. This is set
and managed by the Tailscale operator.
type: object
properties:
conditions:
type: array
items:
description: Condition contains details for one aspect of the current state of this API Resource.
type: object
required:
- lastTransitionTime
- message
- reason
- status
- type
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.
type: string
format: date-time
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
type: string
maxLength: 32768
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.
type: integer
format: int64
minimum: 0
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.
type: string
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
status:
description: status of the condition, one of True, False, Unknown.
type: string
enum:
- "True"
- "False"
- Unknown
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
type: string
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])$
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
served: true
storage: true
subresources:
status: {}

View File

@@ -16,6 +16,8 @@
- [ProxyClassList](#proxyclasslist)
- [ProxyGroup](#proxygroup)
- [ProxyGroupList](#proxygrouplist)
- [ProxyGroupPolicy](#proxygrouppolicy)
- [ProxyGroupPolicyList](#proxygrouppolicylist)
- [Recorder](#recorder)
- [RecorderList](#recorderlist)
- [Tailnet](#tailnet)
@@ -725,6 +727,81 @@ _Appears in:_
| `items` _[ProxyGroup](#proxygroup) array_ | | | |
#### ProxyGroupPolicy
_Appears in:_
- [ProxyGroupPolicyList](#proxygrouppolicylist)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `apiVersion` _string_ | `tailscale.com/v1alpha1` | | |
| `kind` _string_ | `ProxyGroupPolicy` | | |
| `kind` _string_ | Kind is a string value representing the REST resource this object represents.<br />Servers may infer this from the endpoint the client submits requests to.<br />Cannot be updated.<br />In CamelCase.<br />More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | | |
| `apiVersion` _string_ | APIVersion defines the versioned schema of this representation of an object.<br />Servers should convert recognized schemas to the latest internal value, and<br />may reject unrecognized values.<br />More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources | | |
| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
| `spec` _[ProxyGroupPolicySpec](#proxygrouppolicyspec)_ | Spec describes the desired state of the ProxyGroupPolicy.<br />More info:<br />https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status | | |
| `status` _[ProxyGroupPolicyStatus](#proxygrouppolicystatus)_ | Status describes the status of the ProxyGroupPolicy. This is set<br />and managed by the Tailscale operator. | | |
#### ProxyGroupPolicyList
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `apiVersion` _string_ | `tailscale.com/v1alpha1` | | |
| `kind` _string_ | `ProxyGroupPolicyList` | | |
| `kind` _string_ | Kind is a string value representing the REST resource this object represents.<br />Servers may infer this from the endpoint the client submits requests to.<br />Cannot be updated.<br />In CamelCase.<br />More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | | |
| `apiVersion` _string_ | APIVersion defines the versioned schema of this representation of an object.<br />Servers should convert recognized schemas to the latest internal value, and<br />may reject unrecognized values.<br />More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources | | |
| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
| `items` _[ProxyGroupPolicy](#proxygrouppolicy) array_ | | | |
#### ProxyGroupPolicySpec
_Appears in:_
- [ProxyGroupPolicy](#proxygrouppolicy)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `ingress` _string array_ | Names of ProxyGroup resources that can be used by Ingress resources within this namespace. An empty list<br />denotes that no ingress via ProxyGroups is allowed within this namespace. | | |
| `egress` _string array_ | Names of ProxyGroup resources that can be used by Service resources within this namespace. An empty list<br />denotes that no egress via ProxyGroups is allowed within this namespace. | | |
#### ProxyGroupPolicyStatus
_Appears in:_
- [ProxyGroupPolicy](#proxygrouppolicy)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#condition-v1-meta) array_ | | | |
#### ProxyGroupSpec

View File

@@ -69,6 +69,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&ProxyGroupList{},
&Tailnet{},
&TailnetList{},
&ProxyGroupPolicy{},
&ProxyGroupPolicyList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)

View File

@@ -0,0 +1,67 @@
// Copyright (c) Tailscale Inc & contributors
// 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 ProxyGroupPolicy CRD i.e. if someone runs kubectl explain tailnet.
var ProxyGroupPolicyKind = "ProxyGroupPolicy"
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced,shortName=pgp
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=`.status.conditions[?(@.type == "ProxyGroupPolicyReady")].reason`,description="Status of the deployed ProxyGroupPolicy resources."
type ProxyGroupPolicy struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
// Spec describes the desired state of the ProxyGroupPolicy.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
Spec ProxyGroupPolicySpec `json:"spec"`
// Status describes the status of the ProxyGroupPolicy. This is set
// and managed by the Tailscale operator.
// +optional
Status ProxyGroupPolicyStatus `json:"status"`
}
// +kubebuilder:object:root=true
type ProxyGroupPolicyList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []ProxyGroupPolicy `json:"items"`
}
type ProxyGroupPolicySpec struct {
// Names of ProxyGroup resources that can be used by Ingress resources within this namespace. An empty list
// denotes that no ingress via ProxyGroups is allowed within this namespace.
// +optional
Ingress []string `json:"ingress,omitempty"`
// Names of ProxyGroup resources that can be used by Service resources within this namespace. An empty list
// denotes that no egress via ProxyGroups is allowed within this namespace.
// +optional
Egress []string `json:"egress,omitempty"`
}
type ProxyGroupPolicyStatus struct {
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions"`
}
// ProxyGroupPolicyReady is set to True if the ProxyGroupPolicy is available for use by operator workloads.
const ProxyGroupPolicyReady ConditionType = "ProxyGroupPolicyReady"

View File

@@ -832,6 +832,112 @@ func (in *ProxyGroupList) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProxyGroupPolicy) DeepCopyInto(out *ProxyGroupPolicy) {
*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 ProxyGroupPolicy.
func (in *ProxyGroupPolicy) DeepCopy() *ProxyGroupPolicy {
if in == nil {
return nil
}
out := new(ProxyGroupPolicy)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ProxyGroupPolicy) 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 *ProxyGroupPolicyList) DeepCopyInto(out *ProxyGroupPolicyList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ProxyGroupPolicy, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyGroupPolicyList.
func (in *ProxyGroupPolicyList) DeepCopy() *ProxyGroupPolicyList {
if in == nil {
return nil
}
out := new(ProxyGroupPolicyList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ProxyGroupPolicyList) 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 *ProxyGroupPolicySpec) DeepCopyInto(out *ProxyGroupPolicySpec) {
*out = *in
if in.Ingress != nil {
in, out := &in.Ingress, &out.Ingress
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Egress != nil {
in, out := &in.Egress, &out.Egress
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyGroupPolicySpec.
func (in *ProxyGroupPolicySpec) DeepCopy() *ProxyGroupPolicySpec {
if in == nil {
return nil
}
out := new(ProxyGroupPolicySpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProxyGroupPolicyStatus) DeepCopyInto(out *ProxyGroupPolicyStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyGroupPolicyStatus.
func (in *ProxyGroupPolicyStatus) DeepCopy() *ProxyGroupPolicyStatus {
if in == nil {
return nil
}
out := new(ProxyGroupPolicyStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProxyGroupSpec) DeepCopyInto(out *ProxyGroupSpec) {
*out = *in