{,cmd/}k8s-operator: improve docs and validation for image

Change-Id: Ifa9397b4e9dd474a02a8ed3fd56b7d2616738211
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
Tom Proctor 2025-07-08 21:41:38 +01:00
parent dc1a5cd004
commit e6394e2713
9 changed files with 322 additions and 75 deletions

View File

@ -88,6 +88,13 @@ ingressClass:
# If you need more configuration options, take a look at ProxyClass:
# https://tailscale.com/kb/1445/kubernetes-operator-customization#cluster-resource-customization-using-proxyclass-custom-resource
proxyConfig:
# Configure the proxy image to use instead of the default tailscale/tailscale:latest.
# Applying a ProxyClass with `spec.statefulSet.pod.tailscaleContainer.image`
# set will override any defaults here.
#
# Note that ProxyGroups of type "kube-apiserver" use a different default image,
# tailscale/k8s-proxy:latest, and it is currently only possible to override
# that image via the same ProxyClass field.
image:
# Repository defaults to DockerHub, but images are also synced to ghcr.io/tailscale/tailscale.
repository: tailscale/tailscale

View File

@ -1379,12 +1379,21 @@ spec:
type: string
image:
description: |-
Container image name. By default images are pulled from
docker.io/tailscale/tailscale, but the official images are also
available at ghcr.io/tailscale/tailscale. Specifying image name here
will override any proxy image values specified via the Kubernetes
operator's Helm chart values or PROXY_IMAGE env var in the operator
Deployment.
Container image name. By default images are pulled from docker.io/tailscale,
but the official images are also available at ghcr.io/tailscale.
For all uses except on ProxyGroups of type "kube-apiserver", this image must
be either tailscale/tailscale, or an equivalent mirror of that image.
To apply to ProxyGroups of type "kube-apiserver", this image must be
tailscale/k8s-proxy or a mirror of that image.
For "tailscale/tailscale"-based proxies, specifying image name here will
override any proxy image values specified via the Kubernetes operator's
Helm chart values or PROXY_IMAGE env var in the operator Deployment.
For "tailscale/k8s-proxy"-based proxies, there is currently no way to
configure your own default, and this field is the only way to use a
custom image.
https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#image
type: string
imagePullPolicy:
@ -1655,7 +1664,9 @@ spec:
PodSecurityContext, the value specified in SecurityContext takes precedence.
type: string
tailscaleInitContainer:
description: Configuration for the proxy init container that enables forwarding.
description: |-
Configuration for the proxy init container that enables forwarding.
Not valid to apply to ProxyGroups of type "kube-apiserver".
type: object
properties:
debug:
@ -1709,12 +1720,21 @@ spec:
type: string
image:
description: |-
Container image name. By default images are pulled from
docker.io/tailscale/tailscale, but the official images are also
available at ghcr.io/tailscale/tailscale. Specifying image name here
will override any proxy image values specified via the Kubernetes
operator's Helm chart values or PROXY_IMAGE env var in the operator
Deployment.
Container image name. By default images are pulled from docker.io/tailscale,
but the official images are also available at ghcr.io/tailscale.
For all uses except on ProxyGroups of type "kube-apiserver", this image must
be either tailscale/tailscale, or an equivalent mirror of that image.
To apply to ProxyGroups of type "kube-apiserver", this image must be
tailscale/k8s-proxy or a mirror of that image.
For "tailscale/tailscale"-based proxies, specifying image name here will
override any proxy image values specified via the Kubernetes operator's
Helm chart values or PROXY_IMAGE env var in the operator Deployment.
For "tailscale/k8s-proxy"-based proxies, there is currently no way to
configure your own default, and this field is the only way to use a
custom image.
https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#image
type: string
imagePullPolicy:

View File

@ -1852,12 +1852,21 @@ spec:
type: array
image:
description: |-
Container image name. By default images are pulled from
docker.io/tailscale/tailscale, but the official images are also
available at ghcr.io/tailscale/tailscale. Specifying image name here
will override any proxy image values specified via the Kubernetes
operator's Helm chart values or PROXY_IMAGE env var in the operator
Deployment.
Container image name. By default images are pulled from docker.io/tailscale,
but the official images are also available at ghcr.io/tailscale.
For all uses except on ProxyGroups of type "kube-apiserver", this image must
be either tailscale/tailscale, or an equivalent mirror of that image.
To apply to ProxyGroups of type "kube-apiserver", this image must be
tailscale/k8s-proxy or a mirror of that image.
For "tailscale/tailscale"-based proxies, specifying image name here will
override any proxy image values specified via the Kubernetes operator's
Helm chart values or PROXY_IMAGE env var in the operator Deployment.
For "tailscale/k8s-proxy"-based proxies, there is currently no way to
configure your own default, and this field is the only way to use a
custom image.
https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#image
type: string
imagePullPolicy:
@ -2129,7 +2138,9 @@ spec:
type: object
type: object
tailscaleInitContainer:
description: Configuration for the proxy init container that enables forwarding.
description: |-
Configuration for the proxy init container that enables forwarding.
Not valid to apply to ProxyGroups of type "kube-apiserver".
properties:
debug:
description: |-
@ -2182,12 +2193,21 @@ spec:
type: array
image:
description: |-
Container image name. By default images are pulled from
docker.io/tailscale/tailscale, but the official images are also
available at ghcr.io/tailscale/tailscale. Specifying image name here
will override any proxy image values specified via the Kubernetes
operator's Helm chart values or PROXY_IMAGE env var in the operator
Deployment.
Container image name. By default images are pulled from docker.io/tailscale,
but the official images are also available at ghcr.io/tailscale.
For all uses except on ProxyGroups of type "kube-apiserver", this image must
be either tailscale/tailscale, or an equivalent mirror of that image.
To apply to ProxyGroups of type "kube-apiserver", this image must be
tailscale/k8s-proxy or a mirror of that image.
For "tailscale/tailscale"-based proxies, specifying image name here will
override any proxy image values specified via the Kubernetes operator's
Helm chart values or PROXY_IMAGE env var in the operator Deployment.
For "tailscale/k8s-proxy"-based proxies, there is currently no way to
configure your own default, and this field is the only way to use a
custom image.
https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#image
type: string
imagePullPolicy:

View File

@ -17,6 +17,7 @@ import (
"strings"
"sync"
dockerref "github.com/distribution/reference"
"go.uber.org/zap"
xslices "golang.org/x/exp/slices"
appsv1 "k8s.io/api/apps/v1"
@ -161,10 +162,6 @@ func (r *ProxyGroupReconciler) reconcilePG(ctx context.Context, pg *tsapi.ProxyG
}
}
if err := r.validate(ctx, pg); err != nil {
return r.notReady(reasonProxyGroupInvalid, fmt.Sprintf("invalid ProxyGroup spec: %v", err))
}
proxyClassName := r.defaultProxyClass
if pg.Spec.ProxyClass != "" {
proxyClassName = pg.Spec.ProxyClass
@ -182,7 +179,6 @@ func (r *ProxyGroupReconciler) reconcilePG(ctx context.Context, pg *tsapi.ProxyG
if err != nil {
return r.notReadyErrf(pg, "error getting ProxyGroup's ProxyClass %q: %w", proxyClassName, err)
}
validateProxyClassForPG(logger, pg, proxyClass)
if !tsoperator.ProxyClassIsReady(proxyClass) {
msg := fmt.Sprintf("the ProxyGroup's ProxyClass %q is not yet in a ready state, waiting...", proxyClassName)
logger.Info(msg)
@ -190,6 +186,10 @@ func (r *ProxyGroupReconciler) reconcilePG(ctx context.Context, pg *tsapi.ProxyG
}
}
if err := r.validate(ctx, pg, proxyClass, logger); err != nil {
return r.notReady(reasonProxyGroupInvalid, fmt.Sprintf("invalid ProxyGroup spec: %v", err))
}
staticEndpoints, nrr, err := r.maybeProvision(ctx, pg, proxyClass)
if err != nil {
if strings.Contains(err.Error(), optimisticLockErrorMsg) {
@ -204,39 +204,7 @@ func (r *ProxyGroupReconciler) reconcilePG(ctx context.Context, pg *tsapi.ProxyG
return staticEndpoints, nrr, nil
}
func (r *ProxyGroupReconciler) validate(ctx context.Context, pg *tsapi.ProxyGroup) error {
if isAuthAPIServerProxy(pg) {
// Validate that the static ServiceAccount already exists.
sa := &corev1.ServiceAccount{}
if err := r.Get(ctx, types.NamespacedName{Namespace: r.tsNamespace, Name: authAPIServerProxySAName}, sa); err != nil {
if apierrors.IsNotFound(err) {
return fmt.Errorf("the ServiceAccount %q used for the API server proxy in auth mode does not exist but "+
"should have been created during operator installation; use apiServerProxyConfig.allowImpersonation=true "+
"in the helm chart, or authproxy-rbac.yaml from the static manifests", authAPIServerProxySAName)
}
return fmt.Errorf("error validating that ServiceAccount %q exists: %w", authAPIServerProxySAName, err)
}
return nil
}
// Validate that the ServiceAccount we create won't overwrite the static one.
// TODO(tomhjp): This doesn't cover other controllers that could create a
// ServiceAccount. Perhaps should have some guards to ensure that an update
// would never change the ownership of a resource we expect to already be owned.
if pgServiceAccountName(pg) == authAPIServerProxySAName {
return fmt.Errorf("the name of the ProxyGroup %q conflicts with the static ServiceAccount used for the API server proxy in auth mode", pg.Name)
}
return nil
}
// validateProxyClassForPG applies custom validation logic for ProxyClass applied to ProxyGroup.
func validateProxyClassForPG(logger *zap.SugaredLogger, pg *tsapi.ProxyGroup, pc *tsapi.ProxyClass) {
if pg.Spec.Type == tsapi.ProxyGroupTypeIngress {
return
}
func (r *ProxyGroupReconciler) validate(ctx context.Context, pg *tsapi.ProxyGroup, pc *tsapi.ProxyClass, logger *zap.SugaredLogger) error {
// Our custom logic for ensuring minimum downtime ProxyGroup update rollouts relies on the local health check
// beig accessible on the replica Pod IP:9002. This address can also be modified by users, via
// TS_LOCAL_ADDR_PORT env var.
@ -248,13 +216,69 @@ func validateProxyClassForPG(logger *zap.SugaredLogger, pg *tsapi.ProxyGroup, pc
// shouldn't need to set their own).
//
// TODO(irbekrm): maybe disallow configuring this env var in future (in Tailscale 1.84 or later).
if hasLocalAddrPortSet(pc) {
if pg.Spec.Type == tsapi.ProxyGroupTypeEgress && hasLocalAddrPortSet(pc) {
msg := fmt.Sprintf("ProxyClass %s applied to an egress ProxyGroup has TS_LOCAL_ADDR_PORT env var set to a custom value."+
"This will disable the ProxyGroup graceful failover mechanism, so you might experience downtime when ProxyGroup pods are restarted."+
"In future we will remove the ability to set custom TS_LOCAL_ADDR_PORT for egress ProxyGroups."+
"Please raise an issue if you expect that this will cause issues for your workflow.", pc.Name)
logger.Warn(msg)
}
// image is the value of pc.Spec.StatefulSet.Pod.TailscaleContainer.Image or ""
// imagePath is a slash-delimited path ending with the image name, e.g.
// "tailscale/tailscale" or maybe "k8s-proxy" if hosted at example.com/k8s-proxy.
var image, imagePath string
if pc != nil &&
pc.Spec.StatefulSet != nil &&
pc.Spec.StatefulSet.Pod != nil &&
pc.Spec.StatefulSet.Pod.TailscaleContainer != nil {
image, err := dockerref.ParseNormalizedNamed(pc.Spec.StatefulSet.Pod.TailscaleContainer.Image)
if err != nil {
// Shouldn't be possible as the ProxyClass won't be marked ready
// without successfully parsing the image.
return fmt.Errorf("error parsing %q as a container image reference: %w", pc.Spec.StatefulSet.Pod.TailscaleContainer.Image, err)
}
imagePath = dockerref.Path(image)
}
var errs []error
if isAuthAPIServerProxy(pg) {
// Validate that the static ServiceAccount already exists.
sa := &corev1.ServiceAccount{}
if err := r.Get(ctx, types.NamespacedName{Namespace: r.tsNamespace, Name: authAPIServerProxySAName}, sa); err != nil {
if !apierrors.IsNotFound(err) {
return fmt.Errorf("error validating that ServiceAccount %q exists: %w", authAPIServerProxySAName, err)
}
errs = append(errs, fmt.Errorf("the ServiceAccount %q used for the API server proxy in auth mode does not exist but "+
"should have been created during operator installation; use apiServerProxyConfig.allowImpersonation=true "+
"in the helm chart, or authproxy-rbac.yaml from the static manifests", authAPIServerProxySAName))
}
} else {
// Validate that the ServiceAccount we create won't overwrite the static one.
// TODO(tomhjp): This doesn't cover other controllers that could create a
// ServiceAccount. Perhaps should have some guards to ensure that an update
// would never change the ownership of a resource we expect to already be owned.
if pgServiceAccountName(pg) == authAPIServerProxySAName {
errs = append(errs, fmt.Errorf("the name of the ProxyGroup %q conflicts with the static ServiceAccount used for the API server proxy in auth mode", pg.Name))
}
}
if pg.Spec.Type == tsapi.ProxyGroupTypeKubernetesAPIServer {
if strings.HasSuffix(imagePath, "tailscale") {
errs = append(errs, fmt.Errorf("the configured ProxyClass %q specifies to use image %q but expected a %q image for ProxyGroup of type %q", pc.Name, image, "k8s-proxy", pg.Spec.Type))
}
if pc.Spec.StatefulSet != nil && pc.Spec.StatefulSet.Pod != nil && pc.Spec.StatefulSet.Pod.TailscaleInitContainer != nil {
errs = append(errs, fmt.Errorf("the configured ProxyClass %q specifies Tailscale init container config, but ProxyGroups of type %q do not use init containers", pc.Name, pg.Spec.Type))
}
} else {
if strings.HasSuffix(imagePath, "k8s-proxy") {
errs = append(errs, fmt.Errorf("the configured ProxyClass %q specifies to use image %q but expected a %q image for ProxyGroup of type %q", pc.Name, image, "tailscale", pg.Spec.Type))
}
}
return errors.Join(errs...)
}
func (r *ProxyGroupReconciler) maybeProvision(ctx context.Context, pg *tsapi.ProxyGroup, proxyClass *tsapi.ProxyClass) (map[string][]netip.AddrPort, *notReadyReason, error) {

View File

@ -300,7 +300,7 @@ func kubeAPIServerStatefulSet(pg *tsapi.ProxyGroup, namespace, image string) (*a
ServiceAccountName: pgServiceAccountName(pg),
Containers: []corev1.Container{
{
Name: "k8s-proxy",
Name: mainContainerName,
Image: image,
Env: []corev1.EnvVar{
{

View File

@ -1310,6 +1310,170 @@ func TestIngressAdvertiseServicesConfigPreserved(t *testing.T) {
})
}
func TestValidateProxyGroup(t *testing.T) {
type testCase struct {
typ tsapi.ProxyGroupType
pgName string
image string
noauth bool
initContainer bool
staticSAExists bool
expectedErrs int
}
for name, tc := range map[string]testCase{
"default_ingress": {
typ: tsapi.ProxyGroupTypeIngress,
},
"default_kube": {
typ: tsapi.ProxyGroupTypeKubernetesAPIServer,
staticSAExists: true,
},
"default_kube_noauth": {
typ: tsapi.ProxyGroupTypeKubernetesAPIServer,
noauth: true,
// Does not require the static ServiceAccount to exist.
},
"kube_static_sa_missing": {
typ: tsapi.ProxyGroupTypeKubernetesAPIServer,
staticSAExists: false,
expectedErrs: 1,
},
"kube_noauth_would_overwrite_static_sa": {
typ: tsapi.ProxyGroupTypeKubernetesAPIServer,
staticSAExists: true,
noauth: true,
pgName: authAPIServerProxySAName,
expectedErrs: 1,
},
"ingress_would_overwrite_static_sa": {
typ: tsapi.ProxyGroupTypeIngress,
staticSAExists: true,
pgName: authAPIServerProxySAName,
expectedErrs: 1,
},
"tailscale_image_for_kube_pg_1": {
typ: tsapi.ProxyGroupTypeKubernetesAPIServer,
staticSAExists: true,
image: "example.com/tailscale/tailscale",
expectedErrs: 1,
},
"tailscale_image_for_kube_pg_2": {
typ: tsapi.ProxyGroupTypeKubernetesAPIServer,
staticSAExists: true,
image: "example.com/tailscale",
expectedErrs: 1,
},
"tailscale_image_for_kube_pg_3": {
typ: tsapi.ProxyGroupTypeKubernetesAPIServer,
staticSAExists: true,
image: "example.com/tailscale/tailscale:latest",
expectedErrs: 1,
},
"tailscale_image_for_kube_pg_4": {
typ: tsapi.ProxyGroupTypeKubernetesAPIServer,
staticSAExists: true,
image: "tailscale/tailscale",
expectedErrs: 1,
},
"k8s_proxy_image_for_ingress_pg": {
typ: tsapi.ProxyGroupTypeIngress,
image: "example.com/k8s-proxy",
expectedErrs: 1,
},
"init_container_for_kube_pg": {
typ: tsapi.ProxyGroupTypeKubernetesAPIServer,
staticSAExists: true,
initContainer: true,
expectedErrs: 1,
},
"init_container_for_ingress_pg": {
typ: tsapi.ProxyGroupTypeIngress,
initContainer: true,
},
"init_container_for_egress_pg": {
typ: tsapi.ProxyGroupTypeEgress,
initContainer: true,
},
} {
t.Run(name, func(t *testing.T) {
pc := &tsapi.ProxyClass{
ObjectMeta: metav1.ObjectMeta{
Name: "some-pc",
},
Spec: tsapi.ProxyClassSpec{
StatefulSet: &tsapi.StatefulSet{
Pod: &tsapi.Pod{},
},
},
}
if tc.image != "" {
pc.Spec.StatefulSet.Pod.TailscaleContainer = &tsapi.Container{
Image: tc.image,
}
}
if tc.initContainer {
pc.Spec.StatefulSet.Pod.TailscaleInitContainer = &tsapi.Container{}
}
pgName := "some-pg"
if tc.pgName != "" {
pgName = tc.pgName
}
pg := &tsapi.ProxyGroup{
ObjectMeta: metav1.ObjectMeta{
Name: pgName,
},
Spec: tsapi.ProxyGroupSpec{
Type: tc.typ,
},
}
if tc.noauth {
pg.Spec.KubeAPIServer = &tsapi.KubeAPIServerConfig{
Mode: ptr.To(tsapi.APIServerProxyModeNoAuth),
}
}
var objs []client.Object
if tc.staticSAExists {
objs = append(objs, &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: authAPIServerProxySAName,
Namespace: tsNamespace,
},
})
}
r := ProxyGroupReconciler{
tsNamespace: tsNamespace,
Client: fake.NewClientBuilder().
WithObjects(objs...).
Build(),
}
logger, _ := zap.NewDevelopment()
err := r.validate(t.Context(), pg, pc, logger.Sugar())
if tc.expectedErrs == 0 {
if err != nil {
t.Fatalf("expected no errors, got: %v", err)
}
// Test finished.
return
}
if err == nil {
t.Fatalf("expected %d errors, got none", tc.expectedErrs)
}
type unwrapper interface {
Unwrap() []error
}
errs := err.(unwrapper)
if len(errs.Unwrap()) != tc.expectedErrs {
t.Fatalf("expected %d errors, got %d: %v", tc.expectedErrs, len(errs.Unwrap()), err)
}
})
}
}
func proxyClassesForLEStagingTest() (*tsapi.ProxyClass, *tsapi.ProxyClass, *tsapi.ProxyClass) {
pcLEStaging := &tsapi.ProxyClass{
ObjectMeta: metav1.ObjectMeta{

View File

@ -102,6 +102,8 @@ const (
defaultLocalAddrPort = 9002 // metrics and health check port
letsEncryptStagingEndpoint = "https://acme-staging-v02.api.letsencrypt.org/directory"
mainContainerName = "tailscale"
)
var (
@ -903,7 +905,7 @@ func enableEndpoints(ss *appsv1.StatefulSet, metrics, debug bool) {
}
func isMainContainer(c *corev1.Container) bool {
return c.Name == "tailscale" || c.Name == "k8s-proxy"
return c.Name == mainContainerName
}
// tailscaledConfig takes a proxy config, a newly generated auth key if generated and a Secret with the previous proxy

View File

@ -157,7 +157,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `env` _[Env](#env) array_ | List of environment variables to set in the container.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables<br />Note that environment variables provided here will take precedence<br />over Tailscale-specific environment variables set by the operator,<br />however running proxies with custom values for Tailscale environment<br />variables (i.e TS_USERSPACE) is not recommended and might break in<br />the future. | | |
| `image` _string_ | Container image name. By default images are pulled from<br />docker.io/tailscale/tailscale, but the official images are also<br />available at ghcr.io/tailscale/tailscale. Specifying image name here<br />will override any proxy image values specified via the Kubernetes<br />operator's Helm chart values or PROXY_IMAGE env var in the operator<br />Deployment.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#image | | |
| `image` _string_ | Container image name. By default images are pulled from docker.io/tailscale,<br />but the official images are also available at ghcr.io/tailscale.<br />For all uses except on ProxyGroups of type "kube-apiserver", this image must<br />be either tailscale/tailscale, or an equivalent mirror of that image.<br />To apply to ProxyGroups of type "kube-apiserver", this image must be<br />tailscale/k8s-proxy or a mirror of that image.<br />For "tailscale/tailscale"-based proxies, specifying image name here will<br />override any proxy image values specified via the Kubernetes operator's<br />Helm chart values or PROXY_IMAGE env var in the operator Deployment.<br />For "tailscale/k8s-proxy"-based proxies, there is currently no way to<br />configure your own default, and this field is the only way to use a<br />custom image.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#image | | |
| `imagePullPolicy` _[PullPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#pullpolicy-v1-core)_ | Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#image | | Enum: [Always Never IfNotPresent] <br /> |
| `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#resourcerequirements-v1-core)_ | Container resource requirements.<br />By default Tailscale Kubernetes operator does not apply any resource<br />requirements. The amount of resources required wil depend on the<br />amount of resources the operator needs to parse, usage patterns and<br />cluster size.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources | | |
| `securityContext` _[SecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#securitycontext-v1-core)_ | Container security context.<br />Security context specified here will override the security context set by the operator.<br />By default the operator sets the Tailscale container and the Tailscale init container to privileged<br />for proxies created for Tailscale ingress and egress Service, Connector and ProxyGroup.<br />You can reduce the permissions of the Tailscale container to cap NET_ADMIN by<br />installing device plugin in your cluster and configuring the proxies tun device to be created<br />by the device plugin, see https://github.com/tailscale/tailscale/issues/10814#issuecomment-2479977752<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context | | |
@ -490,7 +490,7 @@ _Appears in:_
| `annotations` _object (keys:string, values:string)_ | Annotations that will be added to the proxy Pod.<br />Any annotations specified here will be merged with the default<br />annotations applied to the Pod by the Tailscale Kubernetes operator.<br />Annotations must be valid Kubernetes annotations.<br />https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set | | |
| `affinity` _[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#affinity-v1-core)_ | Proxy Pod's affinity rules.<br />By default, the Tailscale Kubernetes operator does not apply any affinity rules.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#affinity | | |
| `tailscaleContainer` _[Container](#container)_ | Configuration for the proxy container running tailscale. | | |
| `tailscaleInitContainer` _[Container](#container)_ | Configuration for the proxy init container that enables forwarding. | | |
| `tailscaleInitContainer` _[Container](#container)_ | Configuration for the proxy init container that enables forwarding.<br />Not valid to apply to ProxyGroups of type "kube-apiserver". | | |
| `securityContext` _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#podsecuritycontext-v1-core)_ | Proxy Pod's security context.<br />By default Tailscale Kubernetes operator does not apply any Pod<br />security context.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context-2 | | |
| `imagePullSecrets` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#localobjectreference-v1-core) array_ | Proxy Pod's image pull Secrets.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec | | |
| `nodeName` _string_ | Proxy Pod's node name.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling | | |

View File

@ -264,6 +264,7 @@ type Pod struct {
// +optional
TailscaleContainer *Container `json:"tailscaleContainer,omitempty"`
// Configuration for the proxy init container that enables forwarding.
// Not valid to apply to ProxyGroups of type "kube-apiserver".
// +optional
TailscaleInitContainer *Container `json:"tailscaleInitContainer,omitempty"`
// Proxy Pod's security context.
@ -364,12 +365,21 @@ type Container struct {
// the future.
// +optional
Env []Env `json:"env,omitempty"`
// Container image name. By default images are pulled from
// docker.io/tailscale/tailscale, but the official images are also
// available at ghcr.io/tailscale/tailscale. Specifying image name here
// will override any proxy image values specified via the Kubernetes
// operator's Helm chart values or PROXY_IMAGE env var in the operator
// Deployment.
// Container image name. By default images are pulled from docker.io/tailscale,
// but the official images are also available at ghcr.io/tailscale.
//
// For all uses except on ProxyGroups of type "kube-apiserver", this image must
// be either tailscale/tailscale, or an equivalent mirror of that image.
// To apply to ProxyGroups of type "kube-apiserver", this image must be
// tailscale/k8s-proxy or a mirror of that image.
//
// For "tailscale/tailscale"-based proxies, specifying image name here will
// override any proxy image values specified via the Kubernetes operator's
// Helm chart values or PROXY_IMAGE env var in the operator Deployment.
// For "tailscale/k8s-proxy"-based proxies, there is currently no way to
// configure your own default, and this field is the only way to use a
// custom image.
//
// https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#image
// +optional
Image string `json:"image,omitempty"`