mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-12 19:16:09 +00:00
Add kube-cert-agent controller for getting kube API keypair
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kubecertagent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
)
|
||||
|
||||
type annotaterController struct {
|
||||
agentInfo *Info
|
||||
k8sClient kubernetes.Interface
|
||||
podInformer corev1informers.PodInformer
|
||||
}
|
||||
|
||||
// NewAnnotaterController returns a controller that updates agent pods with the path to the kube
|
||||
// API's certificate and key.
|
||||
//
|
||||
// This controller will add annotations to agent pods, using the provided
|
||||
// agentInfo.CertPathAnnotation and agentInfo.KeyPathAnnotation annotation keys, with the best-guess
|
||||
// paths to the kube API's certificate and key.
|
||||
func NewAnnotaterController(
|
||||
agentInfo *Info,
|
||||
k8sClient kubernetes.Interface,
|
||||
podInformer corev1informers.PodInformer,
|
||||
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
||||
) controllerlib.Controller {
|
||||
return controllerlib.New(
|
||||
controllerlib.Config{
|
||||
Name: "kube-cert-agent-annotater-controller",
|
||||
Syncer: &annotaterController{
|
||||
agentInfo: agentInfo,
|
||||
k8sClient: k8sClient,
|
||||
podInformer: podInformer,
|
||||
},
|
||||
},
|
||||
withInformer(
|
||||
podInformer,
|
||||
pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool {
|
||||
return isControllerManagerPod(obj) || isAgentPod(obj, agentInfo.Template.Labels)
|
||||
}),
|
||||
controllerlib.InformerOption{},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Sync implements controllerlib.Syncer.
|
||||
func (c *annotaterController) Sync(ctx controllerlib.Context) error {
|
||||
agentSelector := labels.SelectorFromSet(c.agentInfo.Template.Labels)
|
||||
agentPods, err := c.podInformer.
|
||||
Lister().
|
||||
Pods(ControllerManagerNamespace).
|
||||
List(agentSelector)
|
||||
if err != nil {
|
||||
return fmt.Errorf("informer cannot list agent pods: %w", err)
|
||||
}
|
||||
|
||||
for _, agentPod := range agentPods {
|
||||
controllerManagerPod, err := findControllerManagerPod(agentPod, c.podInformer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if controllerManagerPod == nil {
|
||||
// The deleter will clean this orphaned agent.
|
||||
continue
|
||||
}
|
||||
|
||||
certPath, certPathOK := getContainerArgByName(controllerManagerPod, "cluster-signing-cert-file")
|
||||
keyPath, keyPathOK := getContainerArgByName(controllerManagerPod, "cluster-signing-key-file")
|
||||
if err := c.maybeUpdateAgentPod(
|
||||
ctx.Context,
|
||||
agentPod.Name,
|
||||
certPath,
|
||||
certPathOK,
|
||||
keyPath,
|
||||
keyPathOK,
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot update agent pod: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *annotaterController) maybeUpdateAgentPod(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
certPath string,
|
||||
certPathOK bool,
|
||||
keyPath string,
|
||||
keyPathOK bool,
|
||||
) error {
|
||||
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
agentPod, err := c.podInformer.Lister().Pods(ControllerManagerNamespace).Get(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (certPathOK && agentPod.Annotations[c.agentInfo.CertPathAnnotation] != certPath) ||
|
||||
(keyPathOK && agentPod.Annotations[c.agentInfo.KeyPathAnnotation] != keyPath) {
|
||||
if err := c.reallyUpdateAgentPod(
|
||||
ctx,
|
||||
agentPod,
|
||||
certPath,
|
||||
certPathOK,
|
||||
keyPath,
|
||||
keyPathOK,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *annotaterController) reallyUpdateAgentPod(
|
||||
ctx context.Context,
|
||||
agentPod *corev1.Pod,
|
||||
certPath string,
|
||||
certPathOK bool,
|
||||
keyPath string,
|
||||
keyPathOK bool,
|
||||
) error {
|
||||
// Create a deep copy of the agent pod since it is coming straight from the cache.
|
||||
updatedAgentPod := agentPod.DeepCopy()
|
||||
if updatedAgentPod.Annotations == nil {
|
||||
updatedAgentPod.Annotations = make(map[string]string)
|
||||
}
|
||||
if certPathOK {
|
||||
updatedAgentPod.Annotations[c.agentInfo.CertPathAnnotation] = certPath
|
||||
}
|
||||
if keyPathOK {
|
||||
updatedAgentPod.Annotations[c.agentInfo.KeyPathAnnotation] = keyPath
|
||||
}
|
||||
|
||||
klog.InfoS(
|
||||
"updating agent pod annotations",
|
||||
"pod",
|
||||
klog.KObj(updatedAgentPod),
|
||||
"certPath",
|
||||
certPath,
|
||||
"keyPath",
|
||||
keyPath,
|
||||
)
|
||||
_, err := c.k8sClient.
|
||||
CoreV1().
|
||||
Pods(ControllerManagerNamespace).
|
||||
Update(ctx, updatedAgentPod, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func getContainerArgByName(pod *corev1.Pod, name string) (string, bool) {
|
||||
for _, container := range pod.Spec.Containers {
|
||||
flagset := pflag.NewFlagSet("", pflag.ContinueOnError)
|
||||
flagset.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
|
||||
var val string
|
||||
flagset.StringVar(&val, name, "", "")
|
||||
_ = flagset.Parse(append(container.Command, container.Args...))
|
||||
if val != "" {
|
||||
return val, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kubecertagent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sclevine/spec"
|
||||
"github.com/sclevine/spec/report"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
func TestAnnotaterControllerFilter(t *testing.T) {
|
||||
runFilterTest(
|
||||
t,
|
||||
"AnnotaterControllerFilter",
|
||||
func(
|
||||
agentPodTemplate *corev1.Pod,
|
||||
podsInformer corev1informers.PodInformer,
|
||||
observableWithInformerOption *testutil.ObservableWithInformerOption,
|
||||
) {
|
||||
_ = NewAnnotaterController(
|
||||
&Info{
|
||||
Template: agentPodTemplate,
|
||||
},
|
||||
nil, // k8sClient, shouldn't matter
|
||||
podsInformer,
|
||||
observableWithInformerOption.WithInformer,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestAnnotaterControllerSync(t *testing.T) {
|
||||
spec.Run(t, "AnnotaterControllerSync", func(t *testing.T, when spec.G, it spec.S) {
|
||||
const kubeSystemNamespace = "kube-system"
|
||||
|
||||
const (
|
||||
certPath = "some-cert-path"
|
||||
certPathAnnotation = "some-cert-path-annotation"
|
||||
|
||||
keyPath = "some-key-path"
|
||||
keyPathAnnotation = "some-key-path-annotation"
|
||||
)
|
||||
|
||||
var r *require.Assertions
|
||||
|
||||
var subject controllerlib.Controller
|
||||
var kubeAPIClient *kubernetesfake.Clientset
|
||||
var kubeInformerClient *kubernetesfake.Clientset
|
||||
var kubeInformers kubeinformers.SharedInformerFactory
|
||||
var timeoutContext context.Context
|
||||
var timeoutContextCancel context.CancelFunc
|
||||
var syncContext *controllerlib.Context
|
||||
|
||||
agentPodTemplate := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "some-agent-name-",
|
||||
Labels: map[string]string{
|
||||
"some-label-key": "some-label-value",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Image: "some-agent-image",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
controllerManagerPod := &corev1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: corev1.SchemeGroupVersion.String(),
|
||||
Kind: "Pod",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: kubeSystemNamespace,
|
||||
Name: "some-controller-manager-name",
|
||||
Labels: map[string]string{
|
||||
"component": "kube-controller-manager",
|
||||
},
|
||||
UID: types.UID("some-controller-manager-uid"),
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Image: "some-controller-manager-image",
|
||||
Command: []string{
|
||||
"kube-controller-manager",
|
||||
"--cluster-signing-cert-file=" + certPath,
|
||||
"--cluster-signing-key-file=" + keyPath,
|
||||
},
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "some-volume-mount-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
NodeName: "some-node-name",
|
||||
NodeSelector: map[string]string{
|
||||
"some-node-selector-key": "some-node-selector-value",
|
||||
},
|
||||
Tolerations: []corev1.Toleration{
|
||||
{
|
||||
Key: "some-toleration",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
},
|
||||
}
|
||||
|
||||
// fnv 32a hash of controller-manager uid
|
||||
controllerManagerPodHash := "fbb0addd"
|
||||
agentPod := agentPodTemplate.DeepCopy()
|
||||
agentPod.Namespace = kubeSystemNamespace
|
||||
agentPod.Name += controllerManagerPodHash
|
||||
agentPod.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
Name: controllerManagerPod.Name,
|
||||
UID: controllerManagerPod.UID,
|
||||
Controller: boolPtr(true),
|
||||
BlockOwnerDeletion: boolPtr(true),
|
||||
},
|
||||
}
|
||||
agentPod.Spec.Containers[0].VolumeMounts = controllerManagerPod.Spec.Containers[0].VolumeMounts
|
||||
agentPod.Spec.RestartPolicy = corev1.RestartPolicyNever
|
||||
agentPod.Spec.AutomountServiceAccountToken = boolPtr(false)
|
||||
agentPod.Spec.NodeName = controllerManagerPod.Spec.NodeName
|
||||
agentPod.Spec.NodeSelector = controllerManagerPod.Spec.NodeSelector
|
||||
agentPod.Spec.Tolerations = controllerManagerPod.Spec.Tolerations
|
||||
|
||||
podsGVR := schema.GroupVersionResource{
|
||||
Group: corev1.SchemeGroupVersion.Group,
|
||||
Version: corev1.SchemeGroupVersion.Version,
|
||||
Resource: "pods",
|
||||
}
|
||||
|
||||
// Defer starting the informers until the last possible moment so that the
|
||||
// nested Before's can keep adding things to the informer caches.
|
||||
var startInformersAndController = func() {
|
||||
// Set this at the last second to allow for injection of server override.
|
||||
subject = NewAnnotaterController(
|
||||
&Info{
|
||||
Template: agentPodTemplate,
|
||||
CertPathAnnotation: certPathAnnotation,
|
||||
KeyPathAnnotation: keyPathAnnotation,
|
||||
},
|
||||
kubeAPIClient,
|
||||
kubeInformers.Core().V1().Pods(),
|
||||
controllerlib.WithInformer,
|
||||
)
|
||||
|
||||
// Set this at the last second to support calling subject.Name().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: kubeSystemNamespace,
|
||||
Name: "should-not-matter",
|
||||
},
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeInformers.Start(timeoutContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
|
||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||
kubeAPIClient = kubernetesfake.NewSimpleClientset()
|
||||
|
||||
// Add a pod into the test that doesn't matter to make sure we don't accidentally trigger any
|
||||
// logic on this thing.
|
||||
ignorablePod := corev1.Pod{}
|
||||
ignorablePod.Name = "some-ignorable-pod"
|
||||
r.NoError(kubeInformerClient.Tracker().Add(&ignorablePod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(&ignorablePod))
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
})
|
||||
|
||||
when("there is an agent pod without annotations set", func() {
|
||||
it.Before(func() {
|
||||
r.NoError(kubeInformerClient.Tracker().Add(agentPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(agentPod))
|
||||
})
|
||||
|
||||
when("there is a matching controller manager pod", func() {
|
||||
it.Before(func() {
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("updates the annotations according to the controller manager pod", func() {
|
||||
startInformersAndController()
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
|
||||
updatedAgentPod := agentPod.DeepCopy()
|
||||
updatedAgentPod.Annotations = make(map[string]string)
|
||||
updatedAgentPod.Annotations[certPathAnnotation] = certPath
|
||||
updatedAgentPod.Annotations[keyPathAnnotation] = keyPath
|
||||
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewUpdateAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
updatedAgentPod,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("there is a controller manager pod with CLI flag values separated by spaces", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.Spec.Containers[0].Command = []string{
|
||||
"kube-controller-manager",
|
||||
"--cluster-signing-cert-file", certPath,
|
||||
"--cluster-signing-key-file", keyPath,
|
||||
}
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("updates the annotations according to the controller manager pod", func() {
|
||||
startInformersAndController()
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
|
||||
updatedAgentPod := agentPod.DeepCopy()
|
||||
updatedAgentPod.Annotations = make(map[string]string)
|
||||
updatedAgentPod.Annotations[certPathAnnotation] = certPath
|
||||
updatedAgentPod.Annotations[keyPathAnnotation] = keyPath
|
||||
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewUpdateAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
updatedAgentPod,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("there is a controller manager pod with unparsable CLI flags", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.Spec.Containers[0].Command = []string{
|
||||
"kube-controller-manager",
|
||||
"--cluster-signing-cert-file-blah", certPath,
|
||||
"--cluster-signing-key-file-blah", keyPath,
|
||||
}
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("does not update any annotations", func() {
|
||||
startInformersAndController()
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
r.Equal(
|
||||
[]coretesting.Action{},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("there is a controller manager pod with unparsable cert CLI flag", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.Spec.Containers[0].Command = []string{
|
||||
"kube-controller-manager",
|
||||
"--cluster-signing-cert-file-blah", certPath,
|
||||
"--cluster-signing-key-file", keyPath,
|
||||
}
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("updates the key annotation", func() {
|
||||
startInformersAndController()
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
|
||||
updatedAgentPod := agentPod.DeepCopy()
|
||||
updatedAgentPod.Annotations = make(map[string]string)
|
||||
updatedAgentPod.Annotations[keyPathAnnotation] = keyPath
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewUpdateAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
updatedAgentPod,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("there is a controller manager pod with unparsable keey CLI flag", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.Spec.Containers[0].Command = []string{
|
||||
"kube-controller-manager",
|
||||
"--cluster-signing-cert-file", certPath,
|
||||
"--cluster-signing-key-file-blah", keyPath,
|
||||
}
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("updates the cert annotation", func() {
|
||||
startInformersAndController()
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
|
||||
updatedAgentPod := agentPod.DeepCopy()
|
||||
updatedAgentPod.Annotations = make(map[string]string)
|
||||
updatedAgentPod.Annotations[certPathAnnotation] = certPath
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewUpdateAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
updatedAgentPod,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("there is a non-matching controller manager pod via uid", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.UID = "some-other-controller-manager-uid"
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("does nothing; the deleter will delete this pod to trigger resync", func() {
|
||||
startInformersAndController()
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
r.Equal(
|
||||
[]coretesting.Action{},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("there is a non-matching controller manager pod via name", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.Name = "some-other-controller-manager-name"
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("does nothing; the deleter will delete this pod to trigger resync", func() {
|
||||
startInformersAndController()
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
r.Equal(
|
||||
[]coretesting.Action{},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
when("there is an agent pod with correct annotations set", func() {
|
||||
it.Before(func() {
|
||||
agentPod.Annotations = make(map[string]string)
|
||||
agentPod.Annotations[certPathAnnotation] = certPath
|
||||
agentPod.Annotations[keyPathAnnotation] = keyPath
|
||||
r.NoError(kubeInformerClient.Tracker().Add(agentPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(agentPod))
|
||||
})
|
||||
|
||||
when("there is a matching controller manager pod", func() {
|
||||
it.Before(func() {
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("does nothing since the pod is up to date", func() {
|
||||
startInformersAndController()
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
r.Equal(
|
||||
[]coretesting.Action{},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
when("there is an agent pod with the wrong cert annotation", func() {
|
||||
it.Before(func() {
|
||||
agentPod.Annotations = make(map[string]string)
|
||||
agentPod.Annotations[certPathAnnotation] = "wrong"
|
||||
agentPod.Annotations[keyPathAnnotation] = keyPath
|
||||
r.NoError(kubeInformerClient.Tracker().Add(agentPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(agentPod))
|
||||
})
|
||||
|
||||
when("there is a matching controller manager pod", func() {
|
||||
it.Before(func() {
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("updates the agent with the correct cert annotation", func() {
|
||||
startInformersAndController()
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
|
||||
updatedAgentPod := agentPod.DeepCopy()
|
||||
updatedAgentPod.Annotations[certPathAnnotation] = certPath
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewUpdateAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
updatedAgentPod,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
when("there is an agent pod with the wrong key annotation", func() {
|
||||
it.Before(func() {
|
||||
agentPod.Annotations = make(map[string]string)
|
||||
agentPod.Annotations[certPathAnnotation] = certPath
|
||||
agentPod.Annotations[keyPathAnnotation] = "key"
|
||||
r.NoError(kubeInformerClient.Tracker().Add(agentPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(agentPod))
|
||||
})
|
||||
|
||||
when("there is a matching controller manager pod", func() {
|
||||
it.Before(func() {
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("updates the agent with the correct key annotation", func() {
|
||||
startInformersAndController()
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
|
||||
updatedAgentPod := agentPod.DeepCopy()
|
||||
updatedAgentPod.Annotations[keyPathAnnotation] = keyPath
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewUpdateAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
updatedAgentPod,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kubecertagent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
)
|
||||
|
||||
type createrController struct {
|
||||
agentInfo *Info
|
||||
k8sClient kubernetes.Interface
|
||||
podInformer corev1informers.PodInformer
|
||||
}
|
||||
|
||||
// NewCreaterController returns a controller that creates new kube-cert-agent pods for every known
|
||||
// kube-controller-manager pod.
|
||||
//
|
||||
// This controller only uses the Template field of the provided agentInfo.
|
||||
func NewCreaterController(
|
||||
agentInfo *Info,
|
||||
k8sClient kubernetes.Interface,
|
||||
podInformer corev1informers.PodInformer,
|
||||
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
||||
) controllerlib.Controller {
|
||||
return controllerlib.New(
|
||||
controllerlib.Config{
|
||||
//nolint: misspell
|
||||
Name: "kube-cert-agent-creater-controller",
|
||||
Syncer: &createrController{
|
||||
agentInfo: agentInfo,
|
||||
k8sClient: k8sClient,
|
||||
podInformer: podInformer,
|
||||
},
|
||||
},
|
||||
withInformer(
|
||||
podInformer,
|
||||
pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool {
|
||||
return isControllerManagerPod(obj) || isAgentPod(obj, agentInfo.Template.Labels)
|
||||
}),
|
||||
controllerlib.InformerOption{},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Sync implements controllerlib.Syncer.
|
||||
func (c *createrController) Sync(ctx controllerlib.Context) error {
|
||||
controllerManagerSelector, err := labels.Parse("component=kube-controller-manager")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create controller manager selector: %w", err)
|
||||
}
|
||||
|
||||
controllerManagerPods, err := c.podInformer.Lister().List(controllerManagerSelector)
|
||||
if err != nil {
|
||||
return fmt.Errorf("informer cannot list controller manager pods: %w", err)
|
||||
}
|
||||
|
||||
for _, controllerManagerPod := range controllerManagerPods {
|
||||
agentPod, err := findAgentPod(controllerManagerPod, c.podInformer, c.agentInfo.Template.Labels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agentPod == nil {
|
||||
agentPod = newAgentPod(controllerManagerPod, c.agentInfo.Template)
|
||||
|
||||
klog.InfoS(
|
||||
"creating agent pod",
|
||||
"pod",
|
||||
klog.KObj(agentPod),
|
||||
"controller",
|
||||
klog.KObj(controllerManagerPod),
|
||||
)
|
||||
_, err := c.k8sClient.CoreV1().
|
||||
Pods(ControllerManagerNamespace).
|
||||
Create(ctx.Context, agentPod, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create agent pod: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The deleter controller handles the case where the expected fields do not match in the agent
|
||||
// pod.
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kubecertagent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sclevine/spec"
|
||||
"github.com/sclevine/spec/report"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
func TestCreaterControllerFilter(t *testing.T) {
|
||||
runFilterTest(
|
||||
t,
|
||||
"CreaterControllerFilter",
|
||||
func(
|
||||
agentPodTemplate *corev1.Pod,
|
||||
podsInformer corev1informers.PodInformer,
|
||||
observableWithInformerOption *testutil.ObservableWithInformerOption,
|
||||
) {
|
||||
_ = NewCreaterController(
|
||||
&Info{
|
||||
Template: agentPodTemplate,
|
||||
},
|
||||
nil, // k8sClient, shouldn't matter
|
||||
podsInformer,
|
||||
observableWithInformerOption.WithInformer,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCreaterControllerSync(t *testing.T) {
|
||||
spec.Run(t, "CreaterControllerSync", func(t *testing.T, when spec.G, it spec.S) {
|
||||
const kubeSystemNamespace = "kube-system"
|
||||
|
||||
var r *require.Assertions
|
||||
|
||||
var subject controllerlib.Controller
|
||||
var kubeAPIClient *kubernetesfake.Clientset
|
||||
var kubeInformerClient *kubernetesfake.Clientset
|
||||
var kubeInformers kubeinformers.SharedInformerFactory
|
||||
var timeoutContext context.Context
|
||||
var timeoutContextCancel context.CancelFunc
|
||||
var syncContext *controllerlib.Context
|
||||
|
||||
agentPodTemplate := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "some-agent-name-",
|
||||
Labels: map[string]string{
|
||||
"some-label-key": "some-label-value",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Image: "some-agent-image",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
controllerManagerPod := &corev1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: corev1.SchemeGroupVersion.String(),
|
||||
Kind: "Pod",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: kubeSystemNamespace,
|
||||
Name: "some-controller-manager-name",
|
||||
Labels: map[string]string{
|
||||
"component": "kube-controller-manager",
|
||||
},
|
||||
UID: types.UID("some-controller-manager-uid"),
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Image: "some-controller-manager-image",
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "some-volume-mount-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
NodeName: "some-node-name",
|
||||
NodeSelector: map[string]string{
|
||||
"some-node-selector-key": "some-node-selector-value",
|
||||
},
|
||||
Tolerations: []corev1.Toleration{
|
||||
{
|
||||
Key: "some-toleration",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
},
|
||||
}
|
||||
|
||||
// fnv 32a hash of controller-manager uid
|
||||
controllerManagerPodHash := "fbb0addd"
|
||||
agentPod := agentPodTemplate.DeepCopy()
|
||||
agentPod.Namespace = kubeSystemNamespace
|
||||
agentPod.Name += controllerManagerPodHash
|
||||
agentPod.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
Name: controllerManagerPod.Name,
|
||||
UID: controllerManagerPod.UID,
|
||||
Controller: boolPtr(true),
|
||||
BlockOwnerDeletion: boolPtr(true),
|
||||
},
|
||||
}
|
||||
agentPod.Spec.Containers[0].VolumeMounts = controllerManagerPod.Spec.Containers[0].VolumeMounts
|
||||
agentPod.Spec.RestartPolicy = corev1.RestartPolicyNever
|
||||
agentPod.Spec.AutomountServiceAccountToken = boolPtr(false)
|
||||
agentPod.Spec.NodeName = controllerManagerPod.Spec.NodeName
|
||||
agentPod.Spec.NodeSelector = controllerManagerPod.Spec.NodeSelector
|
||||
agentPod.Spec.Tolerations = controllerManagerPod.Spec.Tolerations
|
||||
|
||||
podsGVR := schema.GroupVersionResource{
|
||||
Group: corev1.SchemeGroupVersion.Group,
|
||||
Version: corev1.SchemeGroupVersion.Version,
|
||||
Resource: "pods",
|
||||
}
|
||||
|
||||
// Defer starting the informers until the last possible moment so that the
|
||||
// nested Before's can keep adding things to the informer caches.
|
||||
var startInformersAndController = func() {
|
||||
// Set this at the last second to allow for injection of server override.
|
||||
subject = NewCreaterController(
|
||||
&Info{
|
||||
Template: agentPodTemplate,
|
||||
},
|
||||
kubeAPIClient,
|
||||
kubeInformers.Core().V1().Pods(),
|
||||
controllerlib.WithInformer,
|
||||
)
|
||||
|
||||
// Set this at the last second to support calling subject.Name().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: kubeSystemNamespace,
|
||||
Name: "should-not-matter",
|
||||
},
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeInformers.Start(timeoutContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
|
||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||
kubeAPIClient = kubernetesfake.NewSimpleClientset()
|
||||
|
||||
// Add a pod into the test that doesn't matter to make sure we don't accidentally trigger any
|
||||
// logic on this thing.
|
||||
ignorablePod := corev1.Pod{}
|
||||
ignorablePod.Name = "some-ignorable-pod"
|
||||
r.NoError(kubeInformerClient.Tracker().Add(&ignorablePod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(&ignorablePod))
|
||||
|
||||
// Add another valid agent pod to make sure our logic works for just the pod we care about.
|
||||
otherAgentPod := agentPod.DeepCopy()
|
||||
otherAgentPod.Name = "some-other-agent"
|
||||
otherAgentPod.OwnerReferences[0].Name = "some-other-controller-manager-name"
|
||||
otherAgentPod.OwnerReferences[0].UID = "some-other-controller-manager-uid"
|
||||
r.NoError(kubeInformerClient.Tracker().Add(otherAgentPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(otherAgentPod))
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
})
|
||||
|
||||
when("there is a controller manager pod", func() {
|
||||
it.Before(func() {
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
when("there is a matching agent pod", func() {
|
||||
it.Before(func() {
|
||||
r.NoError(kubeInformerClient.Tracker().Add(agentPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(agentPod))
|
||||
})
|
||||
|
||||
it("does nothing", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("there is a non-matching agent pod", func() {
|
||||
it.Before(func() {
|
||||
nonMatchingAgentPod := agentPod.DeepCopy()
|
||||
nonMatchingAgentPod.Name = "some-agent-name-85da432e"
|
||||
nonMatchingAgentPod.OwnerReferences[0].UID = "some-non-matching-uid"
|
||||
r.NoError(kubeInformerClient.Tracker().Add(nonMatchingAgentPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(nonMatchingAgentPod))
|
||||
})
|
||||
|
||||
it("creates a matching agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewCreateAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("there is no matching agent pod", func() {
|
||||
it("creates a matching agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewCreateAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
when("there is no controller manager pod", func() {
|
||||
it("does nothing; the deleter controller will cleanup any leftover agent pods", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kubecertagent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
)
|
||||
|
||||
type deleterController struct {
|
||||
agentInfo *Info
|
||||
k8sClient kubernetes.Interface
|
||||
podInformer corev1informers.PodInformer
|
||||
}
|
||||
|
||||
// NewDeleterController returns a controller that deletes any kube-cert-agent pods that are out of
|
||||
// sync with the known kube-controller-manager pods.
|
||||
//
|
||||
// This controller only uses the Template field of the provided agentInfo.
|
||||
func NewDeleterController(
|
||||
agentInfo *Info,
|
||||
k8sClient kubernetes.Interface,
|
||||
podInformer corev1informers.PodInformer,
|
||||
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
||||
) controllerlib.Controller {
|
||||
return controllerlib.New(
|
||||
controllerlib.Config{
|
||||
Name: "kube-cert-agent-deleter-controller",
|
||||
Syncer: &deleterController{
|
||||
agentInfo: agentInfo,
|
||||
k8sClient: k8sClient,
|
||||
podInformer: podInformer,
|
||||
},
|
||||
},
|
||||
withInformer(
|
||||
podInformer,
|
||||
pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool {
|
||||
return isControllerManagerPod(obj) || isAgentPod(obj, agentInfo.Template.Labels)
|
||||
}),
|
||||
controllerlib.InformerOption{},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Sync implements controllerlib.Syncer.
|
||||
func (c *deleterController) Sync(ctx controllerlib.Context) error {
|
||||
agentSelector := labels.SelectorFromSet(c.agentInfo.Template.Labels)
|
||||
agentPods, err := c.podInformer.
|
||||
Lister().
|
||||
Pods(ControllerManagerNamespace).
|
||||
List(agentSelector)
|
||||
if err != nil {
|
||||
return fmt.Errorf("informer cannot list agent pods: %w", err)
|
||||
}
|
||||
|
||||
for _, agentPod := range agentPods {
|
||||
controllerManagerPod, err := findControllerManagerPod(agentPod, c.podInformer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if controllerManagerPod == nil ||
|
||||
!isAgentPodUpToDate(agentPod, newAgentPod(controllerManagerPod, c.agentInfo.Template)) {
|
||||
klog.InfoS("deleting agent pod", "pod", klog.KObj(agentPod))
|
||||
err := c.k8sClient.
|
||||
CoreV1().
|
||||
Pods(ControllerManagerNamespace).
|
||||
Delete(ctx.Context, agentPod.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete agent pod: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kubecertagent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sclevine/spec"
|
||||
"github.com/sclevine/spec/report"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
func TestDeleterControllerFilter(t *testing.T) {
|
||||
runFilterTest(
|
||||
t,
|
||||
"DeleterControllerFilter",
|
||||
func(
|
||||
agentPodTemplate *corev1.Pod,
|
||||
podsInformer corev1informers.PodInformer,
|
||||
observableWithInformerOption *testutil.ObservableWithInformerOption,
|
||||
) {
|
||||
_ = NewDeleterController(
|
||||
&Info{
|
||||
Template: agentPodTemplate,
|
||||
},
|
||||
nil, // k8sClient, shouldn't matter
|
||||
podsInformer,
|
||||
observableWithInformerOption.WithInformer,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestDeleterControllerSync(t *testing.T) {
|
||||
spec.Run(t, "DeleterControllerSync", func(t *testing.T, when spec.G, it spec.S) {
|
||||
const kubeSystemNamespace = "kube-system"
|
||||
|
||||
var r *require.Assertions
|
||||
|
||||
var subject controllerlib.Controller
|
||||
var kubeAPIClient *kubernetesfake.Clientset
|
||||
var kubeInformerClient *kubernetesfake.Clientset
|
||||
var kubeInformers kubeinformers.SharedInformerFactory
|
||||
var timeoutContext context.Context
|
||||
var timeoutContextCancel context.CancelFunc
|
||||
var syncContext *controllerlib.Context
|
||||
|
||||
agentPodTemplate := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "some-agent-name-",
|
||||
Labels: map[string]string{
|
||||
"some-label-key": "some-label-value",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Image: "some-agent-image",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
controllerManagerPod := &corev1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: corev1.SchemeGroupVersion.String(),
|
||||
Kind: "Pod",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: kubeSystemNamespace,
|
||||
Name: "some-controller-manager-name",
|
||||
Labels: map[string]string{
|
||||
"component": "kube-controller-manager",
|
||||
},
|
||||
UID: types.UID("some-controller-manager-uid"),
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Image: "some-controller-manager-image",
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "some-volume-mount-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
NodeName: "some-node-name",
|
||||
NodeSelector: map[string]string{
|
||||
"some-node-selector-key": "some-node-selector-value",
|
||||
},
|
||||
Tolerations: []corev1.Toleration{
|
||||
{
|
||||
Key: "some-toleration",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
},
|
||||
}
|
||||
|
||||
podsGVR := schema.GroupVersionResource{
|
||||
Group: corev1.SchemeGroupVersion.Group,
|
||||
Version: corev1.SchemeGroupVersion.Version,
|
||||
Resource: "pods",
|
||||
}
|
||||
|
||||
// fnv 32a hash of controller-manager uid
|
||||
controllerManagerPodHash := "fbb0addd"
|
||||
agentPod := agentPodTemplate.DeepCopy()
|
||||
agentPod.Namespace = kubeSystemNamespace
|
||||
agentPod.Name += controllerManagerPodHash
|
||||
agentPod.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
Name: controllerManagerPod.Name,
|
||||
UID: controllerManagerPod.UID,
|
||||
Controller: boolPtr(true),
|
||||
BlockOwnerDeletion: boolPtr(true),
|
||||
},
|
||||
}
|
||||
agentPod.Spec.Containers[0].VolumeMounts = controllerManagerPod.Spec.Containers[0].VolumeMounts
|
||||
agentPod.Spec.RestartPolicy = corev1.RestartPolicyNever
|
||||
agentPod.Spec.AutomountServiceAccountToken = boolPtr(false)
|
||||
agentPod.Spec.NodeName = controllerManagerPod.Spec.NodeName
|
||||
agentPod.Spec.NodeSelector = controllerManagerPod.Spec.NodeSelector
|
||||
agentPod.Spec.Tolerations = controllerManagerPod.Spec.Tolerations
|
||||
|
||||
// Defer starting the informers until the last possible moment so that the
|
||||
// nested Before's can keep adding things to the informer caches.
|
||||
var startInformersAndController = func() {
|
||||
// Set this at the last second to allow for injection of server override.
|
||||
subject = NewDeleterController(
|
||||
&Info{
|
||||
Template: agentPodTemplate,
|
||||
},
|
||||
kubeAPIClient,
|
||||
kubeInformers.Core().V1().Pods(),
|
||||
controllerlib.WithInformer,
|
||||
)
|
||||
|
||||
// Set this at the last second to support calling subject.Name().
|
||||
syncContext = &controllerlib.Context{
|
||||
Context: timeoutContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: kubeSystemNamespace,
|
||||
Name: "should-not-matter",
|
||||
},
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
kubeInformers.Start(timeoutContext.Done())
|
||||
controllerlib.TestRunSynchronously(t, subject)
|
||||
}
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
|
||||
|
||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||
kubeAPIClient = kubernetesfake.NewSimpleClientset()
|
||||
|
||||
// Add an pod into the test that doesn't matter to make sure we don't accidentally
|
||||
// trigger any logic on this thing.
|
||||
ignorablePod := corev1.Pod{}
|
||||
ignorablePod.Name = "some-ignorable-pod"
|
||||
r.NoError(kubeInformerClient.Tracker().Add(&ignorablePod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(&ignorablePod))
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
timeoutContextCancel()
|
||||
})
|
||||
|
||||
when("there is an agent pod", func() {
|
||||
it.Before(func() {
|
||||
r.NoError(kubeInformerClient.Tracker().Add(agentPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(agentPod))
|
||||
})
|
||||
|
||||
when("there is a matching controller manager pod", func() {
|
||||
it.Before(func() {
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("does nothing", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("there is a non-matching controller manager pod via uid", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.UID = "some-other-controller-manager-uid"
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("deletes the agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewDeleteAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod.Name,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("there is a non-matching controller manager pod via name", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.Name = "some-other-controller-manager-name"
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("deletes the agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewDeleteAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod.Name,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("the agent pod is out of sync with the controller manager via volume mounts", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
|
||||
{
|
||||
Name: "some-other-volume-mount",
|
||||
},
|
||||
}
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("deletes the agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewDeleteAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod.Name,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("the agent pod is out of sync with the controller manager via volumes", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "some-other-volume",
|
||||
},
|
||||
}
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("deletes the agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewDeleteAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod.Name,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("the agent pod is out of sync with the controller manager via node selector", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.Spec.NodeSelector = map[string]string{
|
||||
"some-other-node-selector-key": "some-other-node-selector-value",
|
||||
}
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("deletes the agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewDeleteAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod.Name,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("the agent pod is out of sync with the controller manager via node name", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.Spec.NodeName = "some-other-node-name"
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("deletes the agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewDeleteAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod.Name,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("the agent pod is out of sync with the controller manager via tolerations", func() {
|
||||
it.Before(func() {
|
||||
controllerManagerPod.Spec.Tolerations = []corev1.Toleration{
|
||||
{
|
||||
Key: "some-other-toleration-key",
|
||||
},
|
||||
}
|
||||
r.NoError(kubeInformerClient.Tracker().Add(controllerManagerPod))
|
||||
r.NoError(kubeAPIClient.Tracker().Add(controllerManagerPod))
|
||||
})
|
||||
|
||||
it("deletes the agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewDeleteAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod.Name,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("the agent pod is out of sync via restart policy", func() {
|
||||
it.Before(func() {
|
||||
updatedAgentPod := agentPod.DeepCopy()
|
||||
updatedAgentPod.Spec.RestartPolicy = corev1.RestartPolicyAlways
|
||||
r.NoError(kubeInformerClient.Tracker().Update(podsGVR, updatedAgentPod, updatedAgentPod.Namespace))
|
||||
r.NoError(kubeAPIClient.Tracker().Update(podsGVR, updatedAgentPod, updatedAgentPod.Namespace))
|
||||
})
|
||||
|
||||
it.Pend("deletes the agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewDeleteAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod.Name,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("the agent pod is out of sync via automount service account tokem", func() {
|
||||
it.Before(func() {
|
||||
agentPod.Spec.AutomountServiceAccountToken = boolPtr(true)
|
||||
r.NoError(kubeInformerClient.Tracker().Update(podsGVR, agentPod, agentPod.Namespace))
|
||||
r.NoError(kubeAPIClient.Tracker().Update(podsGVR, agentPod, agentPod.Namespace))
|
||||
})
|
||||
|
||||
it.Pend("deletes the agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewDeleteAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod.Name,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
when("there is no matching controller manager pod", func() {
|
||||
it("deletes the agent pod", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{
|
||||
coretesting.NewDeleteAction(
|
||||
podsGVR,
|
||||
kubeSystemNamespace,
|
||||
agentPod.Name,
|
||||
),
|
||||
},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
when("there is no agent pod", func() {
|
||||
it("does nothing", func() {
|
||||
startInformersAndController()
|
||||
err := controllerlib.TestSync(t, subject, *syncContext)
|
||||
|
||||
r.NoError(err)
|
||||
r.Equal(
|
||||
[]coretesting.Action{},
|
||||
kubeAPIClient.Actions(),
|
||||
)
|
||||
})
|
||||
})
|
||||
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package kubecertagent provides controllers that ensure a set of pods (the kube-cert-agent), is
|
||||
// colocated with the Kubernetes controller manager so that Pinniped can access its signing keys.
|
||||
//
|
||||
// Note: the controllers use a filter that accepts all pods that look like the controller manager or
|
||||
// an agent pod, across any add/update/delete event. Each of the controllers only care about a
|
||||
// subset of these events in reality, but the liberal filter implementation serves as an MVP.
|
||||
package kubecertagent
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
// ControllerManagerNamespace is the assumed namespace of the kube-controller-manager pod(s).
|
||||
ControllerManagerNamespace = "kube-system"
|
||||
)
|
||||
|
||||
// Info holds necessary information about the agent pod. It was pulled out into a struct to have a
|
||||
// common parameter for each controller.
|
||||
type Info struct {
|
||||
// Template is an injection point for pod fields. The required pod fields are as follows.
|
||||
// .Name: serves as the name prefix for each of the agent pods
|
||||
// .Labels: serves as a way to filter for agent pods
|
||||
// .Spec.Containers[0].Image: serves as the container image for the agent pods
|
||||
// .Spec.Containers[0].Command: serves as the container command for the agent pods
|
||||
Template *corev1.Pod
|
||||
|
||||
// CertPathAnnotation is the name of the annotation key that will be used when setting the
|
||||
// best-guess path to the kube API's certificate.
|
||||
CertPathAnnotation string
|
||||
|
||||
// KeyPathAnnotation is the name of the annotation key that will be used when setting the
|
||||
// best-guess path to the kube API's private key.
|
||||
KeyPathAnnotation string
|
||||
}
|
||||
|
||||
func isControllerManagerPod(obj metav1.Object) bool {
|
||||
pod, ok := obj.(*corev1.Pod)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if pod.Labels == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
component, ok := pod.Labels["component"]
|
||||
if !ok || component != "kube-controller-manager" {
|
||||
return false
|
||||
}
|
||||
|
||||
if pod.Status.Phase != corev1.PodRunning {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isAgentPod(obj metav1.Object, agentLabels map[string]string) bool {
|
||||
for agentLabelKey, agentLabelValue := range agentLabels {
|
||||
v, ok := obj.GetLabels()[agentLabelKey]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if v != agentLabelValue {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func newAgentPod(
|
||||
controllerManagerPod *corev1.Pod,
|
||||
template *corev1.Pod,
|
||||
) *corev1.Pod {
|
||||
agentPod := template.DeepCopy()
|
||||
|
||||
agentPod.Name = fmt.Sprintf("%s%s", agentPod.Name, hash(controllerManagerPod))
|
||||
|
||||
// controller manager namespace because it is our owner
|
||||
agentPod.Namespace = ControllerManagerNamespace
|
||||
agentPod.OwnerReferences = []metav1.OwnerReference{
|
||||
*metav1.NewControllerRef(
|
||||
controllerManagerPod,
|
||||
schema.GroupVersionKind{
|
||||
Group: corev1.SchemeGroupVersion.Group,
|
||||
Version: corev1.SchemeGroupVersion.Version,
|
||||
Kind: "Pod",
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
agentPod.Spec.Containers[0].VolumeMounts = controllerManagerPod.Spec.Containers[0].VolumeMounts
|
||||
agentPod.Spec.Volumes = controllerManagerPod.Spec.Volumes
|
||||
agentPod.Spec.RestartPolicy = corev1.RestartPolicyNever
|
||||
agentPod.Spec.NodeSelector = controllerManagerPod.Spec.NodeSelector
|
||||
agentPod.Spec.AutomountServiceAccountToken = boolPtr(false)
|
||||
agentPod.Spec.NodeName = controllerManagerPod.Spec.NodeName
|
||||
agentPod.Spec.Tolerations = controllerManagerPod.Spec.Tolerations
|
||||
|
||||
return agentPod
|
||||
}
|
||||
|
||||
func isAgentPodUpToDate(actualAgentPod, expectedAgentPod *corev1.Pod) bool {
|
||||
return equality.Semantic.DeepEqual(
|
||||
actualAgentPod.Spec.Containers[0].VolumeMounts,
|
||||
expectedAgentPod.Spec.Containers[0].VolumeMounts,
|
||||
) &&
|
||||
equality.Semantic.DeepEqual(
|
||||
actualAgentPod.Spec.Volumes,
|
||||
expectedAgentPod.Spec.Volumes,
|
||||
) &&
|
||||
equality.Semantic.DeepEqual(
|
||||
actualAgentPod.Spec.RestartPolicy,
|
||||
expectedAgentPod.Spec.RestartPolicy,
|
||||
) &&
|
||||
equality.Semantic.DeepEqual(
|
||||
actualAgentPod.Spec.NodeSelector,
|
||||
expectedAgentPod.Spec.NodeSelector,
|
||||
) &&
|
||||
equality.Semantic.DeepEqual(
|
||||
actualAgentPod.Spec.AutomountServiceAccountToken,
|
||||
expectedAgentPod.Spec.AutomountServiceAccountToken,
|
||||
) &&
|
||||
equality.Semantic.DeepEqual(
|
||||
actualAgentPod.Spec.NodeName,
|
||||
expectedAgentPod.Spec.NodeName,
|
||||
) &&
|
||||
equality.Semantic.DeepEqual(
|
||||
actualAgentPod.Spec.Tolerations,
|
||||
expectedAgentPod.Spec.Tolerations,
|
||||
)
|
||||
}
|
||||
|
||||
func findAgentPod(
|
||||
controllerManagerPod *corev1.Pod,
|
||||
informer corev1informers.PodInformer,
|
||||
agentLabels map[string]string,
|
||||
) (*corev1.Pod, error) {
|
||||
agentSelector := labels.SelectorFromSet(agentLabels)
|
||||
agentPods, err := informer.
|
||||
Lister().
|
||||
Pods(ControllerManagerNamespace).
|
||||
List(agentSelector)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("informer cannot list agent pods: %w", err)
|
||||
}
|
||||
|
||||
for _, maybeAgentPod := range agentPods {
|
||||
maybeControllerManagerPod, err := findControllerManagerPod(
|
||||
maybeAgentPod,
|
||||
informer,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if maybeControllerManagerPod != nil &&
|
||||
maybeControllerManagerPod.UID == controllerManagerPod.UID {
|
||||
return maybeAgentPod, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func findControllerManagerPod(
|
||||
agentPod *corev1.Pod,
|
||||
informer corev1informers.PodInformer,
|
||||
) (*corev1.Pod, error) {
|
||||
controller := metav1.GetControllerOf(agentPod)
|
||||
if controller == nil {
|
||||
klog.InfoS("found orphan agent pod", "pod", klog.KObj(agentPod))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
maybeControllerManagerPod, err := informer.
|
||||
Lister().
|
||||
Pods(ControllerManagerNamespace).
|
||||
Get(controller.Name)
|
||||
notFound := k8serrors.IsNotFound(err)
|
||||
if err != nil && !notFound {
|
||||
return nil, fmt.Errorf("cannot get controller pod: %w", err)
|
||||
} else if notFound ||
|
||||
maybeControllerManagerPod == nil ||
|
||||
maybeControllerManagerPod.UID != controller.UID {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return maybeControllerManagerPod, nil
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
|
||||
func hash(controllerManagerPod *corev1.Pod) string {
|
||||
// FNV should be faster than SHA, and we don't care about hash-reversibility here, and Kubernetes
|
||||
// uses FNV for their pod templates, so should be good enough for us?
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(controllerManagerPod.UID)) // Never returns an error, per godoc.
|
||||
return hex.EncodeToString(h.Sum([]byte{}))
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kubecertagent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sclevine/spec"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
func runFilterTest(
|
||||
t *testing.T,
|
||||
name string,
|
||||
newFunc func(
|
||||
agentPodTemplate *corev1.Pod,
|
||||
podsInformer corev1informers.PodInformer,
|
||||
observableWithInformerOption *testutil.ObservableWithInformerOption,
|
||||
),
|
||||
) {
|
||||
spec.Run(t, name, func(t *testing.T, when spec.G, it spec.S) {
|
||||
var r *require.Assertions
|
||||
var subject controllerlib.Filter
|
||||
|
||||
whateverPod := &corev1.Pod{}
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
agentPodTemplate := &corev1.Pod{}
|
||||
agentPodTemplate.Labels = map[string]string{
|
||||
"some-label-key": "some-label-value",
|
||||
"some-other-label-key": "some-other-label-value",
|
||||
}
|
||||
podsInformer := kubeinformers.NewSharedInformerFactory(nil, 0).Core().V1().Pods()
|
||||
observableWithInformerOption := testutil.NewObservableWithInformerOption()
|
||||
newFunc(agentPodTemplate, podsInformer, observableWithInformerOption)
|
||||
|
||||
subject = observableWithInformerOption.GetFilterForInformer(podsInformer)
|
||||
})
|
||||
|
||||
when("a pod with the proper controller manager labels and phase is added/updated/deleted", func() {
|
||||
it("returns true", func() {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"component": "kube-controller-manager",
|
||||
},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
},
|
||||
}
|
||||
|
||||
r.True(subject.Add(pod))
|
||||
r.True(subject.Update(whateverPod, pod))
|
||||
r.True(subject.Update(pod, whateverPod))
|
||||
r.True(subject.Delete(pod))
|
||||
})
|
||||
})
|
||||
|
||||
when("a pod without the proper controller manager label is added/updated/deleted", func() {
|
||||
it("returns false", func() {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
},
|
||||
}
|
||||
|
||||
r.False(subject.Add(pod))
|
||||
r.False(subject.Update(whateverPod, pod))
|
||||
r.False(subject.Update(pod, whateverPod))
|
||||
r.False(subject.Delete(pod))
|
||||
})
|
||||
})
|
||||
|
||||
when("a pod without the proper controller manager phase is added/updated/deleted", func() {
|
||||
it("returns false", func() {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"component": "kube-controller-manager",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r.False(subject.Add(pod))
|
||||
r.False(subject.Update(whateverPod, pod))
|
||||
r.False(subject.Update(pod, whateverPod))
|
||||
r.False(subject.Delete(pod))
|
||||
})
|
||||
})
|
||||
|
||||
when("a pod with all the agent labels is added/updated/deleted", func() {
|
||||
it("returns true", func() {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"some-label-key": "some-label-value",
|
||||
"some-other-label-key": "some-other-label-value",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r.True(subject.Add(pod))
|
||||
r.True(subject.Update(whateverPod, pod))
|
||||
r.True(subject.Update(pod, whateverPod))
|
||||
r.True(subject.Delete(pod))
|
||||
})
|
||||
})
|
||||
|
||||
when("a pod missing one of the agent labels is added/updated/deleted", func() {
|
||||
it("returns false", func() {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"some-other-label-key": "some-other-label-value",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r.False(subject.Add(pod))
|
||||
r.False(subject.Update(whateverPod, pod))
|
||||
r.False(subject.Update(pod, whateverPod))
|
||||
r.False(subject.Delete(pod))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user