allow the kube cert agent deployment's strategy type to be configured

This commit is contained in:
Ryan Richard
2025-10-23 17:07:45 -07:00
parent 6e87caaa6a
commit 9be6bb0b94
6 changed files with 217 additions and 18 deletions

View File

@@ -11,6 +11,8 @@ import (
"os" "os"
"strings" "strings"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/utils/ptr" "k8s.io/utils/ptr"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
@@ -200,6 +202,12 @@ func validateKubeCertAgent(agentConfig *KubeCertAgentSpec) error {
return constable.Error(fmt.Sprintf("runAsGroup must be 0 or greater (instead of %d)", *agentConfig.RunAsGroup)) return constable.Error(fmt.Sprintf("runAsGroup must be 0 or greater (instead of %d)", *agentConfig.RunAsGroup))
} }
allowedStrategyTypes := sets.New(appsv1.RecreateDeploymentStrategyType, appsv1.RollingUpdateDeploymentStrategyType)
if agentConfig.DeploymentStrategyType != nil && !allowedStrategyTypes.Has(*agentConfig.DeploymentStrategyType) {
return constable.Error(fmt.Sprintf("deploymentStrategyType must be one of %s (instead of %s)",
sets.List(allowedStrategyTypes), *agentConfig.DeploymentStrategyType))
}
if len(agentConfig.PriorityClassName) == 0 { if len(agentConfig.PriorityClassName) == 0 {
// Optional, so empty is valid. // Optional, so empty is valid.
return nil return nil

View File

@@ -11,6 +11,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/utils/ptr" "k8s.io/utils/ptr"
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
@@ -71,6 +72,7 @@ func TestFromPath(t *testing.T) {
priorityClassName: %s priorityClassName: %s
runAsUser: 1 runAsUser: 1
runAsGroup: 2 runAsGroup: 2
deploymentStrategyType: Recreate
log: log:
level: debug level: debug
tls: tls:
@@ -119,12 +121,13 @@ func TestFromPath(t *testing.T) {
"myLabelKey2": "myLabelValue2", "myLabelKey2": "myLabelValue2",
}, },
KubeCertAgentConfig: KubeCertAgentSpec{ KubeCertAgentConfig: KubeCertAgentSpec{
NamePrefix: ptr.To("kube-cert-agent-name-prefix-"), NamePrefix: ptr.To("kube-cert-agent-name-prefix-"),
Image: ptr.To("kube-cert-agent-image"), Image: ptr.To("kube-cert-agent-image"),
ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"}, ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"},
PriorityClassName: stringOfLength253, PriorityClassName: stringOfLength253,
RunAsUser: ptr.To(int64(1)), RunAsUser: ptr.To(int64(1)),
RunAsGroup: ptr.To(int64(2)), RunAsGroup: ptr.To(int64(2)),
DeploymentStrategyType: ptr.To(appsv1.RecreateDeploymentStrategyType),
}, },
Log: plog.LogSpec{ Log: plog.LogSpec{
Level: plog.LevelDebug, Level: plog.LevelDebug,
@@ -184,6 +187,9 @@ func TestFromPath(t *testing.T) {
image: kube-cert-agent-image image: kube-cert-agent-image
imagePullSecrets: [kube-cert-agent-image-pull-secret] imagePullSecrets: [kube-cert-agent-image-pull-secret]
priorityClassName: kube-cert-agent-priority-class-name priorityClassName: kube-cert-agent-priority-class-name
runAsUser: 1
runAsGroup: 2
deploymentStrategyType: RollingUpdate
log: log:
level: all level: all
format: json format: json
@@ -227,10 +233,13 @@ func TestFromPath(t *testing.T) {
"myLabelKey2": "myLabelValue2", "myLabelKey2": "myLabelValue2",
}, },
KubeCertAgentConfig: KubeCertAgentSpec{ KubeCertAgentConfig: KubeCertAgentSpec{
NamePrefix: ptr.To("kube-cert-agent-name-prefix-"), NamePrefix: ptr.To("kube-cert-agent-name-prefix-"),
Image: ptr.To("kube-cert-agent-image"), Image: ptr.To("kube-cert-agent-image"),
ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"}, ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"},
PriorityClassName: "kube-cert-agent-priority-class-name", PriorityClassName: "kube-cert-agent-priority-class-name",
RunAsUser: ptr.To(int64(1)),
RunAsGroup: ptr.To(int64(2)),
DeploymentStrategyType: ptr.To(appsv1.RollingUpdateDeploymentStrategyType),
}, },
Log: plog.LogSpec{ Log: plog.LogSpec{
Level: plog.LevelAll, Level: plog.LevelAll,
@@ -801,6 +810,27 @@ func TestFromPath(t *testing.T) {
`), `),
wantError: `validate kubeCertAgent: runAsGroup must be 0 or greater (instead of -1)`, wantError: `validate kubeCertAgent: runAsGroup must be 0 or greater (instead of -1)`,
}, },
{
name: "invalid deploymentStrategyType",
yaml: here.Doc(`
---
names:
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
credentialIssuer: pinniped-config
apiService: pinniped-api
impersonationLoadBalancerService: impersonationLoadBalancerService-value
impersonationClusterIPService: impersonationClusterIPService-value
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
impersonationCACertificateSecret: impersonationCACertificateSecret-value
impersonationSignerSecret: impersonationSignerSecret-value
agentServiceAccount: agentServiceAccount-value
impersonationProxyServiceAccount: impersonationProxyServiceAccount-value
impersonationProxyLegacySecret: impersonationProxyLegacySecret-value
kubeCertAgent:
deploymentStrategyType: thisIsInvalid
`),
wantError: `validate kubeCertAgent: deploymentStrategyType must be one of [Recreate RollingUpdate] (instead of thisIsInvalid)`,
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {

View File

@@ -3,7 +3,11 @@
package concierge package concierge
import "go.pinniped.dev/internal/plog" import (
appsv1 "k8s.io/api/apps/v1"
"go.pinniped.dev/internal/plog"
)
const ( const (
Enabled = "enabled" Enabled = "enabled"
@@ -120,4 +124,9 @@ type KubeCertAgentSpec struct {
// The GID to run the entrypoint of the kube-cert-agent container. // The GID to run the entrypoint of the kube-cert-agent container.
// Defaults to 0 (root). No validation is performed on this value. // Defaults to 0 (root). No validation is performed on this value.
RunAsGroup *int64 `json:"runAsGroup"` RunAsGroup *int64 `json:"runAsGroup"`
// DeploymentStrategyType will be set as the agent Deployment's deployment strategy type.
// When nil, the Deployment will not specify any deployment strategy type, and will therefore have its
// deployment strategy type set by Kubernetes default behavior (currently RollingUpdate).
DeploymentStrategyType *appsv1.DeploymentStrategyType `json:"deploymentStrategyType"`
} }

View File

@@ -105,11 +105,16 @@ type AgentConfig struct {
// PriorityClassName optionally sets the PriorityClassName for the agent's pod. // PriorityClassName optionally sets the PriorityClassName for the agent's pod.
PriorityClassName string PriorityClassName string
// RunAsUser is the UID to run the entrypoint of the container process // RunAsUser is the UID to run the entrypoint of the container process.
RunAsUser *int64 RunAsUser *int64
// RunAsGroup is the GID to run the entrypoint of the container process // RunAsGroup is the GID to run the entrypoint of the container process.
RunAsGroup *int64 RunAsGroup *int64
// DeploymentStrategyType will be set as the agent Deployment's deployment strategy type.
// When nil, the Deployment will not specify any deployment strategy type, and will therefore have its
// deployment strategy type set by Kubernetes default behavior (currently RollingUpdate).
DeploymentStrategyType *appsv1.DeploymentStrategyType
} }
// Only select using the unique label which will not match the pods of any other Deployment. // Only select using the unique label which will not match the pods of any other Deployment.
@@ -440,12 +445,14 @@ func (c *agentController) createOrUpdateDeployment(ctx context.Context, newestCo
desireTemplateLabelsUpdate := !apiequality.Semantic.DeepEqual(updatedDeployment.Spec.Template.Labels, existingDeployment.Spec.Template.Labels) desireTemplateLabelsUpdate := !apiequality.Semantic.DeepEqual(updatedDeployment.Spec.Template.Labels, existingDeployment.Spec.Template.Labels)
// The user might want to set PriorityClassName back to the default value of empty string. DeepDerivative() won't detect this case below. // The user might want to set PriorityClassName back to the default value of empty string. DeepDerivative() won't detect this case below.
desirePriorityClassNameUpdate := updatedDeployment.Spec.Template.Spec.PriorityClassName != existingDeployment.Spec.Template.Spec.PriorityClassName desirePriorityClassNameUpdate := updatedDeployment.Spec.Template.Spec.PriorityClassName != existingDeployment.Spec.Template.Spec.PriorityClassName
// The user might want to set deploymentStrategyType back to the default value. DeepDerivative() won't detect this case below.
desireDeploymentStrategyTypeUpdate := updatedDeployment.Spec.Strategy.Type != existingDeployment.Spec.Strategy.Type
// If the existing Deployment already matches our desired spec, we're done. // If the existing Deployment already matches our desired spec, we're done.
if apiequality.Semantic.DeepDerivative(updatedDeployment, existingDeployment) { if apiequality.Semantic.DeepDerivative(updatedDeployment, existingDeployment) {
// DeepDerivative allows the map fields of updatedDeployment to be a subset of existingDeployment, // DeepDerivative allows the map fields of updatedDeployment to be a subset of existingDeployment,
// but we want to check that certain of those map fields are exactly equal before deciding to skip the update. // but we want to check that certain of those map fields are exactly equal before deciding to skip the update.
if !desireSelectorUpdate && !desireTemplateLabelsUpdate && !desirePriorityClassNameUpdate { if !desireSelectorUpdate && !desireTemplateLabelsUpdate && !desirePriorityClassNameUpdate && !desireDeploymentStrategyTypeUpdate {
return nil // already equal enough, so skip update return nil // already equal enough, so skip update
} }
} }
@@ -614,6 +621,14 @@ func (c *agentController) getPodSecurityContext() *corev1.PodSecurityContext {
return podSecurityContext return podSecurityContext
} }
func (c *agentController) getDeploymentStrategy() appsv1.DeploymentStrategy {
s := appsv1.DeploymentStrategy{}
if c.cfg.DeploymentStrategyType != nil {
s.Type = *c.cfg.DeploymentStrategyType
}
return s
}
func (c *agentController) newAgentDeployment(controllerManagerPod *corev1.Pod) *appsv1.Deployment { func (c *agentController) newAgentDeployment(controllerManagerPod *corev1.Pod) *appsv1.Deployment {
var volumeMounts []corev1.VolumeMount var volumeMounts []corev1.VolumeMount
if len(controllerManagerPod.Spec.Containers) > 0 { if len(controllerManagerPod.Spec.Containers) > 0 {
@@ -699,6 +714,9 @@ func (c *agentController) newAgentDeployment(controllerManagerPod *corev1.Pod) *
// Setting MinReadySeconds prevents the agent pods from being churned too quickly by the deployments controller. // Setting MinReadySeconds prevents the agent pods from being churned too quickly by the deployments controller.
MinReadySeconds: 10, MinReadySeconds: 10,
// Allow the user to optionally configure the deployment strategy type.
Strategy: c.getDeploymentStrategy(),
}, },
} }
} }

View File

@@ -232,6 +232,7 @@ func TestAgentController(t *testing.T) {
agentPriorityClassName string agentPriorityClassName string
runAsUser *int64 runAsUser *int64
runAsGroup *int64 runAsGroup *int64
deploymentStrategyType *appsv1.DeploymentStrategyType
pinnipedObjects []runtime.Object pinnipedObjects []runtime.Object
kubeObjects []runtime.Object kubeObjects []runtime.Object
addKubeReactions func(*kubefake.Clientset) addKubeReactions func(*kubefake.Clientset)
@@ -510,6 +511,137 @@ func TestAgentController(t *testing.T) {
LastUpdateTime: metav1.NewTime(now), LastUpdateTime: metav1.NewTime(now),
}, },
}, },
{
name: "created new deployment with overridden deploymentStrategyType, no agent pods running yet",
deploymentStrategyType: ptr.To(appsv1.RecreateDeploymentStrategyType),
pinnipedObjects: []runtime.Object{
initialCredentialIssuer,
},
kubeObjects: []runtime.Object{
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "kube-system",
Name: "kube-controller-manager-3",
Labels: map[string]string{"component": "kube-controller-manager"},
CreationTimestamp: metav1.NewTime(now.Add(-1 * time.Hour)),
},
Spec: corev1.PodSpec{NodeName: schedulableControllerManagerNode.Name},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
},
healthyKubeControllerManagerPod,
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "kube-system",
Name: "kube-controller-manager-2",
Labels: map[string]string{"component": "kube-controller-manager"},
CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Hour)),
},
Spec: corev1.PodSpec{NodeName: schedulableControllerManagerNode.Name},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
},
pendingAgentPod,
schedulableControllerManagerNode,
},
wantDistinctErrors: []string{
"could not find a healthy agent pod (1 candidate)",
},
alsoAllowUndesiredDistinctErrors: []string{
// due to the high amount of nondeterminism in this test, this error will sometimes also happen, but is not required to happen
`could not ensure agent deployment: deployments.apps "pinniped-concierge-kube-cert-agent" already exists`,
},
wantDistinctLogs: []string{
`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
},
wantAgentDeployment: modifiedHealthyHealthyAgentDeployment(func(deployment *appsv1.Deployment) {
deployment.Spec.Strategy = appsv1.DeploymentStrategy{
Type: appsv1.RecreateDeploymentStrategyType,
}
}),
wantDeploymentActionVerbs: []string{"list", "watch", "create"},
wantStrategy: &conciergeconfigv1alpha1.CredentialIssuerStrategy{
Type: conciergeconfigv1alpha1.KubeClusterSigningCertificateStrategyType,
Status: conciergeconfigv1alpha1.ErrorStrategyStatus,
Reason: conciergeconfigv1alpha1.CouldNotFetchKeyStrategyReason,
Message: "could not find a healthy agent pod (1 candidate)",
LastUpdateTime: metav1.NewTime(now),
},
},
{
name: "deployment exists, configmap is valid, exec succeeds, overridden deploymentStrategyType is updated into the deployment",
pinnipedObjects: []runtime.Object{
initialCredentialIssuer,
},
kubeObjects: []runtime.Object{
healthyKubeControllerManagerPod,
healthyAgentDeployment,
healthyAgentPod,
validClusterInfoConfigMap,
schedulableControllerManagerNode,
},
deploymentStrategyType: ptr.To(appsv1.RecreateDeploymentStrategyType),
mocks: mockExecSucceeds,
wantDistinctErrors: []string{""},
wantAgentDeployment: modifiedHealthyHealthyAgentDeployment(func(deployment *appsv1.Deployment) {
deployment.Spec.Strategy = appsv1.DeploymentStrategy{Type: appsv1.RecreateDeploymentStrategyType}
}),
wantDeploymentActionVerbs: []string{"list", "watch", "update"},
wantDistinctLogs: []string{
`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"updating existing deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).loadSigningKey","message":"successfully loaded signing key from agent pod into cache"}`,
},
wantStrategy: &conciergeconfigv1alpha1.CredentialIssuerStrategy{
Type: conciergeconfigv1alpha1.KubeClusterSigningCertificateStrategyType,
Status: conciergeconfigv1alpha1.SuccessStrategyStatus,
Reason: conciergeconfigv1alpha1.FetchedKeyStrategyReason,
Message: "key was fetched successfully",
LastUpdateTime: metav1.NewTime(now),
Frontend: &conciergeconfigv1alpha1.CredentialIssuerFrontend{
Type: conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType,
TokenCredentialRequestAPIInfo: &conciergeconfigv1alpha1.TokenCredentialRequestAPIInfo{
Server: "https://test-kubernetes-endpoint.example.com",
CertificateAuthorityData: "dGVzdC1rdWJlcm5ldGVzLWNh",
},
},
},
},
{
name: "deployment exists with a non-empty deploymentStrategyType, configmap is valid, exec succeeds, deploymentStrategyType config is null so deployment strategy is set back to zero value",
pinnipedObjects: []runtime.Object{
initialCredentialIssuer,
},
kubeObjects: []runtime.Object{
healthyKubeControllerManagerPod,
modifiedHealthyHealthyAgentDeployment(func(deployment *appsv1.Deployment) {
deployment.Spec.Strategy = appsv1.DeploymentStrategy{Type: appsv1.RecreateDeploymentStrategyType}
}),
healthyAgentPod,
validClusterInfoConfigMap,
schedulableControllerManagerNode,
},
deploymentStrategyType: nil,
mocks: mockExecSucceeds,
wantDistinctErrors: []string{""},
wantAgentDeployment: healthyAgentDeployment,
wantDeploymentActionVerbs: []string{"list", "watch", "update"},
wantDistinctLogs: []string{
`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"updating existing deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).loadSigningKey","message":"successfully loaded signing key from agent pod into cache"}`,
},
wantStrategy: &conciergeconfigv1alpha1.CredentialIssuerStrategy{
Type: conciergeconfigv1alpha1.KubeClusterSigningCertificateStrategyType,
Status: conciergeconfigv1alpha1.SuccessStrategyStatus,
Reason: conciergeconfigv1alpha1.FetchedKeyStrategyReason,
Message: "key was fetched successfully",
LastUpdateTime: metav1.NewTime(now),
Frontend: &conciergeconfigv1alpha1.CredentialIssuerFrontend{
Type: conciergeconfigv1alpha1.TokenCredentialRequestAPIFrontendType,
TokenCredentialRequestAPIInfo: &conciergeconfigv1alpha1.TokenCredentialRequestAPIInfo{
Server: "https://test-kubernetes-endpoint.example.com",
CertificateAuthorityData: "dGVzdC1rdWJlcm5ldGVzLWNh",
},
},
},
},
{ {
name: "created new deployment based on alternate supported controller-manager CLI flags, no agent pods running yet", name: "created new deployment based on alternate supported controller-manager CLI flags, no agent pods running yet",
pinnipedObjects: []runtime.Object{ pinnipedObjects: []runtime.Object{
@@ -1681,10 +1813,11 @@ func TestAgentController(t *testing.T) {
// Concierge Deployment, so we do not want it to exist on the Kube cert agent pods. // Concierge Deployment, so we do not want it to exist on the Kube cert agent pods.
"app": "anything", "app": "anything",
}, },
DiscoveryURLOverride: tt.discoveryURLOverride, DiscoveryURLOverride: tt.discoveryURLOverride,
PriorityClassName: tt.agentPriorityClassName, PriorityClassName: tt.agentPriorityClassName,
RunAsUser: tt.runAsUser, RunAsUser: tt.runAsUser,
RunAsGroup: tt.runAsGroup, RunAsGroup: tt.runAsGroup,
DeploymentStrategyType: tt.deploymentStrategyType,
}, },
&kubeclient.Client{Kubernetes: kubeClientset, PinnipedConcierge: conciergeClientset}, &kubeclient.Client{Kubernetes: kubeClientset, PinnipedConcierge: conciergeClientset},
kubeInformers.Core().V1().Pods(), kubeInformers.Core().V1().Pods(),

View File

@@ -143,6 +143,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol
PriorityClassName: c.KubeCertAgentConfig.PriorityClassName, PriorityClassName: c.KubeCertAgentConfig.PriorityClassName,
RunAsUser: c.KubeCertAgentConfig.RunAsUser, RunAsUser: c.KubeCertAgentConfig.RunAsUser,
RunAsGroup: c.KubeCertAgentConfig.RunAsGroup, RunAsGroup: c.KubeCertAgentConfig.RunAsGroup,
DeploymentStrategyType: c.KubeCertAgentConfig.DeploymentStrategyType,
} }
// Create controller manager. // Create controller manager.