2024-02-13 05:27:54 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
package main
import (
"context"
"fmt"
2024-07-17 13:34:56 +00:00
"slices"
2024-04-15 16:24:59 +00:00
"strings"
2024-07-17 13:34:56 +00:00
"sync"
2024-02-13 05:27:54 +00:00
2024-06-07 15:18:44 +00:00
dockerref "github.com/distribution/reference"
2024-02-13 05:27:54 +00:00
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
apivalidation "k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metavalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
2024-07-17 13:34:56 +00:00
"k8s.io/apimachinery/pkg/types"
2024-02-13 05:27:54 +00:00
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
tsoperator "tailscale.com/k8s-operator"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/tstime"
2024-07-17 13:34:56 +00:00
"tailscale.com/util/clientmetric"
"tailscale.com/util/set"
2024-02-13 05:27:54 +00:00
)
const (
reasonProxyClassInvalid = "ProxyClassInvalid"
reasonProxyClassValid = "ProxyClassValid"
2024-04-15 16:24:59 +00:00
reasonCustomTSEnvVar = "CustomTSEnvVar"
2024-02-13 05:27:54 +00:00
messageProxyClassInvalid = "ProxyClass is not valid: %v"
2024-04-15 16:24:59 +00:00
messageCustomTSEnvVar = "ProxyClass overrides the default value for %s env var for %s container. Running with custom values for Tailscale env vars is not recommended and might break in the future."
2024-02-13 05:27:54 +00:00
)
type ProxyClassReconciler struct {
client . Client
recorder record . EventRecorder
logger * zap . SugaredLogger
clock tstime . Clock
2024-07-17 13:34:56 +00:00
mu sync . Mutex // protects following
// managedProxyClasses is a set of all ProxyClass resources that we're currently
// managing. This is only used for metrics.
managedProxyClasses set . Slice [ types . UID ]
2024-02-13 05:27:54 +00:00
}
2024-07-17 13:34:56 +00:00
var (
// gaugeProxyClassResources tracks the number of ProxyClass resources
// that we're currently managing.
gaugeProxyClassResources = clientmetric . NewGauge ( "k8s_proxyclass_resources" )
)
2024-02-13 05:27:54 +00:00
func ( pcr * ProxyClassReconciler ) Reconcile ( ctx context . Context , req reconcile . Request ) ( res reconcile . Result , err error ) {
logger := pcr . logger . With ( "ProxyClass" , req . Name )
logger . Debugf ( "starting reconcile" )
defer logger . Debugf ( "reconcile finished" )
pc := new ( tsapi . ProxyClass )
err = pcr . Get ( ctx , req . NamespacedName , pc )
if apierrors . IsNotFound ( err ) {
logger . Debugf ( "ProxyClass not found, assuming it was deleted" )
return reconcile . Result { } , nil
} else if err != nil {
return reconcile . Result { } , fmt . Errorf ( "failed to get tailscale.com ProxyClass: %w" , err )
}
if ! pc . DeletionTimestamp . IsZero ( ) {
2024-07-17 13:34:56 +00:00
logger . Debugf ( "ProxyClass is being deleted" )
return reconcile . Result { } , pcr . maybeCleanup ( ctx , logger , pc )
}
// Add a finalizer so that we can ensure that metrics get updated when
// this ProxyClass is deleted.
if ! slices . Contains ( pc . Finalizers , FinalizerName ) {
logger . Debugf ( "updating ProxyClass finalizers" )
pc . Finalizers = append ( pc . Finalizers , FinalizerName )
if err := pcr . Update ( ctx , pc ) ; err != nil {
return res , fmt . Errorf ( "failed to add finalizer: %w" , err )
}
2024-02-13 05:27:54 +00:00
}
2024-07-17 13:34:56 +00:00
// Ensure this ProxyClass is tracked in metrics.
pcr . mu . Lock ( )
pcr . managedProxyClasses . Add ( pc . UID )
gaugeProxyClassResources . Set ( int64 ( pcr . managedProxyClasses . Len ( ) ) )
pcr . mu . Unlock ( )
2024-02-13 05:27:54 +00:00
oldPCStatus := pc . Status . DeepCopy ( )
if errs := pcr . validate ( pc ) ; errs != nil {
msg := fmt . Sprintf ( messageProxyClassInvalid , errs . ToAggregate ( ) . Error ( ) )
pcr . recorder . Event ( pc , corev1 . EventTypeWarning , reasonProxyClassInvalid , msg )
tsoperator . SetProxyClassCondition ( pc , tsapi . ProxyClassready , metav1 . ConditionFalse , reasonProxyClassInvalid , msg , pc . Generation , pcr . clock , logger )
} else {
tsoperator . SetProxyClassCondition ( pc , tsapi . ProxyClassready , metav1 . ConditionTrue , reasonProxyClassValid , reasonProxyClassValid , pc . Generation , pcr . clock , logger )
}
if ! apiequality . Semantic . DeepEqual ( oldPCStatus , pc . Status ) {
if err := pcr . Client . Status ( ) . Update ( ctx , pc ) ; err != nil {
logger . Errorf ( "error updating ProxyClass status: %v" , err )
return reconcile . Result { } , err
}
}
return reconcile . Result { } , nil
}
2024-07-17 13:34:56 +00:00
func ( pcr * ProxyClassReconciler ) validate ( pc * tsapi . ProxyClass ) ( violations field . ErrorList ) {
2024-02-13 05:27:54 +00:00
if sts := pc . Spec . StatefulSet ; sts != nil {
if len ( sts . Labels ) > 0 {
if errs := metavalidation . ValidateLabels ( sts . Labels , field . NewPath ( ".spec.statefulSet.labels" ) ) ; errs != nil {
violations = append ( violations , errs ... )
}
}
if len ( sts . Annotations ) > 0 {
if errs := apivalidation . ValidateAnnotations ( sts . Annotations , field . NewPath ( ".spec.statefulSet.annotations" ) ) ; errs != nil {
violations = append ( violations , errs ... )
}
}
if pod := sts . Pod ; pod != nil {
if len ( pod . Labels ) > 0 {
if errs := metavalidation . ValidateLabels ( pod . Labels , field . NewPath ( ".spec.statefulSet.pod.labels" ) ) ; errs != nil {
violations = append ( violations , errs ... )
}
}
if len ( pod . Annotations ) > 0 {
if errs := apivalidation . ValidateAnnotations ( pod . Annotations , field . NewPath ( ".spec.statefulSet.pod.annotations" ) ) ; errs != nil {
violations = append ( violations , errs ... )
}
}
2024-04-15 16:24:59 +00:00
if tc := pod . TailscaleContainer ; tc != nil {
for _ , e := range tc . Env {
if strings . HasPrefix ( string ( e . Name ) , "TS_" ) {
2024-07-17 13:34:56 +00:00
pcr . recorder . Event ( pc , corev1 . EventTypeWarning , reasonCustomTSEnvVar , fmt . Sprintf ( messageCustomTSEnvVar , string ( e . Name ) , "tailscale" ) )
2024-04-15 16:24:59 +00:00
}
if strings . EqualFold ( string ( e . Name ) , "EXPERIMENTAL_TS_CONFIGFILE_PATH" ) {
2024-07-17 13:34:56 +00:00
pcr . recorder . Event ( pc , corev1 . EventTypeWarning , reasonCustomTSEnvVar , fmt . Sprintf ( messageCustomTSEnvVar , string ( e . Name ) , "tailscale" ) )
2024-04-15 16:24:59 +00:00
}
if strings . EqualFold ( string ( e . Name ) , "EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS" ) {
2024-07-17 13:34:56 +00:00
pcr . recorder . Event ( pc , corev1 . EventTypeWarning , reasonCustomTSEnvVar , fmt . Sprintf ( messageCustomTSEnvVar , string ( e . Name ) , "tailscale" ) )
2024-04-15 16:24:59 +00:00
}
}
2024-06-07 15:18:44 +00:00
if tc . Image != "" {
// Same validation as used by kubelet https://github.com/kubernetes/kubernetes/blob/release-1.30/pkg/kubelet/images/image_manager.go#L212
if _ , err := dockerref . ParseNormalizedNamed ( tc . Image ) ; err != nil {
violations = append ( violations , field . TypeInvalid ( field . NewPath ( "spec" , "statefulSet" , "pod" , "tailscaleContainer" , "image" ) , tc . Image , err . Error ( ) ) )
}
}
}
if tc := pod . TailscaleInitContainer ; tc != nil {
if tc . Image != "" {
// Same validation as used by kubelet https://github.com/kubernetes/kubernetes/blob/release-1.30/pkg/kubelet/images/image_manager.go#L212
if _ , err := dockerref . ParseNormalizedNamed ( tc . Image ) ; err != nil {
violations = append ( violations , field . TypeInvalid ( field . NewPath ( "spec" , "statefulSet" , "pod" , "tailscaleInitContainer" , "image" ) , tc . Image , err . Error ( ) ) )
}
}
2024-04-15 16:24:59 +00:00
}
2024-02-13 05:27:54 +00:00
}
}
// We do not validate embedded fields (security context, resource
// requirements etc) as we inherit upstream validation for those fields.
// Invalid values would get rejected by upstream validations at apply
// time.
return violations
}
2024-07-17 13:34:56 +00:00
// maybeCleanup removes tailscale.com finalizer and ensures that the ProxyClass
// is no longer counted towards k8s_proxyclass_resources.
func ( pcr * ProxyClassReconciler ) maybeCleanup ( ctx context . Context , logger * zap . SugaredLogger , pc * tsapi . ProxyClass ) error {
ix := slices . Index ( pc . Finalizers , FinalizerName )
if ix < 0 {
logger . Debugf ( "no finalizer, nothing to do" )
pcr . mu . Lock ( )
defer pcr . mu . Unlock ( )
pcr . managedProxyClasses . Remove ( pc . UID )
gaugeProxyClassResources . Set ( int64 ( pcr . managedProxyClasses . Len ( ) ) )
return nil
}
pc . Finalizers = append ( pc . Finalizers [ : ix ] , pc . Finalizers [ ix + 1 : ] ... )
if err := pcr . Update ( ctx , pc ) ; err != nil {
return fmt . Errorf ( "failed to remove finalizer: %w" , err )
}
pcr . mu . Lock ( )
defer pcr . mu . Unlock ( )
pcr . managedProxyClasses . Remove ( pc . UID )
gaugeProxyClassResources . Set ( int64 ( pcr . managedProxyClasses . Len ( ) ) )
logger . Infof ( "ProxyClass resources have been cleaned up" )
return nil
}