From 11eb935a752fc8206be9ef4f4d307a573facc8b6 Mon Sep 17 00:00:00 2001 From: Jestin Woods Date: Mon, 3 Mar 2025 15:46:07 -0800 Subject: [PATCH 1/2] Add ability to use bootstrap tokens from image secrets --- api/v1alpha1/humiobootstraptoken_types.go | 2 + .../core.humio.com_humiobootstraptokens.yaml | 4 + .../core.humio.com_humiobootstraptokens.yaml | 4 + docs/api.md | 7 ++ .../humiobootstraptoken_controller.go | 74 ++++++++++++++++++- .../humiobootstraptoken_defaults.go | 19 ++++- .../controller/humiobootstraptoken_pods.go | 4 +- .../clusters/humiocluster_controller_test.go | 52 ++++++++++++- internal/controller/suite/common.go | 57 +++++++++++--- 9 files changed, 207 insertions(+), 16 deletions(-) diff --git a/api/v1alpha1/humiobootstraptoken_types.go b/api/v1alpha1/humiobootstraptoken_types.go index ef7e8865..8a68d8d9 100644 --- a/api/v1alpha1/humiobootstraptoken_types.go +++ b/api/v1alpha1/humiobootstraptoken_types.go @@ -74,6 +74,8 @@ type HumioBootstrapTokenStatus struct { // HashedTokenSecret is the secret reference that contains the hashed token to use for this HumioBootstrapToken. This is set regardless of whether it's defined // in the spec or automatically created HashedTokenSecretKeyRef HumioHashedTokenSecretStatus `json:"hashedTokenSecretStatus,omitempty"` + // BootstrapImage is the image that was used to issue the token + BootstrapImage string `json:"bootstrapImage,omitempty"` } // HumioTokenSecretStatus contains the secret key reference to a kubernetes secret containing the bootstrap token secret. This is set regardless of whether it's defined diff --git a/charts/humio-operator/crds/core.humio.com_humiobootstraptokens.yaml b/charts/humio-operator/crds/core.humio.com_humiobootstraptokens.yaml index 13987b94..474d7668 100644 --- a/charts/humio-operator/crds/core.humio.com_humiobootstraptokens.yaml +++ b/charts/humio-operator/crds/core.humio.com_humiobootstraptokens.yaml @@ -1134,6 +1134,10 @@ spec: status: description: HumioBootstrapTokenStatus defines the observed state of HumioBootstrapToken. properties: + bootstrapImage: + description: BootstrapImage is the image that was used to issue the + token + type: string hashedTokenSecretStatus: description: |- HashedTokenSecret is the secret reference that contains the hashed token to use for this HumioBootstrapToken. This is set regardless of whether it's defined diff --git a/config/crd/bases/core.humio.com_humiobootstraptokens.yaml b/config/crd/bases/core.humio.com_humiobootstraptokens.yaml index 13987b94..474d7668 100644 --- a/config/crd/bases/core.humio.com_humiobootstraptokens.yaml +++ b/config/crd/bases/core.humio.com_humiobootstraptokens.yaml @@ -1134,6 +1134,10 @@ spec: status: description: HumioBootstrapTokenStatus defines the observed state of HumioBootstrapToken. properties: + bootstrapImage: + description: BootstrapImage is the image that was used to issue the + token + type: string hashedTokenSecretStatus: description: |- HashedTokenSecret is the secret reference that contains the hashed token to use for this HumioBootstrapToken. This is set regardless of whether it's defined diff --git a/docs/api.md b/docs/api.md index b1384759..c0d5d35c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3771,6 +3771,13 @@ HumioBootstrapTokenStatus defines the observed state of HumioBootstrapToken. + bootstrapImage + string + + BootstrapImage is the image that was used to issue the token
+ + false + hashedTokenSecretStatus object diff --git a/internal/controller/humiobootstraptoken_controller.go b/internal/controller/humiobootstraptoken_controller.go index 5bed5428..73de653c 100644 --- a/internal/controller/humiobootstraptoken_controller.go +++ b/internal/controller/humiobootstraptoken_controller.go @@ -21,6 +21,7 @@ import ( "context" "encoding/json" "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "net/http" "strings" "time" @@ -143,6 +144,11 @@ func (r *HumioBootstrapTokenReconciler) updateStatus(ctx context.Context, hbt *h return r.Client.Status().Update(ctx, hbt) } +func (r *HumioBootstrapTokenReconciler) updateStatusImage(ctx context.Context, hbt *humiov1alpha1.HumioBootstrapToken, image string) error { + hbt.Status.BootstrapImage = image + return r.Client.Status().Update(ctx, hbt) +} + func (r *HumioBootstrapTokenReconciler) execCommand(ctx context.Context, pod *corev1.Pod, args []string) (string, error) { configLoader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( clientcmd.NewDefaultClientConfigLoadingRules(), @@ -209,7 +215,10 @@ func (r *HumioBootstrapTokenReconciler) createPod(ctx context.Context, hbt *humi } } humioBootstrapTokenConfig := NewHumioBootstrapTokenConfig(hbt, humioCluster) - pod := ConstructBootstrapPod(&humioBootstrapTokenConfig) + pod, err := r.constructBootstrapPod(ctx, &humioBootstrapTokenConfig) + if err != nil { + return pod, r.logErrorAndReturn(err, "could not construct pod") + } if err := r.Get(ctx, types.NamespacedName{ Namespace: pod.Namespace, Name: pod.Name, @@ -231,7 +240,10 @@ func (r *HumioBootstrapTokenReconciler) createPod(ctx context.Context, hbt *humi func (r *HumioBootstrapTokenReconciler) deletePod(ctx context.Context, hbt *humiov1alpha1.HumioBootstrapToken, hc *humiov1alpha1.HumioCluster) error { existingPod := &corev1.Pod{} humioBootstrapTokenConfig := NewHumioBootstrapTokenConfig(hbt, hc) - pod := ConstructBootstrapPod(&humioBootstrapTokenConfig) + pod, err := r.constructBootstrapPod(ctx, &humioBootstrapTokenConfig) + if err != nil { + return r.logErrorAndReturn(err, "could not construct pod") + } if err := r.Get(ctx, types.NamespacedName{ Namespace: pod.Namespace, Name: pod.Name, @@ -385,6 +397,11 @@ func (r *HumioBootstrapTokenReconciler) ensureBootstrapTokenHashedToken(ctx cont if err = r.Update(ctx, updatedSecret); err != nil { return r.logErrorAndReturn(err, "failed to update secret with hashedToken data") } + + if err := r.updateStatusImage(ctx, hbt, pod.Spec.Containers[0].Image); err != nil { + return r.logErrorAndReturn(err, "failed to update bootstrap token image status") + } + return nil } @@ -398,6 +415,59 @@ func (r *HumioBootstrapTokenReconciler) getBootstrapTokenSecret(ctx context.Cont return existingSecret, err } +func (r *HumioBootstrapTokenReconciler) constructBootstrapPod(ctx context.Context, bootstrapConfig *HumioBootstrapTokenConfig) (*corev1.Pod, error) { + userID := int64(65534) + var image string + + if bootstrapConfig.imageSource() == nil { + image = bootstrapConfig.image() + } else { + configMap, err := kubernetes.GetConfigMap(ctx, r, bootstrapConfig.imageSource().ConfigMapRef.Name, bootstrapConfig.namespace()) + if err != nil { + return &corev1.Pod{}, r.logErrorAndReturn(err, "failed to get imageFromSource") + } + if imageValue, ok := configMap.Data[bootstrapConfig.imageSource().ConfigMapRef.Key]; ok { + image = imageValue + } + } + + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: bootstrapConfig.podName(), + Namespace: bootstrapConfig.namespace(), + }, + Spec: corev1.PodSpec{ + ImagePullSecrets: bootstrapConfig.imagePullSecrets(), + Affinity: bootstrapConfig.affinity(), + Containers: []corev1.Container{ + { + Name: HumioContainerName, + Image: image, + Command: []string{"/bin/sleep", "900"}, + Env: []corev1.EnvVar{ + { + Name: "HUMIO_LOG4J_CONFIGURATION", + Value: "log4j2-json-stdout.xml", + }, + }, + Resources: bootstrapConfig.resources(), + SecurityContext: &corev1.SecurityContext{ + Privileged: helpers.BoolPtr(false), + AllowPrivilegeEscalation: helpers.BoolPtr(false), + ReadOnlyRootFilesystem: helpers.BoolPtr(true), + RunAsUser: &userID, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + }, + }, + }, + }, + }, nil +} + // SetupWithManager sets up the controller with the Manager. func (r *HumioBootstrapTokenReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/internal/controller/humiobootstraptoken_defaults.go b/internal/controller/humiobootstraptoken_defaults.go index c605fd7d..b460c7ed 100644 --- a/internal/controller/humiobootstraptoken_defaults.go +++ b/internal/controller/humiobootstraptoken_defaults.go @@ -60,12 +60,29 @@ func (b *HumioBootstrapTokenConfig) image() string { } if b.ManagedHumioCluster != nil { if len(b.ManagedHumioCluster.Spec.NodePools) > 0 { - return b.ManagedHumioCluster.Spec.NodePools[0].Image + if b.ManagedHumioCluster.Spec.NodePools[0].Image != "" { + return b.ManagedHumioCluster.Spec.NodePools[0].Image + } } } return versions.DefaultHumioImageVersion() } +func (b *HumioBootstrapTokenConfig) imageSource() *humiov1alpha1.HumioImageSource { + + if b.ManagedHumioCluster.Spec.ImageSource != nil { + return b.ManagedHumioCluster.Spec.ImageSource + } + if b.ManagedHumioCluster != nil { + if len(b.ManagedHumioCluster.Spec.NodePools) > 0 { + if b.ManagedHumioCluster.Spec.NodePools[0].ImageSource != nil { + return b.ManagedHumioCluster.Spec.NodePools[0].ImageSource + } + } + } + return nil +} + func (b *HumioBootstrapTokenConfig) imagePullSecrets() []corev1.LocalObjectReference { if len(b.BootstrapToken.Spec.ImagePullSecrets) > 0 { return b.BootstrapToken.Spec.ImagePullSecrets diff --git a/internal/controller/humiobootstraptoken_pods.go b/internal/controller/humiobootstraptoken_pods.go index c9117617..56c57635 100644 --- a/internal/controller/humiobootstraptoken_pods.go +++ b/internal/controller/humiobootstraptoken_pods.go @@ -1,12 +1,14 @@ package controller import ( + "context" + "github.com/humio/humio-operator/internal/helpers" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func ConstructBootstrapPod(bootstrapConfig *HumioBootstrapTokenConfig) *corev1.Pod { +func ConstructBootstrapPod(ctx context.Context, bootstrapConfig *HumioBootstrapTokenConfig) *corev1.Pod { userID := int64(65534) return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/controller/suite/clusters/humiocluster_controller_test.go b/internal/controller/suite/clusters/humiocluster_controller_test.go index 375ba73b..e3665a07 100644 --- a/internal/controller/suite/clusters/humiocluster_controller_test.go +++ b/internal/controller/suite/clusters/humiocluster_controller_test.go @@ -975,6 +975,56 @@ var _ = Describe("HumioCluster Controller", func() { }) }) + Context("Humio Cluster Create with Image Source", Label("envtest", "dummy", "real"), func() { + It("Should correctly create cluster from image source", func() { + key := types.NamespacedName{ + Name: "humiocluster-create-image-source", + Namespace: testProcessNamespace, + } + toCreate := suite.ConstructBasicSingleNodeHumioCluster(key, true) + toCreate.Spec.Image = "" + toCreate.Spec.NodeCount = 2 + toCreate.Spec.ImageSource = &humiov1alpha1.HumioImageSource{ + ConfigMapRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "image-source-create", + }, + Key: "tag", + }, + } + + ctx := context.Background() + var updatedHumioCluster humiov1alpha1.HumioCluster + + suite.UsingClusterBy(key.Name, "Creating the imageSource configmap") + updatedImage := versions.UpgradePatchBestEffortNewVersion() + envVarSourceConfigMap := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "image-source-create", + Namespace: key.Namespace, + }, + Data: map[string]string{"tag": updatedImage}, + } + Expect(k8sClient.Create(ctx, &envVarSourceConfigMap)).To(Succeed()) + + suite.UsingClusterBy(key.Name, "Creating the cluster successfully") + suite.CreateAndBootstrapCluster(ctx, k8sClient, testHumioClient, toCreate, true, humiov1alpha1.HumioClusterStateRunning, testTimeout) + defer suite.CleanupCluster(ctx, k8sClient, toCreate) + + Eventually(func() string { + updatedHumioCluster = humiov1alpha1.HumioCluster{} + Expect(k8sClient.Get(ctx, key, &updatedHumioCluster)).Should(Succeed()) + return updatedHumioCluster.Status.State + }, testTimeout, suite.TestInterval).Should(BeIdenticalTo(humiov1alpha1.HumioClusterStateRunning)) + + Eventually(func() error { + bootstrapToken, err := suite.GetHumioBootstrapToken(ctx, key, k8sClient) + Expect(bootstrapToken.Status.BootstrapImage).To(BeEquivalentTo(updatedImage)) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + }) + }) + Context("Humio Cluster Update Image Source", Label("envtest", "dummy", "real"), func() { It("Update should correctly replace pods to use new image", func() { key := types.NamespacedName{ @@ -4012,7 +4062,7 @@ var _ = Describe("HumioCluster Controller", func() { return k8sClient.Update(ctx, &updatedHumioCluster) }, testTimeout, suite.TestInterval).Should(Succeed()) - suite.SimulateHumioBootstrapTokenCreatingSecretAndUpdatingStatus(ctx, key, k8sClient, testTimeout) + suite.SimulateHumioBootstrapTokenCreatingSecretAndUpdatingStatus(ctx, key, k8sClient, testTimeout, &updatedHumioCluster) suite.UsingClusterBy(key.Name, "Confirming we only created ingresses with expected hostname") foundIngressList = []networkingv1.Ingress{} diff --git a/internal/controller/suite/common.go b/internal/controller/suite/common.go index 0fa6d4f1..31edd4d4 100644 --- a/internal/controller/suite/common.go +++ b/internal/controller/suite/common.go @@ -422,7 +422,7 @@ func CreateAndBootstrapCluster(ctx context.Context, k8sClient client.Client, hum return } - SimulateHumioBootstrapTokenCreatingSecretAndUpdatingStatus(ctx, key, k8sClient, testTimeout) + SimulateHumioBootstrapTokenCreatingSecretAndUpdatingStatus(ctx, key, k8sClient, testTimeout, cluster) UsingClusterBy(key.Name, "Confirming cluster enters running state") var updatedHumioCluster humiov1alpha1.HumioCluster @@ -718,21 +718,41 @@ func CreateDockerRegredSecret(ctx context.Context, namespace corev1.Namespace, k Expect(k8sClient.Create(ctx, ®credSecret)).To(Succeed()) } -func SimulateHumioBootstrapTokenCreatingSecretAndUpdatingStatus(ctx context.Context, key types.NamespacedName, k8sClient client.Client, testTimeout time.Duration) { +func SimulateHumioBootstrapTokenCreatingSecretAndUpdatingStatus(ctx context.Context, key types.NamespacedName, k8sClient client.Client, testTimeout time.Duration, cluster *humiov1alpha1.HumioCluster) { UsingClusterBy(key.Name, "Simulating HumioBootstrapToken Controller running and adding the secret and status") Eventually(func() error { - hbtList, err := kubernetes.ListHumioBootstrapTokens(ctx, k8sClient, key.Namespace, kubernetes.LabelsForHumioBootstrapToken(key.Name)) - if err != nil { - return err + var bootstrapImage string + bootstrapImage = "test" + if cluster.Spec.Image != "" { + bootstrapImage = cluster.Spec.Image } - if len(hbtList) == 0 { - return fmt.Errorf("no humiobootstraptokens for cluster %s", key.Name) + if cluster.Spec.ImageSource != nil { + configMap, err := kubernetes.GetConfigMap(ctx, k8sClient, cluster.Spec.ImageSource.ConfigMapRef.Name, cluster.Namespace) + if err != nil && !k8serrors.IsNotFound(err) { + Expect(err).Should(Succeed()) + } else { + bootstrapImage = configMap.Data[cluster.Spec.ImageSource.ConfigMapRef.Key] + } } - if len(hbtList) > 1 { - return fmt.Errorf("too many humiobootstraptokens for cluster %s. found list : %+v", key.Name, hbtList) + for _, nodePool := range cluster.Spec.NodePools { + if nodePool.HumioNodeSpec.Image != "" { + bootstrapImage = nodePool.HumioNodeSpec.Image + break + } + if nodePool.ImageSource != nil { + configMap, err := kubernetes.GetConfigMap(ctx, k8sClient, nodePool.ImageSource.ConfigMapRef.Name, cluster.Namespace) + if err != nil && !k8serrors.IsNotFound(err) { + Expect(err).Should(Succeed()) + } else { + bootstrapImage = configMap.Data[nodePool.ImageSource.ConfigMapRef.Key] + break + } + } + } + updatedHumioBootstrapToken, err := GetHumioBootstrapToken(ctx, key, k8sClient) + if err != nil { + return err } - - updatedHumioBootstrapToken := hbtList[0] updatedHumioBootstrapToken.Status.State = humiov1alpha1.HumioBootstrapTokenStateReady updatedHumioBootstrapToken.Status.TokenSecretKeyRef = humiov1alpha1.HumioTokenSecretStatus{ SecretKeyRef: &corev1.SecretKeySelector{ @@ -750,6 +770,21 @@ func SimulateHumioBootstrapTokenCreatingSecretAndUpdatingStatus(ctx context.Cont Key: "hashedToken", }, } + updatedHumioBootstrapToken.Status.BootstrapImage = bootstrapImage return k8sClient.Status().Update(ctx, &updatedHumioBootstrapToken) }, testTimeout, TestInterval).Should(Succeed()) } + +func GetHumioBootstrapToken(ctx context.Context, key types.NamespacedName, k8sClient client.Client) (humiov1alpha1.HumioBootstrapToken, error) { + hbtList, err := kubernetes.ListHumioBootstrapTokens(ctx, k8sClient, key.Namespace, kubernetes.LabelsForHumioBootstrapToken(key.Name)) + if err != nil { + return humiov1alpha1.HumioBootstrapToken{}, err + } + if len(hbtList) == 0 { + return humiov1alpha1.HumioBootstrapToken{}, fmt.Errorf("no humiobootstraptokens for cluster %s", key.Name) + } + if len(hbtList) > 1 { + return humiov1alpha1.HumioBootstrapToken{}, fmt.Errorf("too many humiobootstraptokens for cluster %s. found list : %+v", key.Name, hbtList) + } + return hbtList[0], nil +} From 027ff4cbf6cd6a36ffdd704cd95b74f9da23207c Mon Sep 17 00:00:00 2001 From: Mike Rostermund Date: Thu, 6 Mar 2025 08:46:52 +0100 Subject: [PATCH 2/2] golangci-lint: fix goimports --- internal/controller/humiobootstraptoken_controller.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/controller/humiobootstraptoken_controller.go b/internal/controller/humiobootstraptoken_controller.go index 73de653c..448fed5e 100644 --- a/internal/controller/humiobootstraptoken_controller.go +++ b/internal/controller/humiobootstraptoken_controller.go @@ -21,11 +21,12 @@ import ( "context" "encoding/json" "fmt" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "net/http" "strings" "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/go-logr/logr" humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1" "github.com/humio/humio-operator/internal/helpers"