diff --git a/api/v1alpha1/humiobootstraptoken_types.go b/api/v1alpha1/humiobootstraptoken_types.go new file mode 100644 index 00000000..cd18baf6 --- /dev/null +++ b/api/v1alpha1/humiobootstraptoken_types.go @@ -0,0 +1,117 @@ +/* +Copyright 2020 Humio https://humio.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // HumioBootstrapTokenStateNotReady is the NotReady state of the bootstrap token + HumioBootstrapTokenStateNotReady = "NotReady" + // HumioBootstrapTokenStateReady is the Ready state of the bootstrap token + HumioBootstrapTokenStateReady = "Ready" +) + +// HumioBootstrapTokenSpec defines the bootstrap token that Humio will use to bootstrap authentication +type HumioBootstrapTokenSpec struct { + // ManagedClusterName refers to the name of the HumioCluster which will use this bootstrap token + ManagedClusterName string `json:"managedClusterName,omitempty"` + // ExternalClusterName refers to the name of the HumioExternalCluster which will use this bootstrap token for authentication + // This conflicts with ManagedClusterName. + ExternalClusterName string `json:"externalClusterName,omitempty"` + // Image can be set to override the image used to run when generating a bootstrap token. This will default to the image + // that is used by either the HumioCluster resource or the first NodePool resource if ManagedClusterName is set on the HumioBootstrapTokenSpec + Image string `json:"bootstrapImage,omitempty"` + // ImagePullSecrets defines the imagepullsecrets for the bootstrap image onetime pod. These secrets are not created by the operator. This will default to the imagePullSecrets + // that are used by either the HumioCluster resource or the first NodePool resource if ManagedClusterName is set on the HumioBootstrapTokenSpec + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + // Resources is the kubernetes resource limits for the bootstrap onetime pod + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + // TokenSecret is the secret reference that contains the token to use for this HumioBootstrapToken. This is used if one wants to use an existing + // token for the BootstrapToken rather than letting the operator create one by running a bootstrap token onetime pod + TokenSecret HumioTokenSecretSpec `json:"tokenSecret,omitempty"` + // HashedTokenSecret is the secret reference that contains the hashed token to use for this HumioBootstrapToken. This is used if one wants to use an existing + // hashed token for the BootstrapToken rather than letting the operator create one by running a bootstrap token onetime pod + HashedTokenSecret HumioHashedTokenSecretSpec `json:"hashedTokenSecret,omitempty"` +} + +type HumioTokenSecretSpec struct { + // SecretKeyRef is the secret key reference to a kubernetes secret containing the bootstrap token secret + SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"` +} + +type HumioHashedTokenSecretSpec struct { + // SecretKeyRef is the secret key reference to a kubernetes secret containing the bootstrap hashed token secret + SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"` +} + +type HumioBootstrapTokenStatus struct { + // State can be "NotReady" or "Ready" + State string `json:"state,omitempty"` + // TokenSecretKeyRef contains the secret key reference to a kubernetes secret containing the bootstrap token secret. This is set regardless of whether it's defined + // in the spec or automatically created + TokenSecretKeyRef HumioTokenSecretStatus `json:"tokenSecretStatus,omitempty"` + // 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"` +} + +// HumioTokenSecretStatus contains the secret key reference to a kubernetes secret containing the bootstrap token secret. This is set regardless of whether it's defined +// in the spec or automatically created +type HumioTokenSecretStatus struct { + // SecretKeyRef contains the secret key reference to a kubernetes secret containing the bootstrap token secret. This is set regardless of whether it's defined + // in the spec or automatically created + SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,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 +// in the spec or automatically created +type HumioHashedTokenSecretStatus struct { + // SecretKeyRef 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 + SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:path=humiobootstraptokens,scope=Namespaced +//+kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="The state of the bootstrap token" +//+operator-sdk:gen-csv:customresourcedefinitions.displayName="Humio Bootstrap Token" + +// HumioBootstrapToken defines the bootstrap token that Humio will use to bootstrap authentication +type HumioBootstrapToken struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec HumioBootstrapTokenSpec `json:"spec,omitempty"` + Status HumioBootstrapTokenStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// HumioBootstrapTokenList contains a list of HumioBootstrapTokens +type HumioBootstrapTokenList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []HumioBootstrapToken `json:"items"` +} + +func init() { + SchemeBuilder.Register(&HumioBootstrapToken{}, &HumioBootstrapTokenList{}) +} diff --git a/api/v1alpha1/humiocluster_types.go b/api/v1alpha1/humiocluster_types.go index aa87b743..1fbdaabd 100644 --- a/api/v1alpha1/humiocluster_types.go +++ b/api/v1alpha1/humiocluster_types.go @@ -118,7 +118,7 @@ type HumioNodeSpec struct { // DataVolumeSource is the volume that is mounted on the humio pods. This conflicts with DataVolumePersistentVolumeClaimSpecTemplate. DataVolumeSource corev1.VolumeSource `json:"dataVolumeSource,omitempty"` - // AuthServiceAccountName is the name of the Kubernetes Service Account that will be attached to the auth container in the humio pod. + // *Deprecated: AuthServiceAccountName is no longer used as the auth sidecar container has been removed.* AuthServiceAccountName string `json:"authServiceAccountName,omitempty"` // DisableInitContainer is used to disable the init container completely which collects the availability zone from the Kubernetes worker node. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ba56c5f6..4b7b833e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -534,6 +534,109 @@ func (in *HumioAlertStatus) DeepCopy() *HumioAlertStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HumioBootstrapToken) DeepCopyInto(out *HumioBootstrapToken) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioBootstrapToken. +func (in *HumioBootstrapToken) DeepCopy() *HumioBootstrapToken { + if in == nil { + return nil + } + out := new(HumioBootstrapToken) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HumioBootstrapToken) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HumioBootstrapTokenList) DeepCopyInto(out *HumioBootstrapTokenList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HumioBootstrapToken, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioBootstrapTokenList. +func (in *HumioBootstrapTokenList) DeepCopy() *HumioBootstrapTokenList { + if in == nil { + return nil + } + out := new(HumioBootstrapTokenList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HumioBootstrapTokenList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HumioBootstrapTokenSpec) DeepCopyInto(out *HumioBootstrapTokenSpec) { + *out = *in + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + in.TokenSecret.DeepCopyInto(&out.TokenSecret) + in.HashedTokenSecret.DeepCopyInto(&out.HashedTokenSecret) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioBootstrapTokenSpec. +func (in *HumioBootstrapTokenSpec) DeepCopy() *HumioBootstrapTokenSpec { + if in == nil { + return nil + } + out := new(HumioBootstrapTokenSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HumioBootstrapTokenStatus) DeepCopyInto(out *HumioBootstrapTokenStatus) { + *out = *in + in.TokenSecretKeyRef.DeepCopyInto(&out.TokenSecretKeyRef) + in.HashedTokenSecretKeyRef.DeepCopyInto(&out.HashedTokenSecretKeyRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioBootstrapTokenStatus. +func (in *HumioBootstrapTokenStatus) DeepCopy() *HumioBootstrapTokenStatus { + if in == nil { + return nil + } + out := new(HumioBootstrapTokenStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HumioCluster) DeepCopyInto(out *HumioCluster) { *out = *in @@ -952,6 +1055,46 @@ func (in *HumioFilterAlertStatus) DeepCopy() *HumioFilterAlertStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HumioHashedTokenSecretSpec) DeepCopyInto(out *HumioHashedTokenSecretSpec) { + *out = *in + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioHashedTokenSecretSpec. +func (in *HumioHashedTokenSecretSpec) DeepCopy() *HumioHashedTokenSecretSpec { + if in == nil { + return nil + } + out := new(HumioHashedTokenSecretSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HumioHashedTokenSecretStatus) DeepCopyInto(out *HumioHashedTokenSecretStatus) { + *out = *in + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioHashedTokenSecretStatus. +func (in *HumioHashedTokenSecretStatus) DeepCopy() *HumioHashedTokenSecretStatus { + if in == nil { + return nil + } + out := new(HumioHashedTokenSecretStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HumioHostnameSource) DeepCopyInto(out *HumioHostnameSource) { *out = *in @@ -1709,6 +1852,46 @@ func (in *HumioScheduledSearchStatus) DeepCopy() *HumioScheduledSearchStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HumioTokenSecretSpec) DeepCopyInto(out *HumioTokenSecretSpec) { + *out = *in + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioTokenSecretSpec. +func (in *HumioTokenSecretSpec) DeepCopy() *HumioTokenSecretSpec { + if in == nil { + return nil + } + out := new(HumioTokenSecretSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HumioTokenSecretStatus) DeepCopyInto(out *HumioTokenSecretStatus) { + *out = *in + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioTokenSecretStatus. +func (in *HumioTokenSecretStatus) DeepCopy() *HumioTokenSecretStatus { + if in == nil { + return nil + } + out := new(HumioTokenSecretStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HumioUpdateStrategy) DeepCopyInto(out *HumioUpdateStrategy) { *out = *in diff --git a/charts/humio-operator/crds/core.humio.com_humiobootstraptokens.yaml b/charts/humio-operator/crds/core.humio.com_humiobootstraptokens.yaml new file mode 100644 index 00000000..63fff4a2 --- /dev/null +++ b/charts/humio-operator/crds/core.humio.com_humiobootstraptokens.yaml @@ -0,0 +1,268 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: humiobootstraptokens.core.humio.com + labels: + app: 'humio-operator' + app.kubernetes.io/name: 'humio-operator' + app.kubernetes.io/instance: 'humio-operator' + app.kubernetes.io/managed-by: 'Helm' + helm.sh/chart: 'humio-operator-0.24.0' +spec: + group: core.humio.com + names: + kind: HumioBootstrapToken + listKind: HumioBootstrapTokenList + plural: humiobootstraptokens + singular: humiobootstraptoken + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The state of the bootstrap token + jsonPath: .status.state + name: State + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: HumioBootstrapToken defines the bootstrap token that Humio will + use to bootstrap authentication + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: HumioBootstrapTokenSpec defines the bootstrap token that + Humio will use to bootstrap authentication + properties: + bootstrapImage: + description: |- + Image can be set to override the image used to run when generating a bootstrap token. This will default to the image + that is used by either the HumioCluster resource or the first NodePool resource if ManagedClusterName is set on the HumioBootstrapTokenSpec + type: string + externalClusterName: + description: |- + ExternalClusterName refers to the name of the HumioExternalCluster which will use this bootstrap token for authentication + This conflicts with ManagedClusterName. + type: string + hashedTokenSecret: + description: |- + HashedTokenSecret is the secret reference that contains the hashed token to use for this HumioBootstrapToken. This is used if one wants to use an existing + hashed token for the BootstrapToken rather than letting the operator create one by running a bootstrap token onetime pod + properties: + secretKeyRef: + description: SecretKeyRef is the secret key reference to a kubernetes + secret containing the bootstrap hashed token secret + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must be + defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + imagePullSecrets: + description: |- + ImagePullSecrets defines the imagepullsecrets for the bootstrap image onetime pod. These secrets are not created by the operator. This will default to the imagePullSecrets + that are used by either the HumioCluster resource or the first NodePool resource if ManagedClusterName is set on the HumioBootstrapTokenSpec + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: array + managedClusterName: + description: ManagedClusterName refers to the name of the HumioCluster + which will use this bootstrap token + type: string + resources: + description: Resources is the kubernetes resource limits for the bootstrap + onetime pod + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + tokenSecret: + description: |- + TokenSecret is the secret reference that contains the token to use for this HumioBootstrapToken. This is used if one wants to use an existing + token for the BootstrapToken rather than letting the operator create one by running a bootstrap token onetime pod + properties: + secretKeyRef: + description: SecretKeyRef is the secret key reference to a kubernetes + secret containing the bootstrap token secret + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must be + defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + status: + properties: + 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 + in the spec or automatically created + properties: + secretKeyRef: + description: |- + SecretKeyRef 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 + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must be + defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + state: + description: State can be "NotReady" or "Ready" + type: string + tokenSecretStatus: + description: |- + TokenSecretKeyRef contains the secret key reference to a kubernetes secret containing the bootstrap token secret. This is set regardless of whether it's defined + in the spec or automatically created + properties: + secretKeyRef: + description: |- + SecretKeyRef contains the secret key reference to a kubernetes secret containing the bootstrap token secret. This is set regardless of whether it's defined + in the spec or automatically created + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must be + defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/humio-operator/crds/core.humio.com_humioclusters.yaml b/charts/humio-operator/crds/core.humio.com_humioclusters.yaml index 0794678c..7b4a9316 100644 --- a/charts/humio-operator/crds/core.humio.com_humioclusters.yaml +++ b/charts/humio-operator/crds/core.humio.com_humioclusters.yaml @@ -946,9 +946,8 @@ spec: type: object type: object authServiceAccountName: - description: AuthServiceAccountName is the name of the Kubernetes - Service Account that will be attached to the auth container in the - humio pod. + description: '*Deprecated: AuthServiceAccountName is no longer used + as the auth sidecar container has been removed.*' type: string autoRebalancePartitions: description: |- @@ -6640,9 +6639,8 @@ spec: type: object type: object authServiceAccountName: - description: AuthServiceAccountName is the name of the Kubernetes - Service Account that will be attached to the auth container - in the humio pod. + description: '*Deprecated: AuthServiceAccountName is no + longer used as the auth sidecar container has been removed.*' type: string containerLivenessProbe: description: |- diff --git a/charts/humio-operator/templates/operator-rbac.yaml b/charts/humio-operator/templates/operator-rbac.yaml index 457838c9..07f2a470 100644 --- a/charts/humio-operator/templates/operator-rbac.yaml +++ b/charts/humio-operator/templates/operator-rbac.yaml @@ -23,6 +23,7 @@ rules: - "" resources: - pods + - pods/exec - services - services/finalizers - endpoints @@ -67,6 +68,9 @@ rules: - humioclusters - humioclusters/finalizers - humioclusters/status + - humiobootstraptokens + - humiobootstraptokens/finalizers + - humiobootstraptokens/status - humioparsers - humioparsers/finalizers - humioparsers/status @@ -179,6 +183,7 @@ rules: - "" resources: - pods + - pods/exec - services - services/finalizers - endpoints @@ -231,6 +236,9 @@ rules: - humioclusters - humioclusters/finalizers - humioclusters/status + - humiobootstraptokens + - humiobootstraptokens/finalizers + - humiobootstraptokens/status - humioparsers - humioparsers/finalizers - humioparsers/status diff --git a/config/crd/bases/core.humio.com_humiobootstraptokens.yaml b/config/crd/bases/core.humio.com_humiobootstraptokens.yaml new file mode 100644 index 00000000..63fff4a2 --- /dev/null +++ b/config/crd/bases/core.humio.com_humiobootstraptokens.yaml @@ -0,0 +1,268 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: humiobootstraptokens.core.humio.com + labels: + app: 'humio-operator' + app.kubernetes.io/name: 'humio-operator' + app.kubernetes.io/instance: 'humio-operator' + app.kubernetes.io/managed-by: 'Helm' + helm.sh/chart: 'humio-operator-0.24.0' +spec: + group: core.humio.com + names: + kind: HumioBootstrapToken + listKind: HumioBootstrapTokenList + plural: humiobootstraptokens + singular: humiobootstraptoken + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The state of the bootstrap token + jsonPath: .status.state + name: State + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: HumioBootstrapToken defines the bootstrap token that Humio will + use to bootstrap authentication + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: HumioBootstrapTokenSpec defines the bootstrap token that + Humio will use to bootstrap authentication + properties: + bootstrapImage: + description: |- + Image can be set to override the image used to run when generating a bootstrap token. This will default to the image + that is used by either the HumioCluster resource or the first NodePool resource if ManagedClusterName is set on the HumioBootstrapTokenSpec + type: string + externalClusterName: + description: |- + ExternalClusterName refers to the name of the HumioExternalCluster which will use this bootstrap token for authentication + This conflicts with ManagedClusterName. + type: string + hashedTokenSecret: + description: |- + HashedTokenSecret is the secret reference that contains the hashed token to use for this HumioBootstrapToken. This is used if one wants to use an existing + hashed token for the BootstrapToken rather than letting the operator create one by running a bootstrap token onetime pod + properties: + secretKeyRef: + description: SecretKeyRef is the secret key reference to a kubernetes + secret containing the bootstrap hashed token secret + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must be + defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + imagePullSecrets: + description: |- + ImagePullSecrets defines the imagepullsecrets for the bootstrap image onetime pod. These secrets are not created by the operator. This will default to the imagePullSecrets + that are used by either the HumioCluster resource or the first NodePool resource if ManagedClusterName is set on the HumioBootstrapTokenSpec + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: array + managedClusterName: + description: ManagedClusterName refers to the name of the HumioCluster + which will use this bootstrap token + type: string + resources: + description: Resources is the kubernetes resource limits for the bootstrap + onetime pod + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + tokenSecret: + description: |- + TokenSecret is the secret reference that contains the token to use for this HumioBootstrapToken. This is used if one wants to use an existing + token for the BootstrapToken rather than letting the operator create one by running a bootstrap token onetime pod + properties: + secretKeyRef: + description: SecretKeyRef is the secret key reference to a kubernetes + secret containing the bootstrap token secret + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must be + defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + status: + properties: + 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 + in the spec or automatically created + properties: + secretKeyRef: + description: |- + SecretKeyRef 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 + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must be + defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + state: + description: State can be "NotReady" or "Ready" + type: string + tokenSecretStatus: + description: |- + TokenSecretKeyRef contains the secret key reference to a kubernetes secret containing the bootstrap token secret. This is set regardless of whether it's defined + in the spec or automatically created + properties: + secretKeyRef: + description: |- + SecretKeyRef contains the secret key reference to a kubernetes secret containing the bootstrap token secret. This is set regardless of whether it's defined + in the spec or automatically created + properties: + key: + description: The key of the secret to select from. Must be + a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must be + defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.humio.com_humioclusters.yaml b/config/crd/bases/core.humio.com_humioclusters.yaml index 0794678c..7b4a9316 100644 --- a/config/crd/bases/core.humio.com_humioclusters.yaml +++ b/config/crd/bases/core.humio.com_humioclusters.yaml @@ -946,9 +946,8 @@ spec: type: object type: object authServiceAccountName: - description: AuthServiceAccountName is the name of the Kubernetes - Service Account that will be attached to the auth container in the - humio pod. + description: '*Deprecated: AuthServiceAccountName is no longer used + as the auth sidecar container has been removed.*' type: string autoRebalancePartitions: description: |- @@ -6640,9 +6639,8 @@ spec: type: object type: object authServiceAccountName: - description: AuthServiceAccountName is the name of the Kubernetes - Service Account that will be attached to the auth container - in the humio pod. + description: '*Deprecated: AuthServiceAccountName is no + longer used as the auth sidecar container has been removed.*' type: string containerLivenessProbe: description: |- diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 7fb6e26c..fd131bb4 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -13,6 +13,7 @@ resources: - bases/core.humio.com_humiofilteralerts.yaml - bases/core.humio.com_humioscheduledsearches.yaml - bases/core.humio.com_humioaggregatealerts.yaml +- bases/core.humio.com_humiobootstraptokens.yaml # +kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 5c5f0b84..96532c80 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,2 +1,8 @@ resources: - manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: humio/humio-operator + newTag: latest diff --git a/config/manifests/bases/humio-operator.clusterserviceversion.yaml b/config/manifests/bases/humio-operator.clusterserviceversion.yaml new file mode 100644 index 00000000..f7695cb1 --- /dev/null +++ b/config/manifests/bases/humio-operator.clusterserviceversion.yaml @@ -0,0 +1,80 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: '[]' + capabilities: Basic Install + name: humio-operator.v0.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: HumioAction is the Schema for the humioactions API + displayName: Humio Action + kind: HumioAction + name: humioactions.core.humio.com + version: v1alpha1 + - description: HumioAlert is the Schema for the humioalerts API + displayName: Humio Alert + kind: HumioAlert + name: humioalerts.core.humio.com + version: v1alpha1 + - description: HumioCluster is the Schema for the humioclusters API + displayName: Humio Cluster + kind: HumioCluster + name: humioclusters.core.humio.com + version: v1alpha1 + - description: HumioExternalCluster is the Schema for the humioexternalclusters + API + displayName: Humio External Cluster + kind: HumioExternalCluster + name: humioexternalclusters.core.humio.com + version: v1alpha1 + - description: HumioIngestToken is the Schema for the humioingesttokens API + displayName: Humio Ingest Token + kind: HumioIngestToken + name: humioingesttokens.core.humio.com + version: v1alpha1 + - description: HumioParser is the Schema for the humioparsers API + displayName: Humio Parser + kind: HumioParser + name: humioparsers.core.humio.com + version: v1alpha1 + - description: HumioRepository is the Schema for the humiorepositories API + displayName: Humio Repository + kind: HumioRepository + name: humiorepositories.core.humio.com + version: v1alpha1 + - description: HumioView is the Schema for the humioviews API + displayName: Humio View + kind: HumioView + name: humioviews.core.humio.com + version: v1alpha1 + description: Operator for managing Humio Clusters + displayName: Humio Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: null + strategy: "" + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - humio + links: + - name: Humio Operator + url: https://humio-operator.domain + maturity: alpha + provider: + name: Humio + version: 0.0.0 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index ef45756c..a538a200 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -112,6 +112,32 @@ rules: - patch - update - watch +- apiGroups: + - core.humio.com + resources: + - HumioBootstrapTokens + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.humio.com + resources: + - HumioBootstrapTokens/finalizers + verbs: + - update +- apiGroups: + - core.humio.com + resources: + - HumioBootstrapTokens/status + verbs: + - get + - patch + - update - apiGroups: - core.humio.com resources: diff --git a/config/samples/core_v1alpha1_humiocluster_shared_serviceaccount.yaml b/config/samples/core_v1alpha1_humiocluster_shared_serviceaccount.yaml index e1a4c49a..5eeddcbb 100644 --- a/config/samples/core_v1alpha1_humiocluster_shared_serviceaccount.yaml +++ b/config/samples/core_v1alpha1_humiocluster_shared_serviceaccount.yaml @@ -14,7 +14,6 @@ spec: image: "humio/humio-core:1.82.1" humioServiceAccountName: humio initServiceAccountName: humio - authServiceAccountName: humio podAnnotations: linkerd.io/inject: enabled config.linkerd.io/skip-outbound-ports: "2181" diff --git a/controllers/humioaction_controller.go b/controllers/humioaction_controller.go index eca81448..796a6268 100644 --- a/controllers/humioaction_controller.go +++ b/controllers/humioaction_controller.go @@ -73,7 +73,7 @@ func (r *HumioActionReconciler) Reconcile(ctx context.Context, req ctrl.Request) r.Log = r.Log.WithValues("Request.UID", ha.UID) - cluster, err := helpers.NewCluster(ctx, r, ha.Spec.ManagedClusterName, ha.Spec.ExternalClusterName, ha.Namespace, helpers.UseCertManager(), true) + cluster, err := helpers.NewCluster(ctx, r, ha.Spec.ManagedClusterName, ha.Spec.ExternalClusterName, ha.Namespace, helpers.UseCertManager(), true, false) if err != nil || cluster == nil || cluster.Config() == nil { setStateErr := r.setState(ctx, humiov1alpha1.HumioActionStateConfigError, ha) if setStateErr != nil { diff --git a/controllers/humioaggregatealert_controller.go b/controllers/humioaggregatealert_controller.go index 9f73c578..d654fe21 100644 --- a/controllers/humioaggregatealert_controller.go +++ b/controllers/humioaggregatealert_controller.go @@ -74,7 +74,7 @@ func (r *HumioAggregateAlertReconciler) Reconcile(ctx context.Context, req ctrl. r.Log = r.Log.WithValues("Request.UID", haa.UID) - cluster, err := helpers.NewCluster(ctx, r, haa.Spec.ManagedClusterName, haa.Spec.ExternalClusterName, haa.Namespace, helpers.UseCertManager(), true) + cluster, err := helpers.NewCluster(ctx, r, haa.Spec.ManagedClusterName, haa.Spec.ExternalClusterName, haa.Namespace, helpers.UseCertManager(), true, false) if err != nil || cluster == nil || cluster.Config() == nil { setStateErr := r.setState(ctx, humiov1alpha1.HumioAggregateAlertStateConfigError, haa) if setStateErr != nil { diff --git a/controllers/humioalert_controller.go b/controllers/humioalert_controller.go index e772f31a..1eded9bf 100644 --- a/controllers/humioalert_controller.go +++ b/controllers/humioalert_controller.go @@ -20,10 +20,11 @@ import ( "context" "errors" "fmt" - "github.com/humio/humio-operator/pkg/kubernetes" "reflect" "time" + "github.com/humio/humio-operator/pkg/kubernetes" + humioapi "github.com/humio/cli/api" "github.com/humio/humio-operator/pkg/helpers" @@ -76,7 +77,7 @@ func (r *HumioAlertReconciler) Reconcile(ctx context.Context, req ctrl.Request) r.Log = r.Log.WithValues("Request.UID", ha.UID) - cluster, err := helpers.NewCluster(ctx, r, ha.Spec.ManagedClusterName, ha.Spec.ExternalClusterName, ha.Namespace, helpers.UseCertManager(), true) + cluster, err := helpers.NewCluster(ctx, r, ha.Spec.ManagedClusterName, ha.Spec.ExternalClusterName, ha.Namespace, helpers.UseCertManager(), true, false) if err != nil || cluster == nil || cluster.Config() == nil { setStateErr := r.setState(ctx, humiov1alpha1.HumioAlertStateConfigError, ha) if setStateErr != nil { diff --git a/controllers/humiobootstraptoken_controller.go b/controllers/humiobootstraptoken_controller.go new file mode 100644 index 00000000..48d23605 --- /dev/null +++ b/controllers/humiobootstraptoken_controller.go @@ -0,0 +1,416 @@ +/* +Copyright 2020 Humio https://humio.com +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/humio/humio-operator/pkg/helpers" + "github.com/humio/humio-operator/pkg/kubernetes" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/go-logr/logr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1" +) + +const ( + // BootstrapTokenSecretHashedTokenName is the name of the hashed token key inside the bootstrap token secret + BootstrapTokenSecretHashedTokenName = "hashedToken" + // BootstrapTokenSecretSecretName is the name of the secret key inside the bootstrap token secret + BootstrapTokenSecretSecretName = "secret" +) + +// HumioBootstrapTokenReconciler reconciles a HumioBootstrapToken object +type HumioBootstrapTokenReconciler struct { + client.Client + BaseLogger logr.Logger + Log logr.Logger + Namespace string +} + +type HumioBootstrapTokenSecretData struct { + Secret string `json:"secret"` + HashedToken string `json:"hashedToken"` +} + +//+kubebuilder:rbac:groups=core.humio.com,resources=HumioBootstrapTokens,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.humio.com,resources=HumioBootstrapTokens/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=core.humio.com,resources=HumioBootstrapTokens/finalizers,verbs=update + +// Reconcile runs the reconciler for a HumioBootstrapToken object +func (r *HumioBootstrapTokenReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + if r.Namespace != "" { + if r.Namespace != req.Namespace { + return reconcile.Result{}, nil + } + } + + r.Log = r.BaseLogger.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name, "Request.Type", helpers.GetTypeName(r), "Reconcile.ID", kubernetes.RandomString()) + r.Log.Info("Reconciling HumioBootstrapToken") + + // Fetch the HumioBootstrapToken + hbt := &humiov1alpha1.HumioBootstrapToken{} + if err := r.Get(ctx, req.NamespacedName, hbt); err != nil { + if k8serrors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, err + } + + hc := &humiov1alpha1.HumioCluster{} + hcRequest := types.NamespacedName{ + Name: hbt.Spec.ManagedClusterName, + Namespace: hbt.Namespace, + } + if err := r.Get(ctx, hcRequest, hc); err != nil { + if k8serrors.IsNotFound(err) { + r.Log.Error(err, fmt.Sprintf("humiocluster %s not found", hcRequest.Name)) + return reconcile.Result{}, err + } + r.Log.Error(err, fmt.Sprintf("problem fetching humiocluster %s", hcRequest.Name)) + return reconcile.Result{}, err + } + + if err := r.ensureBootstrapTokenSecret(ctx, hbt, hc); err != nil { + _ = r.updateStatus(ctx, hbt, humiov1alpha1.HumioBootstrapTokenStateNotReady) + return reconcile.Result{}, err + } + + if err := r.ensureBootstrapTokenHashedToken(ctx, hbt, hc); err != nil { + _ = r.updateStatus(ctx, hbt, humiov1alpha1.HumioBootstrapTokenStateNotReady) + return reconcile.Result{}, err + } + + if err := r.updateStatus(ctx, hbt, humiov1alpha1.HumioBootstrapTokenStateReady); err != nil { + return reconcile.Result{}, err + } + + return reconcile.Result{RequeueAfter: time.Second * 60}, nil +} + +func (r *HumioBootstrapTokenReconciler) updateStatus(ctx context.Context, hbt *humiov1alpha1.HumioBootstrapToken, state string) error { + hbt.Status.State = state + if state == humiov1alpha1.HumioBootstrapTokenStateReady { + hbt.Status.TokenSecretKeyRef = humiov1alpha1.HumioTokenSecretStatus{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: fmt.Sprintf("%s-%s", hbt.Name, kubernetes.BootstrapTokenSecretNameSuffix), + }, + Key: BootstrapTokenSecretSecretName, + }, + } + hbt.Status.HashedTokenSecretKeyRef = humiov1alpha1.HumioHashedTokenSecretStatus{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: fmt.Sprintf("%s-%s", hbt.Name, kubernetes.BootstrapTokenSecretNameSuffix), + }, + Key: BootstrapTokenSecretHashedTokenName, + }, + } + } + return r.Client.Status().Update(ctx, hbt) +} + +func (r *HumioBootstrapTokenReconciler) execCommand(pod *corev1.Pod, args []string) (string, error) { + configLoader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), + &clientcmd.ConfigOverrides{}, + ) + + // create the Config object + cfg, err := configLoader.ClientConfig() + if err != nil { + return "", err + } + + // we want to use the core API (namespaces lives here) + cfg.APIPath = "/api" + cfg.GroupVersion = &corev1.SchemeGroupVersion + cfg.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + // create a RESTClient + rc, err := rest.RESTClientFor(cfg) + if err != nil { + return "", err + } + + req := rc.Post(). + Resource("pods"). + Name(pod.Name). + Namespace(pod.Namespace). + SubResource("exec") + req.VersionedParams(&corev1.PodExecOptions{ + Container: "humio", // TODO: changeme + Command: args, + Stdin: false, + Stdout: true, + Stderr: true, + TTY: false, + }, scheme.ParameterCodec) + + exec, err := remotecommand.NewSPDYExecutor(cfg, "POST", req.URL()) + if err != nil { + return "", err + } + var stdout, stderr bytes.Buffer + err = exec.StreamWithContext(context.TODO(), remotecommand.StreamOptions{ + Stdin: nil, + Stdout: &stdout, + Stderr: &stderr, + Tty: false, + }) + if err != nil { + return "", err + } + return stdout.String(), nil +} + +func (r *HumioBootstrapTokenReconciler) createPod(ctx context.Context, hbt *humiov1alpha1.HumioBootstrapToken) (*corev1.Pod, error) { + existingPod := &corev1.Pod{} + humioCluster := &humiov1alpha1.HumioCluster{} + if err := r.Get(ctx, types.NamespacedName{ + Namespace: hbt.Namespace, + Name: hbt.Spec.ManagedClusterName, + }, humioCluster); err != nil { + if k8serrors.IsNotFound(err) { + humioCluster = nil + } + } + humioBootstrapTokenConfig := NewHumioBootstrapTokenConfig(hbt, humioCluster) + pod := ConstructBootstrapPod(&humioBootstrapTokenConfig) + if err := r.Get(ctx, types.NamespacedName{ + Namespace: pod.Namespace, + Name: pod.Name, + }, existingPod); err != nil { + if k8serrors.IsNotFound(err) { + if err := controllerutil.SetControllerReference(hbt, pod, r.Scheme()); err != nil { + return &corev1.Pod{}, r.logErrorAndReturn(err, "could not set controller reference") + } + r.Log.Info("creating onetime pod") + if err := r.Create(ctx, pod); err != nil { + return &corev1.Pod{}, r.logErrorAndReturn(err, "could not create pod") + } + return pod, nil + } + } + return existingPod, nil +} + +func (r *HumioBootstrapTokenReconciler) deletePod(ctx context.Context, hbt *humiov1alpha1.HumioBootstrapToken, hc *humiov1alpha1.HumioCluster) error { + existingPod := &corev1.Pod{} + humioBootstrapTokenConfig := NewHumioBootstrapTokenConfig(hbt, hc) + pod := ConstructBootstrapPod(&humioBootstrapTokenConfig) + if err := r.Get(ctx, types.NamespacedName{ + Namespace: pod.Namespace, + Name: pod.Name, + }, existingPod); err != nil { + if k8serrors.IsNotFound(err) { + return nil + } + return r.logErrorAndReturn(err, "could not delete pod") + } + r.Log.Info("deleting onetime pod") + if err := r.Delete(ctx, pod); err != nil { + return r.logErrorAndReturn(err, "could not delete pod") + } + return nil +} + +func (r *HumioBootstrapTokenReconciler) ensureBootstrapTokenSecret(ctx context.Context, hbt *humiov1alpha1.HumioBootstrapToken, hc *humiov1alpha1.HumioCluster) error { + r.Log.Info("ensuring bootstrap token secret") + humioBootstrapTokenConfig := NewHumioBootstrapTokenConfig(hbt, hc) + if _, err := r.getBootstrapTokenSecret(ctx, hbt, hc); err != nil { + if !k8serrors.IsNotFound(err) { + return r.logErrorAndReturn(err, "could not get secret") + } + secretData := map[string][]byte{} + if hbt.Spec.TokenSecret.SecretKeyRef != nil { + secret, err := kubernetes.GetSecret(ctx, r, hbt.Spec.TokenSecret.SecretKeyRef.Name, hbt.Namespace) + if err != nil { + return r.logErrorAndReturn(err, fmt.Sprintf("could not get secret %s", hbt.Spec.TokenSecret.SecretKeyRef.Name)) + } + if secretValue, ok := secret.Data[hbt.Spec.TokenSecret.SecretKeyRef.Key]; ok { + secretData[BootstrapTokenSecretSecretName] = secretValue + } else { + return r.logErrorAndReturn(err, fmt.Sprintf("could not get value from secret %s. "+ + "secret does not contain value for key \"%s\"", hbt.Spec.TokenSecret.SecretKeyRef.Name, hbt.Spec.TokenSecret.SecretKeyRef.Key)) + } + } + if hbt.Spec.HashedTokenSecret.SecretKeyRef != nil { + secret, err := kubernetes.GetSecret(ctx, r, hbt.Spec.TokenSecret.SecretKeyRef.Name, hbt.Namespace) + if err != nil { + return r.logErrorAndReturn(err, fmt.Sprintf("could not get secret %s", hbt.Spec.TokenSecret.SecretKeyRef.Name)) + } + if hashedTokenValue, ok := secret.Data[hbt.Spec.HashedTokenSecret.SecretKeyRef.Key]; ok { + secretData[BootstrapTokenSecretHashedTokenName] = hashedTokenValue + } else { + return r.logErrorAndReturn(err, fmt.Sprintf("could not get value from secret %s. "+ + "secret does not contain value for key \"%s\"", hbt.Spec.HashedTokenSecret.SecretKeyRef.Name, hbt.Spec.HashedTokenSecret.SecretKeyRef.Key)) + } + } + if err := humioBootstrapTokenConfig.validate(); err != nil { + return r.logErrorAndReturn(err, fmt.Sprintf("could not validate bootstrap config for %s", hbt.Name)) + } + okayToCreate, err := humioBootstrapTokenConfig.create() + if err != nil { + return r.logErrorAndReturn(err, "cannot create bootstrap token") + } + if okayToCreate { + secret := kubernetes.ConstructSecret(hbt.Name, hbt.Namespace, humioBootstrapTokenConfig.bootstrapTokenSecretName(), secretData, nil) + if err := controllerutil.SetControllerReference(hbt, secret, r.Scheme()); err != nil { + return r.logErrorAndReturn(err, "could not set controller reference") + } + r.Log.Info(fmt.Sprintf("creating secret: %s", secret.Name)) + if err := r.Create(ctx, secret); err != nil { + return r.logErrorAndReturn(err, "could not create secret") + } + } + } + return nil +} + +func (r *HumioBootstrapTokenReconciler) ensureBootstrapTokenHashedToken(ctx context.Context, hbt *humiov1alpha1.HumioBootstrapToken, hc *humiov1alpha1.HumioCluster) error { + r.Log.Info("ensuring bootstrap hashed token") + bootstrapTokenSecret, err := r.getBootstrapTokenSecret(ctx, hbt, hc) + if err != nil { + return r.logErrorAndReturn(err, "could not get bootstrap token secret") + } + + defer func(ctx context.Context, hbt *humiov1alpha1.HumioBootstrapToken, hc *humiov1alpha1.HumioCluster) { + if err := r.deletePod(ctx, hbt, hc); err != nil { + r.Log.Error(err, "failed to delete pod") + } + }(ctx, hbt, hc) + + if _, ok := bootstrapTokenSecret.Data[BootstrapTokenSecretHashedTokenName]; ok { + return nil + } + + commandArgs := []string{"env", "JVM_TMP_DIR=/tmp", "/app/humio/humio/bin/humio-token-hashing.sh", "--json"} + + if tokenSecret, ok := bootstrapTokenSecret.Data[BootstrapTokenSecretSecretName]; ok { + commandArgs = append(commandArgs, string(tokenSecret)) + } + + pod, err := r.createPod(ctx, hbt) + if err != nil { + return err + } + + var podRunning bool + var foundPod corev1.Pod + for i := 0; i < waitForPodTimeoutSeconds; i++ { + err := r.Get(ctx, types.NamespacedName{ + Namespace: pod.Namespace, + Name: pod.Name, + }, &foundPod) + if err == nil { + if foundPod.Status.Phase == corev1.PodRunning { + podRunning = true + break + } + } + r.Log.Info("waiting for bootstrap token pod to start") + time.Sleep(time.Second * 1) + } + if !podRunning { + return r.logErrorAndReturn(err, "failed to start bootstrap token pod") + } + + r.Log.Info("execing onetime pod") + output, err := r.execCommand(&foundPod, commandArgs) + if err != nil { + return r.logErrorAndReturn(err, "failed to exec pod") + } + + var jsonOutput string + var includeLine bool + outputLines := strings.Split(output, "\n") + for _, line := range outputLines { + if line == "{" { + includeLine = true + } + if line == "}" { + jsonOutput += "}" + includeLine = false + } + if includeLine { + jsonOutput += fmt.Sprintf("%s\n", line) + } + } + var secretData HumioBootstrapTokenSecretData + err = json.Unmarshal([]byte(jsonOutput), &secretData) + if err != nil { + return r.logErrorAndReturn(err, "failed to read output from exec command: output omitted") + } + + updatedSecret, err := r.getBootstrapTokenSecret(ctx, hbt, hc) + if err != nil { + return err + } + // TODO: make tokenHash constant + updatedSecret.Data = map[string][]byte{BootstrapTokenSecretHashedTokenName: []byte(secretData.HashedToken), BootstrapTokenSecretSecretName: []byte(secretData.Secret)} + + if err = r.Update(ctx, updatedSecret); err != nil { + return r.logErrorAndReturn(err, "failed to update secret with hashedToken data") + } + return nil +} + +func (r *HumioBootstrapTokenReconciler) getBootstrapTokenSecret(ctx context.Context, hbt *humiov1alpha1.HumioBootstrapToken, hc *humiov1alpha1.HumioCluster) (*corev1.Secret, error) { + humioBootstrapTokenConfig := NewHumioBootstrapTokenConfig(hbt, hc) + existingSecret := &corev1.Secret{} + err := r.Get(ctx, types.NamespacedName{ + Namespace: hbt.Namespace, + Name: humioBootstrapTokenConfig.bootstrapTokenSecretName(), + }, existingSecret) + return existingSecret, err +} + +// SetupWithManager sets up the controller with the Manager. +func (r *HumioBootstrapTokenReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&humiov1alpha1.HumioBootstrapToken{}). + Owns(&corev1.Secret{}). + Owns(&corev1.Pod{}). + Complete(r) +} + +func (r *HumioBootstrapTokenReconciler) logErrorAndReturn(err error, msg string) error { + r.Log.Error(err, msg) + return fmt.Errorf("%s: %w", msg, err) +} diff --git a/controllers/humiobootstraptoken_defaults.go b/controllers/humiobootstraptoken_defaults.go new file mode 100644 index 00000000..361e0375 --- /dev/null +++ b/controllers/humiobootstraptoken_defaults.go @@ -0,0 +1,107 @@ +package controllers + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/api/resource" + + humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1" + corev1 "k8s.io/api/core/v1" +) + +const ( + bootstrapTokenSecretSuffix = "bootstrap-token" + bootstrapTokenPodNameSuffix = "bootstrap-token-onetime" +) + +type HumioBootstrapTokenConfig struct { + BootstrapToken *humiov1alpha1.HumioBootstrapToken + ManagedHumioCluster *humiov1alpha1.HumioCluster +} + +func NewHumioBootstrapTokenConfig(bootstrapToken *humiov1alpha1.HumioBootstrapToken, managedHumioCluster *humiov1alpha1.HumioCluster) HumioBootstrapTokenConfig { + return HumioBootstrapTokenConfig{BootstrapToken: bootstrapToken, ManagedHumioCluster: managedHumioCluster} +} + +func (b *HumioBootstrapTokenConfig) bootstrapTokenSecretName() string { + if b.BootstrapToken.Spec.TokenSecret.SecretKeyRef != nil { + return b.BootstrapToken.Spec.TokenSecret.SecretKeyRef.Name + } + return fmt.Sprintf("%s-%s", b.BootstrapToken.Name, bootstrapTokenSecretSuffix) +} + +func (b *HumioBootstrapTokenConfig) create() (bool, error) { + if err := b.validate(); err != nil { + return false, err + } + if b.BootstrapToken.Spec.TokenSecret.SecretKeyRef == nil && b.BootstrapToken.Spec.HashedTokenSecret.SecretKeyRef == nil { + return true, nil + } + return false, nil +} + +func (b *HumioBootstrapTokenConfig) validate() error { + if b.BootstrapToken.Spec.TokenSecret.SecretKeyRef == nil && b.BootstrapToken.Spec.HashedTokenSecret.SecretKeyRef == nil { + return nil + } + if b.BootstrapToken.Spec.TokenSecret.SecretKeyRef != nil && b.BootstrapToken.Spec.HashedTokenSecret.SecretKeyRef != nil { + return nil + } + return fmt.Errorf("must set both tokenSecret.secretKeyRef as well as hashedTokenSecret.secretKeyRef") +} + +func (b *HumioBootstrapTokenConfig) image() string { + if b.BootstrapToken.Spec.Image != "" { + return b.BootstrapToken.Spec.Image + } + if b.ManagedHumioCluster.Spec.Image != "" { + return b.ManagedHumioCluster.Spec.Image + } + if b.ManagedHumioCluster != nil { + if len(b.ManagedHumioCluster.Spec.NodePools) > 0 { + return b.ManagedHumioCluster.Spec.NodePools[0].Image + } + } + return Image +} + +func (b *HumioBootstrapTokenConfig) imagePullSecrets() []corev1.LocalObjectReference { + if len(b.BootstrapToken.Spec.ImagePullSecrets) > 0 { + return b.BootstrapToken.Spec.ImagePullSecrets + } + if len(b.ManagedHumioCluster.Spec.ImagePullSecrets) > 0 { + return b.ManagedHumioCluster.Spec.ImagePullSecrets + } + if b.ManagedHumioCluster != nil { + if len(b.ManagedHumioCluster.Spec.NodePools) > 0 { + if len(b.ManagedHumioCluster.Spec.NodePools[0].ImagePullSecrets) > 0 { + return b.ManagedHumioCluster.Spec.NodePools[0].ImagePullSecrets + } + } + } + return []corev1.LocalObjectReference{} +} + +func (b *HumioBootstrapTokenConfig) resources() corev1.ResourceRequirements { + if b.BootstrapToken.Spec.Resources != nil { + return *b.BootstrapToken.Spec.Resources + } + return corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(500*1024*1024, resource.BinarySI), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(50*1024*1024, resource.BinarySI), + }, + } +} + +func (b *HumioBootstrapTokenConfig) podName() string { + return fmt.Sprintf("%s-%s", b.BootstrapToken.Name, bootstrapTokenPodNameSuffix) +} + +func (b *HumioBootstrapTokenConfig) namespace() string { + return b.BootstrapToken.Namespace +} diff --git a/controllers/humiobootstraptoken_pods.go b/controllers/humiobootstraptoken_pods.go new file mode 100644 index 00000000..f963bbb8 --- /dev/null +++ b/controllers/humiobootstraptoken_pods.go @@ -0,0 +1,45 @@ +package controllers + +import ( + "github.com/humio/humio-operator/pkg/helpers" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func ConstructBootstrapPod(bootstrapConfig *HumioBootstrapTokenConfig) *corev1.Pod { + userID := int64(65534) + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: bootstrapConfig.podName(), + Namespace: bootstrapConfig.namespace(), + }, + Spec: corev1.PodSpec{ + ImagePullSecrets: bootstrapConfig.imagePullSecrets(), + Containers: []corev1.Container{ + { + Name: HumioContainerName, + Image: bootstrapConfig.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", + }, + }, + }, + }, + }, + }, + } +} diff --git a/controllers/humiocluster_annotations.go b/controllers/humiocluster_annotations.go index 3a5f51ef..62420a15 100644 --- a/controllers/humiocluster_annotations.go +++ b/controllers/humiocluster_annotations.go @@ -36,6 +36,8 @@ const ( PodRevisionAnnotation = "humio.com/pod-revision" envVarSourceHashAnnotation = "humio.com/env-var-source-hash" pvcHashAnnotation = "humio_pvc_hash" + // #nosec G101 + bootstrapTokenHashAnnotation = "humio.com/bootstrap-token-hash" ) func (r *HumioClusterReconciler) incrementHumioClusterPodRevision(ctx context.Context, hc *humiov1alpha1.HumioCluster, hnp *HumioNodePool) (int, error) { diff --git a/controllers/humiocluster_controller.go b/controllers/humiocluster_controller.go index 314c21bf..cd85fdd4 100644 --- a/controllers/humiocluster_controller.go +++ b/controllers/humiocluster_controller.go @@ -121,6 +121,11 @@ func (r *HumioClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request withObservedGeneration(hc.GetGeneration())) }(ctx, r.HumioClient, hc) + if err := r.ensureHumioClusterBootstrapToken(ctx, hc); err != nil { + return r.updateStatus(ctx, r.Client.Status(), hc, statusOptions(). + withMessage(err.Error())) + } + for _, pool := range humioNodePools.Filter(NodePoolFilterHasNode) { if err := r.setImageFromSource(ctx, pool); err != nil { return r.updateStatus(ctx, r.Client.Status(), hc, statusOptions(). @@ -240,7 +245,6 @@ func (r *HumioClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request r.ensureService, r.ensureHumioPodPermissions, r.ensureInitContainerPermissions, - r.ensureAuthContainerPermissions, r.ensureHumioNodeCertificates, r.ensureExtraKafkaConfigsConfigMap, } { @@ -306,7 +310,7 @@ func (r *HumioClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request } } - cluster, err := helpers.NewCluster(ctx, r, hc.Name, "", hc.Namespace, helpers.UseCertManager(), true) + cluster, err := helpers.NewCluster(ctx, r, hc.Name, "", hc.Namespace, helpers.UseCertManager(), true, false) if err != nil || cluster == nil || cluster.Config() == nil { return r.updateStatus(ctx, r.Client.Status(), hc, statusOptions(). withMessage(r.logErrorAndReturn(err, "unable to obtain humio client config").Error()). @@ -451,6 +455,28 @@ func (r *HumioClusterReconciler) ensurePodRevisionAnnotation(ctx context.Context } return hc.Status.State, nil } +func (r *HumioClusterReconciler) ensureHumioClusterBootstrapToken(ctx context.Context, hc *humiov1alpha1.HumioCluster) error { + r.Log.Info("ensuring humiobootstraptoken") + hbtList, err := kubernetes.ListHumioBootstrapTokens(ctx, r.Client, hc.GetNamespace(), kubernetes.LabelsForHumioBootstrapToken(hc.GetName())) + if err != nil { + return r.logErrorAndReturn(err, "could not list HumioBootstrapToken") + } + if len(hbtList) > 0 { + r.Log.Info("humiobootstraptoken already exists") + return nil + } + + hbt := kubernetes.ConstructHumioBootstrapToken(hc.GetName(), hc.GetNamespace()) + if err := controllerutil.SetControllerReference(hc, hbt, r.Scheme()); err != nil { + return r.logErrorAndReturn(err, "could not set controller reference") + } + r.Log.Info(fmt.Sprintf("creating humiobootstraptoken %s", hbt.Name)) + err = r.Create(ctx, hbt) + if err != nil { + return r.logErrorAndReturn(err, "could not create bootstrap token resource") + } + return nil +} func (r *HumioClusterReconciler) validateInitialPodSpec(hnp *HumioNodePool) error { if _, err := ConstructPod(hnp, "", &podAttachments{}); err != nil { @@ -877,42 +903,6 @@ func (r *HumioClusterReconciler) ensureInitContainerPermissions(ctx context.Cont return nil } -func (r *HumioClusterReconciler) ensureAuthContainerPermissions(ctx context.Context, hc *humiov1alpha1.HumioCluster, hnp *HumioNodePool) error { - // Only add the service account secret if the authServiceAccountName is supplied. This implies the service account, - // cluster role and cluster role binding are managed outside of the operator, so we skip the remaining tasks. - if hnp.AuthServiceAccountIsSetByUser() { - // We do not want to attach the auth service account to the humio pod. Instead, only the auth container should use this - // service account. To do this, we can attach the service account directly to the auth container as per - // https://github.com/kubernetes/kubernetes/issues/66020#issuecomment-590413238 - if err := r.ensureServiceAccountSecretExists(ctx, hc, hnp, hnp.GetAuthServiceAccountSecretName(), hnp.GetAuthServiceAccountName()); err != nil { - return r.logErrorAndReturn(err, "unable to ensure auth service account secret exists") - } - return nil - } - - // The service account is used by the auth container attached to the humio pods. - if err := r.ensureServiceAccountExists(ctx, hc, hnp, hnp.GetAuthServiceAccountName(), map[string]string{}); err != nil { - return r.logErrorAndReturn(err, "unable to ensure auth service account exists") - } - - // We do not want to attach the auth service account to the humio pod. Instead, only the auth container should use this - // service account. To do this, we can attach the service account directly to the auth container as per - // https://github.com/kubernetes/kubernetes/issues/66020#issuecomment-590413238 - if err := r.ensureServiceAccountSecretExists(ctx, hc, hnp, hnp.GetAuthServiceAccountSecretName(), hnp.GetAuthServiceAccountName()); err != nil { - return r.logErrorAndReturn(err, "unable to ensure auth service account secret exists") - } - - if err := r.ensureAuthRole(ctx, hc, hnp); err != nil { - return r.logErrorAndReturn(err, "unable to ensure auth role exists") - } - - if err := r.ensureAuthRoleBinding(ctx, hc, hnp); err != nil { - return r.logErrorAndReturn(err, "unable to ensure auth role binding exists") - } - - return nil -} - // Ensure the CA Issuer is valid/ready func (r *HumioClusterReconciler) ensureValidCAIssuer(ctx context.Context, hc *humiov1alpha1.HumioCluster) error { if !helpers.TLSEnabled(hc) { @@ -1113,27 +1103,6 @@ func (r *HumioClusterReconciler) ensureInitClusterRole(ctx context.Context, hnp return nil } -func (r *HumioClusterReconciler) ensureAuthRole(ctx context.Context, hc *humiov1alpha1.HumioCluster, hnp *HumioNodePool) error { - roleName := hnp.GetAuthRoleName() - _, err := kubernetes.GetRole(ctx, r, roleName, hnp.GetNamespace()) - if err != nil { - if k8serrors.IsNotFound(err) { - role := kubernetes.ConstructAuthRole(roleName, hnp.GetNamespace(), hnp.GetNodePoolLabels()) - if err := controllerutil.SetControllerReference(hc, role, r.Scheme()); err != nil { - return r.logErrorAndReturn(err, "could not set controller reference") - } - r.Log.Info(fmt.Sprintf("creating role: %s", role.Name)) - err = r.Create(ctx, role) - if err != nil { - return r.logErrorAndReturn(err, "unable to create auth role") - } - r.Log.Info(fmt.Sprintf("successfully created auth role %s", roleName)) - humioClusterPrometheusMetrics.Counters.RolesCreated.Inc() - } - } - return nil -} - func (r *HumioClusterReconciler) ensureInitClusterRoleBinding(ctx context.Context, hnp *HumioNodePool) error { clusterRoleBindingName := hnp.GetInitClusterRoleBindingName() _, err := kubernetes.GetClusterRoleBinding(ctx, r, clusterRoleBindingName) @@ -1160,33 +1129,6 @@ func (r *HumioClusterReconciler) ensureInitClusterRoleBinding(ctx context.Contex return nil } -func (r *HumioClusterReconciler) ensureAuthRoleBinding(ctx context.Context, hc *humiov1alpha1.HumioCluster, hnp *HumioNodePool) error { - roleBindingName := hnp.GetAuthRoleBindingName() - _, err := kubernetes.GetRoleBinding(ctx, r, roleBindingName, hnp.GetNamespace()) - if err != nil { - if k8serrors.IsNotFound(err) { - roleBinding := kubernetes.ConstructRoleBinding( - roleBindingName, - hnp.GetAuthRoleName(), - hnp.GetNamespace(), - hnp.GetAuthServiceAccountName(), - hnp.GetNodePoolLabels(), - ) - if err := controllerutil.SetControllerReference(hc, roleBinding, r.Scheme()); err != nil { - return r.logErrorAndReturn(err, "could not set controller reference") - } - r.Log.Info(fmt.Sprintf("creating role binding: %s", roleBinding.Name)) - err = r.Create(ctx, roleBinding) - if err != nil { - return r.logErrorAndReturn(err, "unable to create auth role binding") - } - r.Log.Info(fmt.Sprintf("successfully created auth role binding %s", roleBindingName)) - humioClusterPrometheusMetrics.Counters.RoleBindingsCreated.Inc() - } - } - return nil -} - // validateUserDefinedServiceAccountsExists confirms that the user-defined service accounts all exist as they should. // If any of the service account names explicitly set does not exist, or that we get an error, we return an error. // In case the user does not define any service accounts or that all user-defined service accounts already exists, we return nil. @@ -1209,15 +1151,6 @@ func (r *HumioClusterReconciler) validateUserDefinedServiceAccountsExists(ctx co return r.logErrorAndReturn(err, "could not get service accounts") } } - if hc.Spec.AuthServiceAccountName != "" { - _, err := kubernetes.GetServiceAccount(ctx, r, hc.Spec.AuthServiceAccountName, hc.Namespace) - if err != nil { - if k8serrors.IsNotFound(err) { - return r.logErrorAndReturn(err, "not all referenced service accounts exists") - } - return r.logErrorAndReturn(err, "could not get service accounts") - } - } return nil } @@ -1399,7 +1332,7 @@ func (r *HumioClusterReconciler) ensureLicense(ctx context.Context, hc *humiov1a // Configure a Humio client without an API token which we can use to check the current license on the cluster noLicense := humioapi.OnPremLicense{} - cluster, err := helpers.NewCluster(ctx, r, hc.Name, "", hc.Namespace, helpers.UseCertManager(), false) + cluster, err := helpers.NewCluster(ctx, r, hc.Name, "", hc.Namespace, helpers.UseCertManager(), false, false) if err != nil { return reconcile.Result{}, err } @@ -1437,6 +1370,11 @@ func (r *HumioClusterReconciler) ensureLicense(ctx context.Context, hc *humiov1a // At this point we know a non-empty license has been returned by the Humio API, // so we can continue to parse the license and issue a license update if needed. if existingLicense == nil || existingLicense == noLicense { + cluster, err = helpers.NewCluster(ctx, r, hc.Name, "", hc.Namespace, helpers.UseCertManager(), false, true) + if err != nil { + return reconcile.Result{}, r.logErrorAndReturn(err, "could not install initial license") + } + if err = r.HumioClient.InstallLicense(cluster.Config(), req, licenseStr); err != nil { return reconcile.Result{}, r.logErrorAndReturn(err, "could not install initial license") } @@ -1446,7 +1384,20 @@ func (r *HumioClusterReconciler) ensureLicense(ctx context.Context, hc *humiov1a return reconcile.Result{Requeue: true}, nil } - cluster, err = helpers.NewCluster(ctx, r, hc.Name, "", hc.Namespace, helpers.UseCertManager(), true) + cluster, err = helpers.NewCluster(ctx, r, hc.Name, "", hc.Namespace, helpers.UseCertManager(), false, true) + if err != nil { + return reconcile.Result{}, r.logErrorAndReturn(err, "could not authenticate with bootstrap token") + } + if err = r.HumioClient.InstallLicense(cluster.Config(), req, licenseStr); err != nil { + return reconcile.Result{}, r.logErrorAndReturn(err, "could not install license") + } + + // TODO: ensureLicense should be broken into multiple steps + if err = r.ensurePermissionTokens(ctx, cluster.Config(), req, hc); err != nil { + return reconcile.Result{}, r.logErrorAndReturn(err, fmt.Sprintf("config: %+v", cluster.Config())) + } + + cluster, err = helpers.NewCluster(ctx, r, hc.Name, "", hc.Namespace, helpers.UseCertManager(), true, false) if err != nil { return reconcile.Result{}, err } @@ -1472,6 +1423,11 @@ func (r *HumioClusterReconciler) ensureLicense(ctx context.Context, hc *humiov1a return reconcile.Result{}, nil } +func (r *HumioClusterReconciler) ensurePermissionTokens(ctx context.Context, config *humioapi.Config, req reconcile.Request, hc *humiov1alpha1.HumioCluster) error { + r.Log.Info("ensuring permission tokens") + return r.createPermissionToken(ctx, config, req, hc, "admin", "RootOrg") +} + func (r *HumioClusterReconciler) ensureService(ctx context.Context, hc *humiov1alpha1.HumioCluster, hnp *HumioNodePool) error { r.Log.Info("ensuring service") existingService, err := kubernetes.GetService(ctx, r, hnp.GetNodePoolName(), hnp.GetNamespace()) @@ -1654,50 +1610,6 @@ func (r *HumioClusterReconciler) ensureNodePoolSpecificResourcesHaveLabelWithNod } } - if !hnp.AuthServiceAccountIsSetByUser() { - serviceAccount, err := kubernetes.GetServiceAccount(ctx, r.Client, hnp.GetAuthServiceAccountName(), hnp.GetNamespace()) - if err == nil { - serviceAccount.SetLabels(hnp.GetNodePoolLabels()) - err = r.Client.Update(ctx, serviceAccount) - if err != nil { - return r.logErrorAndReturn(err, "unable to update auth service account") - } - } - if err != nil { - if !k8serrors.IsNotFound(err) { - return r.logErrorAndReturn(err, "unable to get auth service account") - } - } - - role, err := kubernetes.GetRole(ctx, r.Client, hnp.GetAuthRoleName(), hnp.GetNamespace()) - if err == nil { - role.SetLabels(hnp.GetNodePoolLabels()) - err = r.Client.Update(ctx, role) - if err != nil { - return r.logErrorAndReturn(err, "unable to update auth role") - } - } - if err != nil { - if !k8serrors.IsNotFound(err) { - return r.logErrorAndReturn(err, "unable to get auth role") - } - } - - roleBinding, err := kubernetes.GetRoleBinding(ctx, r.Client, hnp.GetAuthRoleBindingName(), hnp.GetNamespace()) - if err == nil { - roleBinding.SetLabels(hnp.GetNodePoolLabels()) - err = r.Client.Update(ctx, roleBinding) - if err != nil { - return r.logErrorAndReturn(err, "unable to update auth role binding") - } - } - if err != nil { - if !k8serrors.IsNotFound(err) { - return r.logErrorAndReturn(err, "unable to get auth role binding") - } - } - } - return nil } @@ -1915,24 +1827,6 @@ func (r *HumioClusterReconciler) getInitServiceAccountSecretName(ctx context.Con return foundInitServiceAccountSecretsList[0].Name, nil } -func (r *HumioClusterReconciler) getAuthServiceAccountSecretName(ctx context.Context, hnp *HumioNodePool) (string, error) { - foundAuthServiceAccountNameSecretsList, err := kubernetes.ListSecrets(ctx, r, hnp.GetNamespace(), hnp.GetLabelsForSecret(hnp.GetAuthServiceAccountSecretName())) - if err != nil { - return "", err - } - if len(foundAuthServiceAccountNameSecretsList) == 0 { - return "", nil - } - if len(foundAuthServiceAccountNameSecretsList) > 1 { - var secretNames []string - for _, secret := range foundAuthServiceAccountNameSecretsList { - secretNames = append(secretNames, secret.Name) - } - return "", fmt.Errorf("found more than one auth service account secret: %s", strings.Join(secretNames, ", ")) - } - return foundAuthServiceAccountNameSecretsList[0].Name, nil -} - func (r *HumioClusterReconciler) ensureHumioServiceAccountAnnotations(ctx context.Context, hnp *HumioNodePool) (bool, error) { // Don't change the service account annotations if the service account is not managed by the operator if hnp.HumioServiceAccountIsSetByUser() { @@ -2009,6 +1903,21 @@ func (r *HumioClusterReconciler) ensureMismatchedPodsAreDeleted(ctx context.Cont attachments.envVarSourceData = envVarSourceData } + humioBootstrapTokens, err := kubernetes.ListHumioBootstrapTokens(ctx, r.Client, hc.GetNamespace(), kubernetes.LabelsForHumioBootstrapToken(hc.GetName())) + if err != nil { + return reconcile.Result{}, r.logErrorAndReturn(err, "failed to get bootstrap token") + } + if len(humioBootstrapTokens) > 0 { + if humioBootstrapTokens[0].Status.State == humiov1alpha1.HumioBootstrapTokenStateReady { + attachments.bootstrapTokenSecretReference.secretReference = humioBootstrapTokens[0].Status.HashedTokenSecretKeyRef.SecretKeyRef + bootstrapTokenHash, err := r.getDesiredBootstrapTokenHash(ctx, hc) + if err != nil { + return reconcile.Result{}, r.logErrorAndReturn(err, "unable to find bootstrap token secret") + } + attachments.bootstrapTokenSecretReference.hash = bootstrapTokenHash + } + } + // prioritize deleting the pods with errors var podList []corev1.Pod if podsStatus.havePodsWithErrors() { diff --git a/controllers/humiocluster_defaults.go b/controllers/humiocluster_defaults.go index 3097248d..8cdb8d20 100644 --- a/controllers/humiocluster_defaults.go +++ b/controllers/humiocluster_defaults.go @@ -33,7 +33,7 @@ import ( ) const ( - Image = "humio/humio-core:1.131.1" + Image = "humio/humio-core:1.142.3" HelperImage = "humio/humio-operator-helper:8f5ef6c7e470226e77d985f36cf39be9a100afea" targetReplicationFactor = 2 digestPartitionsCount = 24 @@ -44,7 +44,6 @@ const ( ViewGroupPermissionsFilename = "view-group-permissions.json" RolePermissionsFilename = "role-permissions.json" HumioContainerName = "humio" - AuthContainerName = "humio-auth" InitContainerName = "humio-init" // cluster-wide resources: @@ -55,10 +54,6 @@ const ( HumioServiceAccountNameSuffix = "humio" initServiceAccountNameSuffix = "init" initServiceAccountSecretNameIdentifier = "init" - authServiceAccountNameSuffix = "auth" - authServiceAccountSecretNameIdentifier = "auth" - authRoleSuffix = "auth" - authRoleBindingSuffix = "auth" extraKafkaConfigsConfigMapNameSuffix = "extra-kafka-configs" viewGroupPermissionsConfigMapNameSuffix = "view-group-permissions" rolePermissionsConfigMapNameSuffix = "role-permissions" @@ -102,7 +97,6 @@ func NewHumioNodeManagerFromHumioCluster(hc *humiov1alpha1.HumioCluster) *HumioN DataVolumePersistentVolumeClaimSpecTemplate: hc.Spec.DataVolumePersistentVolumeClaimSpecTemplate, DataVolumePersistentVolumeClaimPolicy: hc.Spec.DataVolumePersistentVolumeClaimPolicy, DataVolumeSource: hc.Spec.DataVolumeSource, - AuthServiceAccountName: hc.Spec.AuthServiceAccountName, DisableInitContainer: hc.Spec.DisableInitContainer, EnvironmentVariablesSource: hc.Spec.EnvironmentVariablesSource, PodAnnotations: hc.Spec.PodAnnotations, @@ -164,7 +158,6 @@ func NewHumioNodeManagerFromHumioNodePool(hc *humiov1alpha1.HumioCluster, hnp *h NodeCount: hnp.NodeCount, DataVolumePersistentVolumeClaimSpecTemplate: hnp.DataVolumePersistentVolumeClaimSpecTemplate, DataVolumeSource: hnp.DataVolumeSource, - AuthServiceAccountName: hnp.AuthServiceAccountName, DisableInitContainer: hnp.DisableInitContainer, EnvironmentVariablesSource: hnp.EnvironmentVariablesSource, PodAnnotations: hnp.PodAnnotations, @@ -308,6 +301,10 @@ func (hnp *HumioNodePool) GetIngress() humiov1alpha1.HumioClusterIngressSpec { return hnp.ingress } +func (hnp HumioNodePool) GetBootstrapTokenName() string { + return hnp.clusterName +} + func (hnp *HumioNodePool) GetEnvironmentVariables() []corev1.EnvVar { envVars := make([]corev1.EnvVar, len(hnp.humioNodeSpec.EnvironmentVariables)) copy(envVars, hnp.humioNodeSpec.EnvironmentVariables) @@ -502,11 +499,7 @@ func (hnp *HumioNodePool) GetPodAnnotations() map[string]string { return hnp.humioNodeSpec.PodAnnotations } -func (hnp *HumioNodePool) GetAuthServiceAccountSecretName() string { - return fmt.Sprintf("%s-%s", hnp.GetNodePoolName(), authServiceAccountSecretNameIdentifier) -} - -func (hnp *HumioNodePool) GetInitServiceAccountSecretName() string { +func (hnp HumioNodePool) GetInitServiceAccountSecretName() string { return fmt.Sprintf("%s-%s", hnp.GetNodePoolName(), initServiceAccountSecretNameIdentifier) } @@ -521,17 +514,6 @@ func (hnp *HumioNodePool) InitServiceAccountIsSetByUser() bool { return hnp.humioNodeSpec.InitServiceAccountName != "" } -func (hnp *HumioNodePool) GetAuthServiceAccountName() string { - if hnp.humioNodeSpec.AuthServiceAccountName != "" { - return hnp.humioNodeSpec.AuthServiceAccountName - } - return fmt.Sprintf("%s-%s", hnp.GetNodePoolName(), authServiceAccountNameSuffix) -} - -func (hnp *HumioNodePool) AuthServiceAccountIsSetByUser() bool { - return hnp.humioNodeSpec.AuthServiceAccountName != "" -} - func (hnp *HumioNodePool) GetInitClusterRoleName() string { return fmt.Sprintf("%s-%s-%s", hnp.GetNamespace(), hnp.GetNodePoolName(), initClusterRoleSuffix) } @@ -540,14 +522,6 @@ func (hnp *HumioNodePool) GetInitClusterRoleBindingName() string { return fmt.Sprintf("%s-%s-%s", hnp.GetNamespace(), hnp.GetNodePoolName(), initClusterRoleBindingSuffix) } -func (hnp *HumioNodePool) GetAuthRoleName() string { - return fmt.Sprintf("%s-%s", hnp.GetNodePoolName(), authRoleSuffix) -} - -func (hnp *HumioNodePool) GetAuthRoleBindingName() string { - return fmt.Sprintf("%s-%s", hnp.GetNodePoolName(), authRoleBindingSuffix) -} - func (hnp *HumioNodePool) GetShareProcessNamespace() *bool { if hnp.humioNodeSpec.ShareProcessNamespace == nil { return helpers.BoolPtr(false) diff --git a/controllers/humiocluster_permission_tokens.go b/controllers/humiocluster_permission_tokens.go new file mode 100644 index 00000000..7ade00fe --- /dev/null +++ b/controllers/humiocluster_permission_tokens.go @@ -0,0 +1,209 @@ +package controllers + +import ( + "context" + "fmt" + + "github.com/humio/humio-operator/pkg/helpers" + + "github.com/humio/humio-operator/pkg/kubernetes" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/apimachinery/pkg/types" + + "github.com/humio/humio-operator/api/v1alpha1" + + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + humioapi "github.com/humio/cli/api" + corev1 "k8s.io/api/core/v1" +) + +// extractExistingHumioAdminUserID finds the user ID of the Humio user for the admin account, and returns +// empty string and no error if the user doesn't exist +func (r *HumioClusterReconciler) extractExistingHumioAdminUserID(config *humioapi.Config, req reconcile.Request, organizationMode string, username string, organization string) (string, error) { + if organizationMode == "multi" || organizationMode == "multiv2" { + allUserResults, err := r.HumioClient.ListAllHumioUsersMultiOrg(config, req, username, organization) + if err != nil { + // unable to list all users + return "", err + } + for _, userResult := range allUserResults { + if userResult.OrganizationName == "RecoveryRootOrg" { + if userResult.SearchMatch == fmt.Sprintf(" | %s () ()", username) { + fmt.Printf("Found user ID using multi-organization query.\n") + return userResult.EntityId, nil + } + } + } + } + + allUsers, err := r.HumioClient.ListAllHumioUsersSingleOrg(config, req) + if err != nil { + // unable to list all users + return "", err + } + for _, user := range allUsers { + if user.Username == username { + return user.Id, nil + } + } + + return "", nil +} + +// createAndGetAdminAccountUserID ensures a Humio admin account exists and returns the user ID for it +func (r *HumioClusterReconciler) createAndGetAdminAccountUserID(ctx context.Context, config *humioapi.Config, req reconcile.Request, organizationMode string, username string, organization string) (string, error) { + // List all users and grab the user ID for an existing user + userID, err := r.extractExistingHumioAdminUserID(config, req, organizationMode, username, organization) + if err != nil { + // Error while grabbing the user ID + return "", err + } + if userID != "" { + // If we found a user ID, return it + return userID, nil + } + + // If we didn't find a user ID, create a user, extract the user ID and return it + user, err := r.HumioClient.AddUser(config, req, username, true) + if err != nil { + return "", err + } + userID, err = r.extractExistingHumioAdminUserID(config, req, organizationMode, username, organization) + if err != nil { + return "", err + } + if userID != "" { + // If we found a user ID, return it + return userID, nil + } + if userID != user.ID { + return "", fmt.Errorf("unexpected error. userid %s does not match %s", userID, user.ID) + } + + // Return error if we didn't find a valid user ID + return "", fmt.Errorf("could not obtain user ID") +} + +// validateAdminSecretContent grabs the current token stored in kubernetes and returns nil if it is valid +func (r *HumioClusterReconciler) validateAdminSecretContent(ctx context.Context, hc *v1alpha1.HumioCluster, req reconcile.Request) error { + // Get existing Kubernetes secret + adminSecretName := fmt.Sprintf("%s-%s", hc.Name, kubernetes.ServiceTokenSecretNameSuffix) + secret := &corev1.Secret{} + key := types.NamespacedName{ + Name: adminSecretName, + Namespace: hc.Namespace, + } + if err := r.Client.Get(ctx, key, secret); err != nil { + return fmt.Errorf("got err while trying to get existing secret from k8s: %w", err) + } + + // Check if secret currently holds a valid humio api token + if _, ok := secret.Data["token"]; ok { + cluster, err := helpers.NewCluster(ctx, r, hc.Name, "", hc.Namespace, helpers.UseCertManager(), true, false) + if err != nil { + return fmt.Errorf("got err while trying to authenticate using apiToken: %w", err) + } + clientNotReady := + cluster.Config().Token != string(secret.Data["token"]) || + cluster.Config().Address == nil + if clientNotReady { + _, err := helpers.NewCluster(ctx, r, hc.Name, "", hc.Namespace, helpers.UseCertManager(), true, false) + if err != nil { + return fmt.Errorf("got err while trying to authenticate using apiToken: %w", err) + } + } + + _, err = r.HumioClient.GetClusters(cluster.Config(), req) + if err != nil { + return fmt.Errorf("got err while trying to use apiToken: %w", err) + } + + // We could successfully get information about the cluster, so the token must be valid + return nil + } + return fmt.Errorf("unable to validate if kubernetes secret %s holds a valid humio API token", adminSecretName) +} + +// ensureAdminSecretContent ensures the target Kubernetes secret contains the desired API token +func (r *HumioClusterReconciler) ensureAdminSecretContent(ctx context.Context, hc *v1alpha1.HumioCluster, desiredAPIToken string) error { + // Get existing Kubernetes secret + adminSecretName := fmt.Sprintf("%s-%s", hc.Name, kubernetes.ServiceTokenSecretNameSuffix) + key := types.NamespacedName{ + Name: adminSecretName, + Namespace: hc.Namespace, + } + adminSecret := &corev1.Secret{} + err := r.Client.Get(ctx, key, adminSecret) + if err != nil { + if k8serrors.IsNotFound(err) { + // If the secret doesn't exist, create it + desiredSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + Labels: kubernetes.LabelsForHumio(hc.Name), + }, + StringData: map[string]string{ + "token": desiredAPIToken, + }, + Type: corev1.SecretTypeOpaque, + } + if err := r.Client.Create(ctx, &desiredSecret); err != nil { + return r.logErrorAndReturn(err, "unable to create secret") + } + return nil + } + return r.logErrorAndReturn(err, "unable to get secret") + } + + // If we got no error, we compare current token with desired token and update if needed. + if adminSecret.StringData["token"] != desiredAPIToken { + adminSecret.StringData = map[string]string{"token": desiredAPIToken} + if err := r.Client.Update(ctx, adminSecret); err != nil { + return r.logErrorAndReturn(err, "unable to update secret") + } + } + + return nil +} + +func (r *HumioClusterReconciler) createPermissionToken(ctx context.Context, config *humioapi.Config, req reconcile.Request, hc *v1alpha1.HumioCluster, username string, organization string) error { + r.Log.Info("ensuring admin user") + + organizationMode := "single" + if EnvVarHasKey(hc.Spec.EnvironmentVariables, "ORGANIZATION_MODE") { + organizationMode = EnvVarValue(hc.Spec.EnvironmentVariables, "ORGANIZATION_MODE") + } + for _, pool := range hc.Spec.NodePools { + if EnvVarHasKey(pool.EnvironmentVariables, "ORGANIZATION_MODE") { + organizationMode = EnvVarValue(pool.EnvironmentVariables, "ORGANIZATION_MODE") + } + } + // Get user ID of admin account + userID, err := r.createAndGetAdminAccountUserID(ctx, config, req, organizationMode, username, organization) + if err != nil { + return fmt.Errorf("got err trying to obtain user ID of admin user: %s", err) + } + + if err := r.validateAdminSecretContent(ctx, hc, req); err == nil { + return nil + } + + // Get API token for user ID of admin account + apiToken, err := r.HumioClient.RotateUserApiTokenAndGet(config, req, userID) + if err != nil { + return r.logErrorAndReturn(err, fmt.Sprintf("failed to rotate api key for userID %s", userID)) + } + + // Update Kubernetes secret if needed + err = r.ensureAdminSecretContent(ctx, hc, apiToken) + if err != nil { + return r.logErrorAndReturn(err, "unable to ensure admin secret") + + } + + return nil +} diff --git a/controllers/humiocluster_pods.go b/controllers/humiocluster_pods.go index c7b837ad..52056294 100644 --- a/controllers/humiocluster_pods.go +++ b/controllers/humiocluster_pods.go @@ -41,7 +41,6 @@ import ( "github.com/humio/humio-operator/pkg/kubernetes" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" ) const ( @@ -53,10 +52,15 @@ const ( ) type podAttachments struct { - dataVolumeSource corev1.VolumeSource - initServiceAccountSecretName string - authServiceAccountSecretName string - envVarSourceData *map[string]string + dataVolumeSource corev1.VolumeSource + initServiceAccountSecretName string + envVarSourceData *map[string]string + bootstrapTokenSecretReference bootstrapTokenSecret +} + +type bootstrapTokenSecret struct { + hash string + secretReference *corev1.SecretKeySelector } // ConstructContainerArgs returns the container arguments for the Humio pods. We want to grab a UUID from zookeeper @@ -105,96 +109,6 @@ func ConstructPod(hnp *HumioNodePool, humioNodeName string, attachments *podAtta Subdomain: headlessServiceName(hnp.GetClusterName()), Hostname: humioNodeName, Containers: []corev1.Container{ - { - Name: AuthContainerName, - Image: hnp.GetHelperImage(), - ImagePullPolicy: hnp.GetImagePullPolicy(), - Env: []corev1.EnvVar{ - { - Name: "NAMESPACE", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.namespace", - }, - }, - }, - { - Name: "POD_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.name", - }, - }, - }, - { - Name: "MODE", - Value: "auth", - }, - { - Name: "ADMIN_SECRET_NAME_SUFFIX", - Value: kubernetes.ServiceTokenSecretNameSuffix, - }, - { - Name: "CLUSTER_NAME", - Value: hnp.GetClusterName(), - }, - { - Name: "HUMIO_NODE_URL", - Value: fmt.Sprintf("%s://$(POD_NAME):%d/", strings.ToLower(string(hnp.GetProbeScheme())), HumioPort), - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "humio-data", - MountPath: HumioDataPath, - ReadOnly: true, - }, - { - Name: "auth-service-account-secret", - MountPath: "/var/run/secrets/kubernetes.io/serviceaccount", - ReadOnly: true, - }, - }, - ReadinessProbe: &corev1.Probe{ - FailureThreshold: 3, - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/", - Port: intstr.IntOrString{IntVal: 8180}, - Scheme: corev1.URISchemeHTTP, - }, - }, - PeriodSeconds: 10, - SuccessThreshold: 1, - TimeoutSeconds: 1, - }, - LivenessProbe: &corev1.Probe{ - FailureThreshold: 3, - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/", - Port: intstr.IntOrString{IntVal: 8180}, - Scheme: corev1.URISchemeHTTP, - }, - }, - PeriodSeconds: 10, - SuccessThreshold: 1, - TimeoutSeconds: 1, - }, - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), - corev1.ResourceMemory: *resource.NewQuantity(750*1024*1024, resource.BinarySI), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), - corev1.ResourceMemory: *resource.NewQuantity(150*1024*1024, resource.BinarySI), - }, - }, - SecurityContext: hnp.GetContainerSecurityContext(), - }, { Name: HumioContainerName, Image: hnp.GetImage(), @@ -245,15 +159,6 @@ func ConstructPod(hnp *HumioNodePool, humioNodeName string, attachments *podAtta Name: "tmp", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, - { - Name: "auth-service-account-secret", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: attachments.authServiceAccountSecretName, - DefaultMode: &mode, - }, - }, - }, }, Affinity: hnp.GetAffinity(), Tolerations: hnp.GetTolerations(), @@ -377,6 +282,15 @@ func ConstructPod(hnp *HumioNodePool, humioNodeName string, attachments *podAtta }) } + if attachments.bootstrapTokenSecretReference.secretReference != nil { + pod.Spec.Containers[humioIdx].Env = append(pod.Spec.Containers[humioIdx].Env, corev1.EnvVar{ + Name: "BOOTSTRAP_ROOT_TOKEN_HASHED", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: attachments.bootstrapTokenSecretReference.secretReference, + }, + }) + } + if hnp.GetExtraKafkaConfigs() != "" { pod.Spec.Containers[humioIdx].Env = append(pod.Spec.Containers[humioIdx].Env, corev1.EnvVar{ Name: "EXTRA_KAFKA_CONFIGS_FILE", @@ -528,19 +442,6 @@ func ConstructPod(hnp *HumioNodePool, humioNodeName string, attachments *podAtta MountPath: "/var/lib/humio/tls-certificate-secret", }) - // Configuration specific to auth container - authIdx, err := kubernetes.GetContainerIndexByName(pod, AuthContainerName) - if err != nil { - return &corev1.Pod{}, err - } - // We mount in the certificate on top of default system root certs so auth container automatically uses it: - // https://golang.org/src/crypto/x509/root_linux.go - pod.Spec.Containers[authIdx].VolumeMounts = append(pod.Spec.Containers[authIdx].VolumeMounts, corev1.VolumeMount{ - Name: "ca-cert", - ReadOnly: true, - MountPath: "/etc/pki/tls", - }) - // Common configuration for all containers pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ Name: "tls-cert", @@ -569,22 +470,14 @@ func ConstructPod(hnp *HumioNodePool, humioNodeName string, attachments *podAtta }) } + if attachments.bootstrapTokenSecretReference.hash != "" { + pod.Annotations[bootstrapTokenHashAnnotation] = attachments.bootstrapTokenSecretReference.hash + } priorityClassName := hnp.GetPriorityClassName() if priorityClassName != "" { pod.Spec.PriorityClassName = priorityClassName } - if EnvVarHasValue(pod.Spec.Containers[humioIdx].Env, "ENABLE_ORGANIZATIONS", "true") && EnvVarHasKey(pod.Spec.Containers[humioIdx].Env, "ORGANIZATION_MODE") { - authIdx, err := kubernetes.GetContainerIndexByName(pod, AuthContainerName) - if err != nil { - return &corev1.Pod{}, err - } - pod.Spec.Containers[authIdx].Env = append(pod.Spec.Containers[authIdx].Env, corev1.EnvVar{ - Name: "ORGANIZATION_MODE", - Value: EnvVarValue(pod.Spec.Containers[humioIdx].Env, "ORGANIZATION_MODE"), - }) - } - containerArgs, err := ConstructContainerArgs(hnp, pod.Spec.Containers[humioIdx].Env) if err != nil { return &corev1.Pod{}, fmt.Errorf("unable to construct node container args: %w", err) @@ -660,17 +553,6 @@ func sanitizePod(hnp *HumioNodePool, pod *corev1.Pod) *corev1.Pod { } } container.Env = sanitizedEnvVars - } else if container.Name == AuthContainerName { - for _, envVar := range container.Env { - if envVar.Name == "HUMIO_NODE_URL" { - sanitizedEnvVars = append(sanitizedEnvVars, corev1.EnvVar{ - Name: "HUMIO_NODE_URL", - Value: fmt.Sprintf("%s://%s-core-%s.%s:%d/", strings.ToLower(string(hnp.GetProbeScheme())), hnp.GetNodePoolName(), "", hnp.GetNamespace(), HumioPort), - }) - } else { - sanitizedEnvVars = append(sanitizedEnvVars, envVar) - } - } } else { sanitizedEnvVars = container.Env } @@ -708,16 +590,7 @@ func sanitizePod(hnp *HumioNodePool, pod *corev1.Pod) *corev1.Pod { }, }, }) - } else if volume.Name == "auth-service-account-secret" { - sanitizedVolumes = append(sanitizedVolumes, corev1.Volume{ - Name: "auth-service-account-secret", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: fmt.Sprintf("%s-auth-%s", hnp.GetNodePoolName(), ""), - DefaultMode: &mode, - }, - }, - }) + } else if strings.HasPrefix("kube-api-access-", volume.Name) { sanitizedVolumes = append(sanitizedVolumes, corev1.Volume{ Name: "kube-api-access-", @@ -784,6 +657,12 @@ func (r *HumioClusterReconciler) createPod(ctx context.Context, hc *humiov1alpha return &corev1.Pod{}, r.logErrorAndReturn(err, "unable to find pod name") } + bootstrapTokenHash, err := r.getDesiredBootstrapTokenHash(ctx, hc) + if err != nil { + return &corev1.Pod{}, r.logErrorAndReturn(err, "unable to find bootstrap token secret") + } + attachments.bootstrapTokenSecretReference.hash = bootstrapTokenHash + pod, err := ConstructPod(hnp, podNameAndCertHash.podName, attachments) if err != nil { return &corev1.Pod{}, r.logErrorAndReturn(err, "unable to construct pod") @@ -867,6 +746,7 @@ func (r *HumioClusterReconciler) podsMatch(hnp *HumioNodePool, pod corev1.Pod, d var revisionMatches bool var envVarSourceMatches bool var certHasAnnotationMatches bool + var bootstrapTokenAnootationMatches bool desiredPodHash := podSpecAsSHA256(hnp, desiredPod) _, existingPodRevision := hnp.GetHumioClusterNodePoolRevisionAnnotation() @@ -897,6 +777,16 @@ func (r *HumioClusterReconciler) podsMatch(hnp *HumioNodePool, pod corev1.Pod, d certHasAnnotationMatches = true } } + if _, ok := pod.Annotations[bootstrapTokenHashAnnotation]; ok { + if pod.Annotations[bootstrapTokenHashAnnotation] == desiredPod.Annotations[bootstrapTokenHashAnnotation] { + bootstrapTokenAnootationMatches = true + } + } else { + // Ignore bootstrapTokenHashAnnotation if it's not in either the current pod or the desired pod + if _, ok := desiredPod.Annotations[bootstrapTokenHashAnnotation]; !ok { + bootstrapTokenAnootationMatches = true + } + } currentPodCopy := pod.DeepCopy() desiredPodCopy := desiredPod.DeepCopy() @@ -919,6 +809,10 @@ func (r *HumioClusterReconciler) podsMatch(hnp *HumioNodePool, pod corev1.Pod, d r.Log.Info(fmt.Sprintf("pod annotation %s does not match desired pod: got %+v, expected %+v", certHashAnnotation, pod.Annotations[certHashAnnotation], desiredPod.Annotations[certHashAnnotation]), "podSpecDiff", podSpecDiff) return false, nil } + if !bootstrapTokenAnootationMatches { + r.Log.Info(fmt.Sprintf("pod annotation %s bootstrapTokenAnootationMatches not match desired pod: got %+v, expected %+v", bootstrapTokenHashAnnotation, pod.Annotations[bootstrapTokenHashAnnotation], desiredPod.Annotations[bootstrapTokenHashAnnotation]), "podSpecDiff", podSpecDiff) + return false, nil + } return true, nil } @@ -940,6 +834,10 @@ func (r *HumioClusterReconciler) getPodDesiredLifecycleState(hnp *HumioNodePool, desiredPod.Annotations[certHashAnnotation] = GetDesiredCertHash(hnp) } + if attachments.bootstrapTokenSecretReference.secretReference != nil { + desiredPod.Annotations[bootstrapTokenHashAnnotation] = attachments.bootstrapTokenSecretReference.hash + } + podsMatch, err := r.podsMatch(hnp, pod, *desiredPod) if err != nil { r.Log.Error(err, "failed to check if pods match") @@ -982,6 +880,35 @@ type podNameAndCertificateHash struct { podName, certificateHash string } +func (r *HumioClusterReconciler) getDesiredBootstrapTokenHash(ctx context.Context, hc *humiov1alpha1.HumioCluster) (string, error) { + humioBootstrapTokens, err := kubernetes.ListHumioBootstrapTokens(ctx, r.Client, hc.GetNamespace(), kubernetes.LabelsForHumioBootstrapToken(hc.GetName())) + if err != nil { + return "", err + } + + if len(humioBootstrapTokens) == 0 { + return "", fmt.Errorf("could not find bootstrap token matching labels %+v: %w", kubernetes.LabelsForHumioBootstrapToken(hc.GetName()), err) + } + + if humioBootstrapTokens[0].Status.State != humiov1alpha1.HumioBootstrapTokenStateReady { + return "", fmt.Errorf("bootstrap token not ready. status=%s", humioBootstrapTokens[0].Status.State) + } + + existingSecret := &corev1.Secret{} + if err := r.Get(ctx, types.NamespacedName{ + Namespace: hc.GetNamespace(), + Name: humioBootstrapTokens[0].Status.HashedTokenSecretKeyRef.SecretKeyRef.Name, + }, existingSecret); err != nil { + return "", fmt.Errorf("failed to get bootstrap token secret %s: %w", + humioBootstrapTokens[0].Status.HashedTokenSecretKeyRef.SecretKeyRef.Name, err) + } + + if ok := string(existingSecret.Data[humioBootstrapTokens[0].Status.HashedTokenSecretKeyRef.SecretKeyRef.Key]); ok != "" { + return helpers.AsSHA256(string(existingSecret.Data[humioBootstrapTokens[0].Status.HashedTokenSecretKeyRef.SecretKeyRef.Key])), nil + } + return "", fmt.Errorf("bootstrap token %s does not have a value for key %s", humioBootstrapTokens[0].Name, humioBootstrapTokens[0].Status.HashedTokenSecretKeyRef.SecretKeyRef.Key) +} + // findHumioNodeNameAndCertHash looks up the name of a free node certificate to use and the hash of the certificate specification func findHumioNodeNameAndCertHash(ctx context.Context, c client.Client, hnp *HumioNodePool, newlyCreatedPods []corev1.Pod) (podNameAndCertificateHash, error) { // if we do not have TLS enabled, append a random suffix @@ -1046,18 +973,33 @@ func (r *HumioClusterReconciler) newPodAttachments(ctx context.Context, hnp *Hum if volumeSource.PersistentVolumeClaim != nil { pvcClaimNamesInUse[volumeSource.PersistentVolumeClaim.ClaimName] = struct{}{} } - authSASecretName, err := r.getAuthServiceAccountSecretName(ctx, hnp) + + envVarSourceData, err := r.getEnvVarSource(ctx, hnp) if err != nil { - return &podAttachments{}, fmt.Errorf("unable get auth service account secret for HumioCluster: %w", err) + return &podAttachments{}, fmt.Errorf("unable to create Pod for HumioCluster: %w", err) + } + key := types.NamespacedName{ + Name: hnp.GetClusterName(), + Namespace: hnp.GetNamespace(), } - if authSASecretName == "" { - return &podAttachments{}, errors.New("unable to create Pod for HumioCluster: the auth service account secret does not exist") + hbt := &humiov1alpha1.HumioBootstrapToken{} + err = r.Client.Get(ctx, key, hbt) + if err != nil { + return &podAttachments{}, fmt.Errorf("unable to create Pod for HumioCluster. could not find HumioBootstrapToken: %w", err) } + + if hbt.Status.HashedTokenSecretKeyRef.SecretKeyRef == nil { + return &podAttachments{}, fmt.Errorf("unable to create Pod for HumioCluster: %w", fmt.Errorf("bootstraptoken %s does not contain a status for the hashed token secret reference", hnp.GetBootstrapTokenName())) + } + if hnp.InitContainerDisabled() { return &podAttachments{ - dataVolumeSource: volumeSource, - authServiceAccountSecretName: authSASecretName, + dataVolumeSource: volumeSource, + envVarSourceData: envVarSourceData, + bootstrapTokenSecretReference: bootstrapTokenSecret{ + secretReference: hbt.Status.HashedTokenSecretKeyRef.SecretKeyRef, + }, }, nil } @@ -1069,16 +1011,13 @@ func (r *HumioClusterReconciler) newPodAttachments(ctx context.Context, hnp *Hum return &podAttachments{}, errors.New("unable to create Pod for HumioCluster: the init service account secret does not exist") } - envVarSourceData, err := r.getEnvVarSource(ctx, hnp) - if err != nil { - return &podAttachments{}, fmt.Errorf("unable to create Pod for HumioCluster: %w", err) - } - return &podAttachments{ dataVolumeSource: volumeSource, initServiceAccountSecretName: initSASecretName, - authServiceAccountSecretName: authSASecretName, envVarSourceData: envVarSourceData, + bootstrapTokenSecretReference: bootstrapTokenSecret{ + secretReference: hbt.Status.HashedTokenSecretKeyRef.SecretKeyRef, + }, }, nil } diff --git a/controllers/humioexternalcluster_controller.go b/controllers/humioexternalcluster_controller.go index 6b56b179..f7497bcd 100644 --- a/controllers/humioexternalcluster_controller.go +++ b/controllers/humioexternalcluster_controller.go @@ -19,11 +19,12 @@ package controllers import ( "context" "fmt" + "time" + "github.com/humio/humio-operator/pkg/helpers" "github.com/humio/humio-operator/pkg/kubernetes" k8serrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "time" "github.com/go-logr/logr" ctrl "sigs.k8s.io/controller-runtime" @@ -79,7 +80,7 @@ func (r *HumioExternalClusterReconciler) Reconcile(ctx context.Context, req ctrl } } - cluster, err := helpers.NewCluster(ctx, r, "", hec.Name, hec.Namespace, helpers.UseCertManager(), true) + cluster, err := helpers.NewCluster(ctx, r, "", hec.Name, hec.Namespace, helpers.UseCertManager(), true, false) if err != nil || cluster.Config() == nil { return reconcile.Result{}, r.logErrorAndReturn(fmt.Errorf("unable to obtain humio client config: %w", err), "unable to obtain humio client config") } diff --git a/controllers/humiofilteralert_controller.go b/controllers/humiofilteralert_controller.go index d620b438..4b202d00 100644 --- a/controllers/humiofilteralert_controller.go +++ b/controllers/humiofilteralert_controller.go @@ -77,7 +77,7 @@ func (r *HumioFilterAlertReconciler) Reconcile(ctx context.Context, req ctrl.Req r.Log = r.Log.WithValues("Request.UID", hfa.UID) - cluster, err := helpers.NewCluster(ctx, r, hfa.Spec.ManagedClusterName, hfa.Spec.ExternalClusterName, hfa.Namespace, helpers.UseCertManager(), true) + cluster, err := helpers.NewCluster(ctx, r, hfa.Spec.ManagedClusterName, hfa.Spec.ExternalClusterName, hfa.Namespace, helpers.UseCertManager(), true, false) if err != nil || cluster == nil || cluster.Config() == nil { setStateErr := r.setState(ctx, humiov1alpha1.HumioFilterAlertStateConfigError, hfa) if setStateErr != nil { diff --git a/controllers/humioingesttoken_controller.go b/controllers/humioingesttoken_controller.go index f27b9f7d..f9bba373 100644 --- a/controllers/humioingesttoken_controller.go +++ b/controllers/humioingesttoken_controller.go @@ -20,6 +20,8 @@ import ( "context" "errors" "fmt" + "time" + "github.com/go-logr/logr" humioapi "github.com/humio/cli/api" "github.com/humio/humio-operator/pkg/helpers" @@ -30,7 +32,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "time" humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1" "github.com/humio/humio-operator/pkg/humio" @@ -77,7 +78,7 @@ func (r *HumioIngestTokenReconciler) Reconcile(ctx context.Context, req ctrl.Req r.Log = r.Log.WithValues("Request.UID", hit.UID) - cluster, err := helpers.NewCluster(ctx, r, hit.Spec.ManagedClusterName, hit.Spec.ExternalClusterName, hit.Namespace, helpers.UseCertManager(), true) + cluster, err := helpers.NewCluster(ctx, r, hit.Spec.ManagedClusterName, hit.Spec.ExternalClusterName, hit.Namespace, helpers.UseCertManager(), true, false) if err != nil || cluster == nil || cluster.Config() == nil { setStateErr := r.setState(ctx, humiov1alpha1.HumioIngestTokenStateConfigError, hit) if setStateErr != nil { @@ -184,7 +185,7 @@ func (r *HumioIngestTokenReconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *HumioIngestTokenReconciler) finalize(ctx context.Context, config *humioapi.Config, req reconcile.Request, hit *humiov1alpha1.HumioIngestToken) error { - _, err := helpers.NewCluster(ctx, r, hit.Spec.ManagedClusterName, hit.Spec.ExternalClusterName, hit.Namespace, helpers.UseCertManager(), true) + _, err := helpers.NewCluster(ctx, r, hit.Spec.ManagedClusterName, hit.Spec.ExternalClusterName, hit.Namespace, helpers.UseCertManager(), true, false) if err != nil { if k8serrors.IsNotFound(err) { return nil diff --git a/controllers/humioparser_controller.go b/controllers/humioparser_controller.go index 14d15799..cc257d5a 100644 --- a/controllers/humioparser_controller.go +++ b/controllers/humioparser_controller.go @@ -77,7 +77,7 @@ func (r *HumioParserReconciler) Reconcile(ctx context.Context, req ctrl.Request) r.Log = r.Log.WithValues("Request.UID", hp.UID) - cluster, err := helpers.NewCluster(ctx, r, hp.Spec.ManagedClusterName, hp.Spec.ExternalClusterName, hp.Namespace, helpers.UseCertManager(), true) + cluster, err := helpers.NewCluster(ctx, r, hp.Spec.ManagedClusterName, hp.Spec.ExternalClusterName, hp.Namespace, helpers.UseCertManager(), true, false) if err != nil || cluster == nil || cluster.Config() == nil { setStateErr := r.setState(ctx, humiov1alpha1.HumioParserStateConfigError, hp) if setStateErr != nil { @@ -201,7 +201,7 @@ func (r *HumioParserReconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *HumioParserReconciler) finalize(ctx context.Context, config *humioapi.Config, req reconcile.Request, hp *humiov1alpha1.HumioParser) error { - _, err := helpers.NewCluster(ctx, r, hp.Spec.ManagedClusterName, hp.Spec.ExternalClusterName, hp.Namespace, helpers.UseCertManager(), true) + _, err := helpers.NewCluster(ctx, r, hp.Spec.ManagedClusterName, hp.Spec.ExternalClusterName, hp.Namespace, helpers.UseCertManager(), true, false) if err != nil { if k8serrors.IsNotFound(err) { return nil diff --git a/controllers/humiorepository_controller.go b/controllers/humiorepository_controller.go index ff4238e8..3d6829bb 100644 --- a/controllers/humiorepository_controller.go +++ b/controllers/humiorepository_controller.go @@ -75,7 +75,7 @@ func (r *HumioRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Requ r.Log = r.Log.WithValues("Request.UID", hr.UID) - cluster, err := helpers.NewCluster(ctx, r, hr.Spec.ManagedClusterName, hr.Spec.ExternalClusterName, hr.Namespace, helpers.UseCertManager(), true) + cluster, err := helpers.NewCluster(ctx, r, hr.Spec.ManagedClusterName, hr.Spec.ExternalClusterName, hr.Namespace, helpers.UseCertManager(), true, false) if err != nil || cluster == nil || cluster.Config() == nil { setStateErr := r.setState(ctx, humiov1alpha1.HumioRepositoryStateConfigError, hr) if setStateErr != nil { @@ -189,7 +189,7 @@ func (r *HumioRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *HumioRepositoryReconciler) finalize(ctx context.Context, config *humioapi.Config, req reconcile.Request, hr *humiov1alpha1.HumioRepository) error { - _, err := helpers.NewCluster(ctx, r, hr.Spec.ManagedClusterName, hr.Spec.ExternalClusterName, hr.Namespace, helpers.UseCertManager(), true) + _, err := helpers.NewCluster(ctx, r, hr.Spec.ManagedClusterName, hr.Spec.ExternalClusterName, hr.Namespace, helpers.UseCertManager(), true, false) if err != nil { if k8serrors.IsNotFound(err) { return nil diff --git a/controllers/humioscheduledsearch_controller.go b/controllers/humioscheduledsearch_controller.go index 31d6b38e..af3b72fc 100644 --- a/controllers/humioscheduledsearch_controller.go +++ b/controllers/humioscheduledsearch_controller.go @@ -77,7 +77,7 @@ func (r *HumioScheduledSearchReconciler) Reconcile(ctx context.Context, req ctrl r.Log = r.Log.WithValues("Request.UID", hss.UID) - cluster, err := helpers.NewCluster(ctx, r, hss.Spec.ManagedClusterName, hss.Spec.ExternalClusterName, hss.Namespace, helpers.UseCertManager(), true) + cluster, err := helpers.NewCluster(ctx, r, hss.Spec.ManagedClusterName, hss.Spec.ExternalClusterName, hss.Namespace, helpers.UseCertManager(), true, false) if err != nil || cluster == nil || cluster.Config() == nil { setStateErr := r.setState(ctx, humiov1alpha1.HumioScheduledSearchStateConfigError, hss) if setStateErr != nil { diff --git a/controllers/humioview_controller.go b/controllers/humioview_controller.go index e56e04e3..d5a70d29 100644 --- a/controllers/humioview_controller.go +++ b/controllers/humioview_controller.go @@ -75,7 +75,7 @@ func (r *HumioViewReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( r.Log = r.Log.WithValues("Request.UID", hv.UID) - cluster, err := helpers.NewCluster(ctx, r, hv.Spec.ManagedClusterName, hv.Spec.ExternalClusterName, hv.Namespace, helpers.UseCertManager(), true) + cluster, err := helpers.NewCluster(ctx, r, hv.Spec.ManagedClusterName, hv.Spec.ExternalClusterName, hv.Namespace, helpers.UseCertManager(), true, false) if err != nil || cluster == nil || cluster.Config() == nil { setStateErr := r.setState(ctx, humiov1alpha1.HumioParserStateConfigError, hv) if setStateErr != nil { diff --git a/controllers/suite/clusters/humiocluster_controller_test.go b/controllers/suite/clusters/humiocluster_controller_test.go index b62b8c99..0a43158a 100644 --- a/controllers/suite/clusters/humiocluster_controller_test.go +++ b/controllers/suite/clusters/humiocluster_controller_test.go @@ -19,11 +19,12 @@ package clusters import ( "context" "fmt" - cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" "os" "reflect" "strings" + cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1" "github.com/humio/humio-operator/controllers" "github.com/humio/humio-operator/controllers/suite" @@ -182,7 +183,7 @@ var _ = Describe("HumioCluster Controller", func() { }) toCreate.Spec.EnvironmentVariables = append(toCreate.Spec.EnvironmentVariables, corev1.EnvVar{ Name: "ORGANIZATION_MODE", - Value: "multi", + Value: "multiv2", }) suite.UsingClusterBy(key.Name, "Creating the cluster successfully") @@ -1242,18 +1243,6 @@ var _ = Describe("HumioCluster Controller", func() { clusterPods, _ := kubernetes.ListPods(ctx, k8sClient, key.Namespace, controllers.NewHumioNodeManagerFromHumioCluster(toCreate).GetPodLabels()) - suite.UsingClusterBy(key.Name, "Validating pod uses default helper image as auth sidecar container") - Eventually(func() string { - clusterPods, _ := kubernetes.ListPods(ctx, k8sClient, key.Namespace, controllers.NewHumioNodeManagerFromHumioCluster(toCreate).GetPodLabels()) - _ = suite.MarkPodsAsRunning(ctx, k8sClient, clusterPods, key.Name) - - for _, pod := range clusterPods { - authIdx, _ := kubernetes.GetContainerIndexByName(pod, controllers.AuthContainerName) - return pod.Spec.InitContainers[authIdx].Image - } - return "" - }, testTimeout, suite.TestInterval).Should(Equal(controllers.HelperImage)) - suite.UsingClusterBy(key.Name, "Overriding helper image") var updatedHumioCluster humiov1alpha1.HumioCluster customHelperImage := "humio/humio-operator-helper:master" @@ -1279,15 +1268,74 @@ var _ = Describe("HumioCluster Controller", func() { return "" }, testTimeout, suite.TestInterval).Should(Equal(customHelperImage)) - suite.UsingClusterBy(key.Name, "Validating pod is recreated using the explicitly defined helper image as auth sidecar container") + updatedClusterPods, _ := kubernetes.ListPods(ctx, k8sClient, key.Namespace, controllers.NewHumioNodeManagerFromHumioCluster(toCreate).GetPodLabels()) + + if os.Getenv("TEST_USE_EXISTING_CLUSTER") == "true" { + suite.UsingClusterBy(key.Name, "Ensuring pod names are not changed") + Expect(podNames(clusterPods)).To(Equal(podNames(updatedClusterPods))) + } + }) + }) + + Context("Humio Cluster Rotate Bootstrap Token", func() { + It("Update should correctly replace pods to use new bootstrap token", func() { + key := types.NamespacedName{ + Name: "humiocluster-rotate-bootstrap-token", + Namespace: testProcessNamespace, + } + toCreate := suite.ConstructBasicSingleNodeHumioCluster(key, true) + toCreate.Spec.NodeCount = 2 + + suite.UsingClusterBy(key.Name, "Creating a cluster") + ctx := context.Background() + suite.CreateAndBootstrapCluster(ctx, k8sClient, testHumioClient, toCreate, true, humiov1alpha1.HumioClusterStateRunning, testTimeout) + defer suite.CleanupCluster(ctx, k8sClient, toCreate) + + suite.UsingClusterBy(key.Name, "Validating pod bootstrap token annotation hash") Eventually(func() string { clusterPods, _ := kubernetes.ListPods(ctx, k8sClient, key.Namespace, controllers.NewHumioNodeManagerFromHumioCluster(toCreate).GetPodLabels()) - for _, pod := range clusterPods { - authIdx, _ := kubernetes.GetContainerIndexByName(pod, controllers.AuthContainerName) - return pod.Spec.InitContainers[authIdx].Image + _ = suite.MarkPodsAsRunning(ctx, k8sClient, clusterPods, key.Name) + + if len(clusterPods) > 0 { + return clusterPods[0].Annotations["humio.com/bootstrap-token-hash"] } return "" - }, testTimeout, suite.TestInterval).Should(Equal(customHelperImage)) + }, testTimeout, suite.TestInterval).Should(Not(Equal(""))) + + clusterPods, _ := kubernetes.ListPods(ctx, k8sClient, key.Namespace, controllers.NewHumioNodeManagerFromHumioCluster(toCreate).GetPodLabels()) + bootstrapTokenHashValue := clusterPods[0].Annotations["humio.com/bootstrap-token-hash"] + + suite.UsingClusterBy(key.Name, "Rotating bootstrap token") + var bootstrapTokenSecret corev1.Secret + + bootstrapTokenSecretKey := types.NamespacedName{ + Name: fmt.Sprintf("%s-%s", key.Name, kubernetes.BootstrapTokenSecretNameSuffix), + Namespace: key.Namespace, + } + Expect(k8sClient.Get(ctx, bootstrapTokenSecretKey, &bootstrapTokenSecret)).To(BeNil()) + bootstrapTokenSecret.Data["hashedToken"] = []byte("some new token") + Expect(k8sClient.Update(ctx, &bootstrapTokenSecret)).To(BeNil()) + + var updatedHumioCluster humiov1alpha1.HumioCluster + Eventually(func() string { + updatedHumioCluster = humiov1alpha1.HumioCluster{} + Expect(k8sClient.Get(ctx, key, &updatedHumioCluster)).Should(Succeed()) + return updatedHumioCluster.Status.State + }, testTimeout, suite.TestInterval).Should(BeIdenticalTo(humiov1alpha1.HumioClusterStateRestarting)) + + suite.UsingClusterBy(key.Name, "Restarting the cluster in a rolling fashion") + ensurePodsRollingRestart(ctx, controllers.NewHumioNodeManagerFromHumioCluster(&updatedHumioCluster), 2) + + suite.UsingClusterBy(key.Name, "Validating pod is recreated with the new bootstrap token hash annotation") + Eventually(func() string { + clusterPods, _ := kubernetes.ListPods(ctx, k8sClient, key.Namespace, controllers.NewHumioNodeManagerFromHumioCluster(toCreate).GetPodLabels()) + _ = suite.MarkPodsAsRunning(ctx, k8sClient, clusterPods, key.Name) + + if len(clusterPods) > 0 { + return clusterPods[0].Annotations["humio.com/bootstrap-token-hash"] + } + return "" + }, testTimeout, suite.TestInterval).Should(Not(Equal(bootstrapTokenHashValue))) updatedClusterPods, _ := kubernetes.ListPods(ctx, k8sClient, key.Namespace, controllers.NewHumioNodeManagerFromHumioCluster(toCreate).GetPodLabels()) @@ -3314,7 +3362,7 @@ var _ = Describe("HumioCluster Controller", func() { suite.CreateAndBootstrapCluster(ctx, k8sClient, testHumioClient, toCreate, true, humiov1alpha1.HumioClusterStateRunning, testTimeout) defer suite.CleanupCluster(ctx, k8sClient, toCreate) - initialExpectedVolumesCount := 6 + initialExpectedVolumesCount := 5 initialExpectedVolumeMountsCount := 4 if os.Getenv("TEST_USE_EXISTING_CLUSTER") == "true" { @@ -4143,7 +4191,6 @@ var _ = Describe("HumioCluster Controller", func() { } toCreate := suite.ConstructBasicSingleNodeHumioCluster(key, true) toCreate.Spec.InitServiceAccountName = "init-custom-service-account" - toCreate.Spec.AuthServiceAccountName = "auth-custom-service-account" toCreate.Spec.HumioServiceAccountName = "humio-custom-service-account" suite.UsingClusterBy(key.Name, "Creating the cluster successfully") @@ -4170,24 +4217,6 @@ var _ = Describe("HumioCluster Controller", func() { } } } - suite.UsingClusterBy(key.Name, "Confirming auth container is using the correct service account") - for _, pod := range clusterPods { - humioIdx, _ := kubernetes.GetContainerIndexByName(pod, controllers.AuthContainerName) - var serviceAccountSecretVolumeName string - for _, volumeMount := range pod.Spec.Containers[humioIdx].VolumeMounts { - if volumeMount.MountPath == "/var/run/secrets/kubernetes.io/serviceaccount" { - serviceAccountSecretVolumeName = volumeMount.Name - } - } - Expect(serviceAccountSecretVolumeName).To(Not(BeEmpty())) - for _, volume := range pod.Spec.Volumes { - if volume.Name == serviceAccountSecretVolumeName { - secret, err := kubernetes.GetSecret(ctx, k8sClient, volume.Secret.SecretName, key.Namespace) - Expect(err).ShouldNot(HaveOccurred()) - Expect(secret.ObjectMeta.Annotations[corev1.ServiceAccountNameKey]).To(Equal(toCreate.Spec.AuthServiceAccountName)) - } - } - } suite.UsingClusterBy(key.Name, "Confirming humio pod is using the correct service account") for _, pod := range clusterPods { Expect(pod.Spec.ServiceAccountName).To(Equal(toCreate.Spec.HumioServiceAccountName)) @@ -4201,7 +4230,6 @@ var _ = Describe("HumioCluster Controller", func() { } toCreate := suite.ConstructBasicSingleNodeHumioCluster(key, true) toCreate.Spec.InitServiceAccountName = "custom-service-account" - toCreate.Spec.AuthServiceAccountName = "custom-service-account" toCreate.Spec.HumioServiceAccountName = "custom-service-account" suite.UsingClusterBy(key.Name, "Creating the cluster successfully") @@ -4228,24 +4256,6 @@ var _ = Describe("HumioCluster Controller", func() { } } } - suite.UsingClusterBy(key.Name, "Confirming auth container is using the correct service account") - for _, pod := range clusterPods { - humioIdx, _ := kubernetes.GetContainerIndexByName(pod, controllers.AuthContainerName) - var serviceAccountSecretVolumeName string - for _, volumeMount := range pod.Spec.Containers[humioIdx].VolumeMounts { - if volumeMount.MountPath == "/var/run/secrets/kubernetes.io/serviceaccount" { - serviceAccountSecretVolumeName = volumeMount.Name - } - } - Expect(serviceAccountSecretVolumeName).To(Not(BeEmpty())) - for _, volume := range pod.Spec.Volumes { - if volume.Name == serviceAccountSecretVolumeName { - secret, err := kubernetes.GetSecret(ctx, k8sClient, volume.Secret.SecretName, key.Namespace) - Expect(err).ShouldNot(HaveOccurred()) - Expect(secret.ObjectMeta.Annotations[corev1.ServiceAccountNameKey]).To(Equal(toCreate.Spec.AuthServiceAccountName)) - } - } - } suite.UsingClusterBy(key.Name, "Confirming humio pod is using the correct service account") for _, pod := range clusterPods { Expect(pod.Spec.ServiceAccountName).To(Equal(toCreate.Spec.HumioServiceAccountName)) @@ -4436,7 +4446,7 @@ var _ = Describe("HumioCluster Controller", func() { if pod.Spec.ShareProcessNamespace != nil { Expect(*pod.Spec.ShareProcessNamespace).To(BeFalse()) } - Expect(pod.Spec.Containers).Should(HaveLen(2)) + Expect(pod.Spec.Containers).Should(HaveLen(1)) } suite.UsingClusterBy(key.Name, "Enabling shared process namespace and sidecars") @@ -4498,9 +4508,6 @@ var _ = Describe("HumioCluster Controller", func() { if container.Name == controllers.HumioContainerName { continue } - if container.Name == controllers.AuthContainerName { - continue - } return container.Name } } diff --git a/controllers/suite/common.go b/controllers/suite/common.go index 8f88b02f..88514c20 100644 --- a/controllers/suite/common.go +++ b/controllers/suite/common.go @@ -10,8 +10,6 @@ import ( "strings" "time" - ginkgotypes "github.com/onsi/ginkgo/v2/types" - humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1" "github.com/humio/humio-operator/controllers" "github.com/humio/humio-operator/pkg/helpers" @@ -33,10 +31,6 @@ import ( ) const ( - // apiTokenMethodAnnotationName is used to signal what mechanism was used to obtain the API token - apiTokenMethodAnnotationName = "humio.com/api-token-method" // #nosec G101 - // apiTokenMethodFromAPI is used to indicate that the API token was obtained using an API call - apiTokenMethodFromAPI = "api" // dockerUsernameEnvVar is used to login to docker when pulling images dockerUsernameEnvVar = "DOCKER_USERNAME" // dockerPasswordEnvVar is used to login to docker when pulling images @@ -118,22 +112,6 @@ func CleanupCluster(ctx context.Context, k8sClient client.Client, hc *humiov1alp Expect(k8sClient.Delete(ctx, serviceAccount)).To(Succeed()) } } - if cluster.Spec.AuthServiceAccountName != "" { - roleBinding, err := kubernetes.GetRoleBinding(ctx, k8sClient, cluster.Spec.AuthServiceAccountName, cluster.Namespace) - if err == nil { - Expect(k8sClient.Delete(ctx, roleBinding)).To(Succeed()) - } - - role, err := kubernetes.GetRole(ctx, k8sClient, cluster.Spec.AuthServiceAccountName, cluster.Namespace) - if err == nil { - Expect(k8sClient.Delete(ctx, role)).To(Succeed()) - } - - serviceAccount, err := kubernetes.GetServiceAccount(ctx, k8sClient, cluster.Spec.AuthServiceAccountName, cluster.Namespace) - if err == nil { - Expect(k8sClient.Delete(ctx, serviceAccount)).To(Succeed()) - } - } UsingClusterBy(cluster.Name, "Cleaning up any secrets for the cluster") var allSecrets corev1.SecretList @@ -376,34 +354,82 @@ func CreateAndBootstrapCluster(ctx context.Context, k8sClient client.Client, hum } } - if cluster.Spec.AuthServiceAccountName != "" { - if cluster.Spec.AuthServiceAccountName != cluster.Spec.HumioServiceAccountName { - UsingClusterBy(key.Name, "Creating service account for auth container") - authServiceAccount := kubernetes.ConstructServiceAccount(cluster.Spec.AuthServiceAccountName, cluster.Namespace, map[string]string{}, map[string]string{}) - Expect(k8sClient.Create(ctx, authServiceAccount)).To(Succeed()) - } - - UsingClusterBy(key.Name, "Creating role for auth container") - authRole := kubernetes.ConstructAuthRole(cluster.Spec.AuthServiceAccountName, key.Namespace, map[string]string{}) - Expect(k8sClient.Create(ctx, authRole)).To(Succeed()) - - UsingClusterBy(key.Name, "Creating role binding for auth container") - authRoleBinding := kubernetes.ConstructRoleBinding(cluster.Spec.AuthServiceAccountName, authRole.Name, key.Namespace, cluster.Spec.AuthServiceAccountName, map[string]string{}) - Expect(k8sClient.Create(ctx, authRoleBinding)).To(Succeed()) - } - if os.Getenv("TEST_USE_EXISTING_CLUSTER") != "true" { // Simulate sidecar creating the secret which contains the admin token used to authenticate with humio secretData := map[string][]byte{"token": []byte("")} adminTokenSecretName := fmt.Sprintf("%s-%s", key.Name, kubernetes.ServiceTokenSecretNameSuffix) - UsingClusterBy(key.Name, "Simulating the auth container creating the secret containing the API token") + UsingClusterBy(key.Name, "Simulating the admin token secret containing the API token") desiredSecret := kubernetes.ConstructSecret(key.Name, key.Namespace, adminTokenSecretName, secretData, nil) Expect(k8sClient.Create(ctx, desiredSecret)).To(Succeed()) + + UsingClusterBy(key.Name, "Simulating the creation of the HumioBootstrapToken resource") + humioBootstrapToken := kubernetes.ConstructHumioBootstrapToken(key.Name, key.Namespace) + humioBootstrapToken.Spec = humiov1alpha1.HumioBootstrapTokenSpec{ + ManagedClusterName: key.Name, + } + humioBootstrapToken.Status = humiov1alpha1.HumioBootstrapTokenStatus{ + State: humiov1alpha1.HumioBootstrapTokenStateReady, + TokenSecretKeyRef: humiov1alpha1.HumioTokenSecretStatus{SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: fmt.Sprintf("%s-bootstrap-token", key.Name), + }, + Key: "secret", + }, + }, + HashedTokenSecretKeyRef: humiov1alpha1.HumioHashedTokenSecretStatus{SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: fmt.Sprintf("%s-bootstrap-token", key.Name), + }, + Key: "hashedToken", + }}, + } + UsingClusterBy(key.Name, "Creating HumioBootstrapToken resource") + Expect(k8sClient.Create(ctx, humioBootstrapToken)).Should(Succeed()) } + UsingClusterBy(key.Name, "Simulating the humio bootstrap token controller creating the secret containing the API token") + secretData := map[string][]byte{"hashedToken": []byte("P2HS9.20.r+ZbMqd0pHF65h3yQiOt8n1xNytv/4ePWKIj3cElP7gt8YD+gOtdGGvJYmG229kyFWLs6wXx9lfSDiRGGu/xuQ"), "secret": []byte("cYsrKi6IeyOJVzVIdmVK3M6RGl4y9GpgduYKXk4qWvvj")} + bootstrapTokenSecretName := fmt.Sprintf("%s-%s", key.Name, kubernetes.BootstrapTokenSecretNameSuffix) + desiredSecret := kubernetes.ConstructSecret(key.Name, key.Namespace, bootstrapTokenSecretName, secretData, nil) + Expect(k8sClient.Create(ctx, desiredSecret)).To(Succeed()) + UsingClusterBy(key.Name, "Creating HumioCluster resource") Expect(k8sClient.Create(ctx, cluster)).Should(Succeed()) + 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 + } + if len(hbtList) == 0 { + return fmt.Errorf("no humiobootstraptokens for cluster %s", key.Name) + } + if len(hbtList) > 1 { + return fmt.Errorf("too many humiobootstraptokens for cluster %s. found list : %+v", key.Name, hbtList) + } + + updatedHumioBootstrapToken := hbtList[0] + updatedHumioBootstrapToken.Status.State = humiov1alpha1.HumioBootstrapTokenStateReady + updatedHumioBootstrapToken.Status.TokenSecretKeyRef = humiov1alpha1.HumioTokenSecretStatus{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: fmt.Sprintf("%s-bootstrap-token", key.Name), + }, + Key: "secret", + }, + } + updatedHumioBootstrapToken.Status.HashedTokenSecretKeyRef = humiov1alpha1.HumioHashedTokenSecretStatus{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: fmt.Sprintf("%s-bootstrap-token", key.Name), + }, + Key: "hashedToken", + }, + } + return k8sClient.Status().Update(ctx, &updatedHumioBootstrapToken) + }, testTimeout, TestInterval).Should(Succeed()) + if expectedState != humiov1alpha1.HumioClusterStateRunning { return } @@ -492,7 +518,7 @@ func CreateAndBootstrapCluster(ctx context.Context, k8sClient client.Client, hum }, testTimeout, TestInterval).Should(HaveKeyWithValue(revisionKey, "1")) } - UsingClusterBy(key.Name, "Waiting for the auth sidecar to populate the secret containing the API token") + UsingClusterBy(key.Name, "Waiting for the controller to populate the secret containing the admin token") Eventually(func() error { clusterPods, _ = kubernetes.ListPods(ctx, k8sClient, key.Namespace, controllers.NewHumioNodeManagerFromHumioCluster(&updatedHumioCluster).GetCommonClusterLabels()) for idx := range clusterPods { @@ -505,29 +531,16 @@ func CreateAndBootstrapCluster(ctx context.Context, k8sClient client.Client, hum }, &corev1.Secret{}) }, testTimeout, TestInterval).Should(Succeed()) - if os.Getenv("TEST_USE_EXISTING_CLUSTER") == "true" { - UsingClusterBy(key.Name, "Validating API token was obtained using the API method") - var apiTokenSecret corev1.Secret - Eventually(func() error { - return k8sClient.Get(ctx, types.NamespacedName{ - Namespace: key.Namespace, - Name: fmt.Sprintf("%s-%s", key.Name, kubernetes.ServiceTokenSecretNameSuffix), - }, &apiTokenSecret) - }, testTimeout, TestInterval).Should(Succeed()) - Expect(apiTokenSecret.Annotations).Should(HaveKeyWithValue(apiTokenMethodAnnotationName, apiTokenMethodFromAPI)) - } - if os.Getenv("TEST_USE_EXISTING_CLUSTER") == "true" { UsingClusterBy(key.Name, "Validating cluster nodes have ZONE configured correctly") if updatedHumioCluster.Spec.DisableInitContainer { Eventually(func() []string { - clusterConfig, err := helpers.NewCluster(ctx, k8sClient, key.Name, "", key.Namespace, helpers.UseCertManager(), true) + clusterConfig, err := helpers.NewCluster(ctx, k8sClient, key.Name, "", key.Namespace, helpers.UseCertManager(), true, false) Expect(err).To(BeNil()) Expect(clusterConfig).ToNot(BeNil()) Expect(clusterConfig.Config()).ToNot(BeNil()) cluster, err := humioClient.GetClusters(clusterConfig.Config(), reconcile.Request{NamespacedName: key}) - UsingClusterBy(key.Name, fmt.Sprintf("Obtained the following cluster details: %#+v, err: %v", cluster, err)) if err != nil { return []string{fmt.Sprintf("got err: %s", err)} } @@ -548,13 +561,12 @@ func CreateAndBootstrapCluster(ctx context.Context, k8sClient client.Client, hum }, testTimeout, TestInterval).Should(BeEmpty()) } else { Eventually(func() []string { - clusterConfig, err := helpers.NewCluster(ctx, k8sClient, key.Name, "", key.Namespace, helpers.UseCertManager(), true) + clusterConfig, err := helpers.NewCluster(ctx, k8sClient, key.Name, "", key.Namespace, helpers.UseCertManager(), true, false) Expect(err).To(BeNil()) Expect(clusterConfig).ToNot(BeNil()) Expect(clusterConfig.Config()).ToNot(BeNil()) cluster, err := humioClient.GetClusters(clusterConfig.Config(), reconcile.Request{NamespacedName: key}) - UsingClusterBy(key.Name, fmt.Sprintf("Obtained the following cluster details: %#+v, err: %v", cluster, err)) if err != nil || len(cluster.Nodes) < 1 { return []string{} } @@ -626,34 +638,6 @@ func WaitForReconcileToSync(ctx context.Context, key types.NamespacedName, k8sCl }, testTimeout, TestInterval).Should(BeNumerically("==", beforeGeneration)) } -type stdoutErrLine struct { - // We reuse the same names as Ginkgo so when we print out the relevant log lines we have a common field and value to jump from the test result to the relevant log lines by simply searching for the ID shown in the result. - CapturedGinkgoWriterOutput, CapturedStdOutErr string - - // Line contains either the CapturedGinkgoWriterOutput or CapturedStdOutErr we get in the spec/suite report. - Line string - - // LineNumber represents the index of line in the provided slice of lines. This may help to understand what order things were output in case two lines mention the same timestamp. - LineNumber int - - // State includes information about if a given report passed or failed - State ginkgotypes.SpecState -} - -func PrintLinesWithRunID(runID string, lines []string, specState ginkgotypes.SpecState) { - for idx, line := range lines { - output := stdoutErrLine{ - CapturedGinkgoWriterOutput: runID, - CapturedStdOutErr: runID, - Line: line, - LineNumber: idx, - State: specState, - } - u, _ := json.Marshal(output) - fmt.Println(string(u)) - } -} - func useDockerCredentials() bool { return os.Getenv(dockerUsernameEnvVar) != "" && os.Getenv(dockerPasswordEnvVar) != "" && os.Getenv(dockerUsernameEnvVar) != "none" && os.Getenv(dockerPasswordEnvVar) != "none" diff --git a/controllers/suite/resources/suite_test.go b/controllers/suite/resources/suite_test.go index 0c49cb58..2dfbd0fc 100644 --- a/controllers/suite/resources/suite_test.go +++ b/controllers/suite/resources/suite_test.go @@ -261,7 +261,7 @@ var _ = BeforeSuite(func() { cluster.Spec.HumioNodeSpec.Image = "humio/humio-core:1.150.0" suite.CreateAndBootstrapCluster(context.TODO(), k8sClient, humioClient, cluster, true, corev1alpha1.HumioClusterStateRunning, testTimeout) - sharedCluster, err = helpers.NewCluster(context.TODO(), k8sClient, clusterKey.Name, "", clusterKey.Namespace, helpers.UseCertManager(), true) + sharedCluster, err = helpers.NewCluster(context.TODO(), k8sClient, clusterKey.Name, "", clusterKey.Namespace, helpers.UseCertManager(), true, false) Expect(err).To(BeNil()) Expect(sharedCluster).ToNot(BeNil()) Expect(sharedCluster.Config()).ToNot(BeNil()) diff --git a/examples/humiobootstraptoken.yaml b/examples/humiobootstraptoken.yaml new file mode 100644 index 00000000..ef175b3d --- /dev/null +++ b/examples/humiobootstraptoken.yaml @@ -0,0 +1,14 @@ +apiVersion: core.humio.com/v1alpha1 +kind: HumioBootstrapToken +metadata: + name: example-bootstraptoken +spec: + managedClusterName: example-humiocluster + tokenSecret: + secretKeyRef: + name: example-bootstraptoken-token-secret + key: secret + hashedTokenSecret: + secretKeyRef: + name: example-bootstraptoken-token-secret + key: hashedToken diff --git a/examples/humiocluster-kind-local.yaml b/examples/humiocluster-kind-local.yaml index 9938d68d..9ad0801c 100644 --- a/examples/humiocluster-kind-local.yaml +++ b/examples/humiocluster-kind-local.yaml @@ -33,4 +33,6 @@ spec: - name: "ZOOKEEPER_URL" value: "humio-cp-zookeeper-0.humio-cp-zookeeper-headless.default:2181" - name: "KAFKA_SERVERS" - value: "humio-cp-kafka-0.humio-cp-kafka-headless.default:9092" \ No newline at end of file + value: "humio-cp-kafka-0.humio-cp-kafka-headless.default:9092" + - name: "AUTHENTICATION_METHOD" + value: "static" diff --git a/examples/humiocluster-multi-nodepool-kind-local.yaml b/examples/humiocluster-multi-nodepool-kind-local.yaml index 1d079ded..dd4b2cee 100644 --- a/examples/humiocluster-multi-nodepool-kind-local.yaml +++ b/examples/humiocluster-multi-nodepool-kind-local.yaml @@ -1,11 +1,13 @@ apiVersion: core.humio.com/v1alpha1 kind: HumioCluster metadata: - name: example-humiocluster-3 + name: example-humiocluster spec: + #disableInitContainer: true nodePools: - name: ingest-only spec: + #disableInitContainer: true image: "humio/humio-core:1.82.1" nodeCount: 1 dataVolumePersistentVolumeClaimSpecTemplate: @@ -58,4 +60,8 @@ spec: - name: "ZOOKEEPER_URL" value: "humio-cp-zookeeper-0.humio-cp-zookeeper-headless.default:2181" - name: "KAFKA_SERVERS" - value: "humio-cp-kafka-0.humio-cp-kafka-headless.default:9092" \ No newline at end of file + value: "humio-cp-kafka-0.humio-cp-kafka-headless.default:9092" + - name: "STATIC_USERS" + value: "user:user" + - name: "AUTHENTICATION_METHOD" + value: "static" \ No newline at end of file diff --git a/go.mod b/go.mod index b833a082..5c12e1a5 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,11 @@ require ( github.com/cert-manager/cert-manager v1.12.12 github.com/cli/shurcooL-graphql v0.0.4 github.com/go-jose/go-jose/v4 v4.0.1 - github.com/go-logr/logr v1.4.2 + github.com/go-logr/logr v1.4.1 github.com/go-logr/zapr v1.3.0 github.com/google/go-cmp v0.6.0 github.com/humio/cli v0.36.1-0.20240814103929-aacdf44666ce - github.com/onsi/ginkgo/v2 v2.20.0 + github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.34.1 github.com/prometheus/client_golang v1.19.0 go.uber.org/zap v1.27.0 @@ -38,31 +38,34 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect + github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/moby/spdystream v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.52.3 // indirect github.com/prometheus/procfs v0.13.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.26.0 // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.19.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.24.0 // indirect + golang.org/x/tools v0.23.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 78948ccb..eec9e07a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cert-manager/cert-manager v1.12.12 h1:upG8EhS1bLdX1VlZkmKD2QBjld/aXtjVKvTsZkbWEQ4= @@ -22,8 +24,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -48,10 +50,13 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/humio/cli v0.36.1-0.20240814103929-aacdf44666ce h1:WRVLad++Yerg08UcQCzAXY9UwV0P7U1lkOvrdMYUjVY= github.com/humio/cli v0.36.1-0.20240814103929-aacdf44666ce/go.mod h1:Du1GCeQ65rVrUQX/ge45RFflX+I3ZLU3sdCM8kHpuq8= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= @@ -68,6 +73,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -75,8 +82,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw= -github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -110,8 +119,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -120,34 +129,34 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index f4223af8..f309e0f4 100644 --- a/main.go +++ b/main.go @@ -184,6 +184,12 @@ func main() { BaseLogger: log, }).SetupWithManager(mgr); err != nil { ctrl.Log.Error(err, "unable to create controller", "controller", "HumioFilterAlert") + } + if err = (&controllers.HumioBootstrapTokenReconciler{ + Client: mgr.GetClient(), + BaseLogger: log, + }).SetupWithManager(mgr); err != nil { + ctrl.Log.Error(err, "unable to create controller", "controller", "HumioBootstrapToken") os.Exit(1) } if err = (&controllers.HumioAggregateAlertReconciler{ diff --git a/pkg/helpers/clusterinterface.go b/pkg/helpers/clusterinterface.go index 83b4a0db..fa02cdb9 100644 --- a/pkg/helpers/clusterinterface.go +++ b/pkg/helpers/clusterinterface.go @@ -35,7 +35,7 @@ type ClusterInterface interface { Url(context.Context, client.Client) (*url.URL, error) Name() string Config() *humioapi.Config - constructHumioConfig(context.Context, client.Client, bool) (*humioapi.Config, error) + constructHumioConfig(context.Context, client.Client, bool, bool) (*humioapi.Config, error) } type Cluster struct { @@ -44,10 +44,11 @@ type Cluster struct { namespace string certManagerEnabled bool withAPIToken bool + withBootstrapToken bool humioConfig *humioapi.Config } -func NewCluster(ctx context.Context, k8sClient client.Client, managedClusterName, externalClusterName, namespace string, certManagerEnabled bool, withAPIToken bool) (ClusterInterface, error) { +func NewCluster(ctx context.Context, k8sClient client.Client, managedClusterName, externalClusterName, namespace string, certManagerEnabled bool, withAPIToken bool, withBootstrapToken bool) (ClusterInterface, error) { // Return error immediately if we do not have exactly one of the cluster names configured if managedClusterName != "" && externalClusterName != "" { return nil, fmt.Errorf("cannot have both ManagedClusterName and ExternalClusterName set at the same time") @@ -64,9 +65,10 @@ func NewCluster(ctx context.Context, k8sClient client.Client, managedClusterName namespace: namespace, certManagerEnabled: certManagerEnabled, withAPIToken: withAPIToken, + withBootstrapToken: withBootstrapToken, } - humioConfig, err := cluster.constructHumioConfig(ctx, k8sClient, withAPIToken) + humioConfig, err := cluster.constructHumioConfig(ctx, k8sClient, withAPIToken, withBootstrapToken) if err != nil { return nil, err } @@ -129,7 +131,7 @@ func (c Cluster) Config() *humioapi.Config { } // constructHumioConfig returns a config to use with Humio API client with the necessary CA and API token. -func (c Cluster) constructHumioConfig(ctx context.Context, k8sClient client.Client, withAPIToken bool) (*humioapi.Config, error) { +func (c Cluster) constructHumioConfig(ctx context.Context, k8sClient client.Client, withAPIToken bool, withBootstrapToken bool) (*humioapi.Config, error) { if c.managedClusterName != "" { // Lookup ManagedHumioCluster resource to figure out if we expect to use TLS or not var humioManagedCluster humiov1alpha1.HumioCluster @@ -159,11 +161,48 @@ func (c Cluster) constructHumioConfig(ctx context.Context, k8sClient client.Clie Name: fmt.Sprintf("%s-%s", c.managedClusterName, kubernetes.ServiceTokenSecretNameSuffix), }, &apiToken) if err != nil { - return nil, fmt.Errorf("unable to get secret containing api token: %w", err) + return nil, fmt.Errorf("unable to get admin secret containing api token: %w", err) } config.Token = string(apiToken.Data["token"]) } + var bootstrapToken corev1.Secret + if withBootstrapToken { + hbtList := &humiov1alpha1.HumioBootstrapTokenList{} + var hasMatch bool + var matchedHbt humiov1alpha1.HumioBootstrapToken + err := k8sClient.List(ctx, hbtList) + if err != nil { + return nil, fmt.Errorf("unable to get bootstrap token: %w", err) + } + for _, hbt := range hbtList.Items { + if hbt.Spec.ManagedClusterName == c.managedClusterName { + hasMatch = true + matchedHbt = hbt + } + } + + if !hasMatch { + return nil, fmt.Errorf("unable to find bootstrap token with ManagedClusterName %s", c.managedClusterName) + } + + // Get API token + if matchedHbt.Status.TokenSecretKeyRef.SecretKeyRef != nil { + err = k8sClient.Get(ctx, types.NamespacedName{ + Namespace: c.namespace, + Name: matchedHbt.Status.TokenSecretKeyRef.SecretKeyRef.Name, + }, &bootstrapToken) + if err != nil { + return nil, fmt.Errorf("unable to get bootstrap secret containing api token: %w", err) + } + if _, ok := bootstrapToken.Data[matchedHbt.Status.TokenSecretKeyRef.SecretKeyRef.Key]; !ok { + return nil, fmt.Errorf("unable to get bootstrap secret containing api token. secret does not contain key named \"%s\"", matchedHbt.Status.TokenSecretKeyRef.SecretKeyRef.Key) + } + config.Token = fmt.Sprintf("localroot~%s", string(bootstrapToken.Data[matchedHbt.Status.TokenSecretKeyRef.SecretKeyRef.Key])) + } + + } + // If we do not use TLS, return a client without CA certificate if !c.certManagerEnabled || !TLSEnabled(&humioManagedCluster) { config.Insecure = true diff --git a/pkg/helpers/clusterinterface_test.go b/pkg/helpers/clusterinterface_test.go index 45dd5fbe..2cab38f4 100644 --- a/pkg/helpers/clusterinterface_test.go +++ b/pkg/helpers/clusterinterface_test.go @@ -152,8 +152,18 @@ func TestCluster_HumioConfig_managedHumioCluster(t *testing.T) { Name: fmt.Sprintf("%s-admin-token", tt.managedHumioCluster.Name), Namespace: tt.managedHumioCluster.Namespace, }, - StringData: map[string]string{ - "token": "secret-api-token", + Data: map[string][]byte{ + "token": []byte("secret-api-token"), + }, + } + bootstrapTokenSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-bootstrap-token", tt.managedHumioCluster.Name), + Namespace: tt.managedHumioCluster.Namespace, + }, + Data: map[string][]byte{ + "hashedToken": []byte("hashed-token"), + "secret": []byte("secret-api-token"), }, } caCertificateSecret := corev1.Secret{ @@ -168,6 +178,7 @@ func TestCluster_HumioConfig_managedHumioCluster(t *testing.T) { objs := []runtime.Object{ &tt.managedHumioCluster, &apiTokenSecret, + &bootstrapTokenSecret, &caCertificateSecret, } // Register operator types with the runtime scheme. @@ -176,7 +187,7 @@ func TestCluster_HumioConfig_managedHumioCluster(t *testing.T) { cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - cluster, err := NewCluster(context.Background(), cl, tt.managedHumioCluster.Name, "", tt.managedHumioCluster.Namespace, tt.certManagerEnabled, true) + cluster, err := NewCluster(context.Background(), cl, tt.managedHumioCluster.Name, "", tt.managedHumioCluster.Namespace, tt.certManagerEnabled, true, false) if err != nil || cluster.Config() == nil { t.Errorf("unable to obtain humio client config: %s", err) } @@ -338,7 +349,7 @@ func TestCluster_HumioConfig_externalHumioCluster(t *testing.T) { t.Run(tt.name, func(t *testing.T) { apiTokenSecretName := tt.externalHumioCluster.Spec.APITokenSecretName if apiTokenSecretName == "" { - apiTokenSecretName = fmt.Sprintf("%s-unspecified-api-token", tt.externalHumioCluster.Name) + apiTokenSecretName = fmt.Sprintf("%s-unspecified-admin-token", tt.externalHumioCluster.Name) } apiTokenSecret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -373,7 +384,7 @@ func TestCluster_HumioConfig_externalHumioCluster(t *testing.T) { cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - cluster, err := NewCluster(context.Background(), cl, "", tt.externalHumioCluster.Name, tt.externalHumioCluster.Namespace, false, true) + cluster, err := NewCluster(context.Background(), cl, "", tt.externalHumioCluster.Name, tt.externalHumioCluster.Namespace, false, true, false) if tt.expectedConfigFailure && (err == nil) { t.Errorf("unable to get a valid config: %s", err) } @@ -483,8 +494,9 @@ func TestCluster_NewCluster(t *testing.T) { Name: "managed-admin-token", Namespace: "default", }, - StringData: map[string]string{ - "token": "secret-api-token", + Data: map[string][]byte{ + "hashedToken": []byte("secret-api-token"), + "secret": []byte("secret-api-token"), }, } @@ -500,7 +512,7 @@ func TestCluster_NewCluster(t *testing.T) { cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - _, err := NewCluster(context.Background(), cl, tt.managedClusterName, tt.externalClusterName, tt.namespace, false, true) + _, err := NewCluster(context.Background(), cl, tt.managedClusterName, tt.externalClusterName, tt.namespace, false, true, false) if tt.expectError == (err == nil) { t.Fatalf("expectError: %+v but got=%+v", tt.expectError, err) } diff --git a/pkg/humio/client.go b/pkg/humio/client.go index 77a81013..738ff504 100644 --- a/pkg/humio/client.go +++ b/pkg/humio/client.go @@ -48,6 +48,7 @@ type Client interface { FilterAlertsClient AggregateAlertsClient ScheduledSearchClient + UsersClient } type ClusterClient interface { @@ -131,6 +132,14 @@ type LicenseClient interface { InstallLicense(*humioapi.Config, reconcile.Request, string) error } +type UsersClient interface { + AddUser(*humioapi.Config, reconcile.Request, string, bool) (*humioapi.User, error) + ListAllHumioUsersSingleOrg(*humioapi.Config, reconcile.Request) ([]user, error) + ListAllHumioUsersMultiOrg(*humioapi.Config, reconcile.Request, string, string) ([]OrganizationSearchResultEntry, error) + ExtractExistingHumioAdminUserID(*humioapi.Config, reconcile.Request, string, string, string) (string, error) + RotateUserApiTokenAndGet(*humioapi.Config, reconcile.Request, string) (string, error) +} + // ClientConfig stores our Humio api client type ClientConfig struct { humioClients map[humioClientKey]*humioClientConnection @@ -898,3 +907,100 @@ func (h *ClientConfig) ValidateActionsForAggregateAlert(config *humioapi.Config, } return nil } + +type user struct { + Id string + Username string +} + +type OrganizationSearchResultEntry struct { + EntityId string `graphql:"entityId"` + SearchMatch string `graphql:"searchMatch"` + OrganizationName string `graphql:"organizationName"` +} + +type OrganizationSearchResultSet struct { + Results []OrganizationSearchResultEntry `graphql:"results"` +} + +func (h *ClientConfig) ListAllHumioUsersSingleOrg(config *humioapi.Config, req reconcile.Request) ([]user, error) { + var q struct { + Users []user `graphql:"users"` + } + err := h.GetHumioClient(config, req).Query(&q, nil) + return q.Users, err +} + +func (h *ClientConfig) ListAllHumioUsersMultiOrg(config *humioapi.Config, req reconcile.Request, username string, organization string) ([]OrganizationSearchResultEntry, error) { + var q struct { + OrganizationSearchResultSet `graphql:"searchOrganizations(searchFilter: $username, typeFilter: User, sortBy: Name, orderBy: ASC, limit: 1000000, skip: 0)"` + } + + variables := map[string]interface{}{ + "username": graphql.String(username), + } + + err := h.GetHumioClient(config, req).Query(&q, variables) + if err != nil { + return []OrganizationSearchResultEntry{}, err + } + + var allUserResultEntries []OrganizationSearchResultEntry + for _, result := range q.OrganizationSearchResultSet.Results { + if result.OrganizationName == organization { + allUserResultEntries = append(allUserResultEntries, result) + } + } + + return allUserResultEntries, nil +} + +func (h *ClientConfig) ExtractExistingHumioAdminUserID(config *humioapi.Config, req reconcile.Request, organizationMode string, username string, organization string) (string, error) { + if organizationMode == "multi" || organizationMode == "multiv2" { + var allUserResults []OrganizationSearchResultEntry + allUserResults, err := h.ListAllHumioUsersMultiOrg(config, req, username, organization) + if err != nil { + // unable to list all users + return "", err + } + for _, userResult := range allUserResults { + if userResult.OrganizationName == organization { + if userResult.SearchMatch == fmt.Sprintf(" | %s () ()", username) { + fmt.Printf("Found user ID using multi-organization query.\n") + return userResult.EntityId, nil + } + } + } + } + + allUsers, err := h.ListAllHumioUsersSingleOrg(config, req) + if err != nil { + // unable to list all users + return "", err + } + for _, user := range allUsers { + if user.Username == username { + return user.Id, nil + } + } + + return "", nil +} + +func (h *ClientConfig) RotateUserApiTokenAndGet(config *humioapi.Config, req reconcile.Request, userID string) (string, error) { + token, err := h.GetHumioClient(config, req).Users().RotateToken(userID) + if err != nil { + return "", fmt.Errorf("could not rotate apiToken for userID %s, err: %w", userID, err) + } + return token, nil +} + +func (h *ClientConfig) AddUser(config *humioapi.Config, req reconcile.Request, username string, isRoot bool) (*humioapi.User, error) { + user, err := h.GetHumioClient(config, req).Users().Add(username, humioapi.UserChangeSet{ + IsRoot: &isRoot, + }) + if err != nil { + return &humioapi.User{}, err + } + return &user, nil +} diff --git a/pkg/humio/client_mock.go b/pkg/humio/client_mock.go index 68f413e4..9cc0d629 100644 --- a/pkg/humio/client_mock.go +++ b/pkg/humio/client_mock.go @@ -20,10 +20,11 @@ import ( "crypto/sha512" "encoding/hex" "fmt" - "github.com/humio/humio-operator/pkg/helpers" "net/url" "sync" + "github.com/humio/humio-operator/pkg/helpers" + humioapi "github.com/humio/cli/api" humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1" "github.com/humio/humio-operator/pkg/kubernetes" @@ -38,20 +39,16 @@ var ( type resourceKey struct { // clusterName holds the value of the cluster clusterName string - // searchDomainName is the name of the repository or view searchDomainName string - // resourceName is the name of resource, like IngestToken, Parser, etc. resourceName string } type ClientMock struct { - OnPremLicense map[resourceKey]humioapi.OnPremLicense - - Repository map[resourceKey]humioapi.Repository - View map[resourceKey]humioapi.View - + OnPremLicense map[resourceKey]humioapi.OnPremLicense + Repository map[resourceKey]humioapi.Repository + View map[resourceKey]humioapi.View IngestToken map[resourceKey]humioapi.IngestToken Parser map[resourceKey]humioapi.Parser Action map[resourceKey]humioapi.Action @@ -59,6 +56,7 @@ type ClientMock struct { FilterAlert map[resourceKey]humioapi.FilterAlert AggregateAlert map[resourceKey]humioapi.AggregateAlert ScheduledSearch map[resourceKey]humioapi.ScheduledSearch + User humioapi.User } type MockClientConfig struct { @@ -68,11 +66,9 @@ type MockClientConfig struct { func NewMockClient() *MockClientConfig { mockClientConfig := &MockClientConfig{ apiClient: &ClientMock{ - OnPremLicense: make(map[resourceKey]humioapi.OnPremLicense), - - Repository: make(map[resourceKey]humioapi.Repository), - View: make(map[resourceKey]humioapi.View), - + OnPremLicense: make(map[resourceKey]humioapi.OnPremLicense), + Repository: make(map[resourceKey]humioapi.Repository), + View: make(map[resourceKey]humioapi.View), IngestToken: make(map[resourceKey]humioapi.IngestToken), Parser: make(map[resourceKey]humioapi.Parser), Action: make(map[resourceKey]humioapi.Action), @@ -80,6 +76,7 @@ func NewMockClient() *MockClientConfig { FilterAlert: make(map[resourceKey]humioapi.FilterAlert), AggregateAlert: make(map[resourceKey]humioapi.AggregateAlert), ScheduledSearch: make(map[resourceKey]humioapi.ScheduledSearch), + User: humioapi.User{}, }, } @@ -937,7 +934,6 @@ func (h *MockClientConfig) ClearHumioClientConnections(repoNameToKeep string) { } } h.apiClient.View = make(map[resourceKey]humioapi.View) - h.apiClient.IngestToken = make(map[resourceKey]humioapi.IngestToken) h.apiClient.Parser = make(map[resourceKey]humioapi.Parser) h.apiClient.Action = make(map[resourceKey]humioapi.Action) @@ -965,3 +961,28 @@ func (h *MockClientConfig) searchDomainNameExists(clusterName, searchDomainName return false } + +func (h *MockClientConfig) ListAllHumioUsersSingleOrg(config *humioapi.Config, req reconcile.Request) ([]user, error) { + return []user{}, nil +} + +func (h *MockClientConfig) ListAllHumioUsersMultiOrg(config *humioapi.Config, req reconcile.Request, username string, organization string) ([]OrganizationSearchResultEntry, error) { + return []OrganizationSearchResultEntry{}, nil +} + +func (h *MockClientConfig) ExtractExistingHumioAdminUserID(config *humioapi.Config, req reconcile.Request, organizationMode string, username string, organization string) (string, error) { + return "", nil +} + +func (h *MockClientConfig) RotateUserApiTokenAndGet(config *humioapi.Config, req reconcile.Request, userID string) (string, error) { + return "", nil +} + +func (h *MockClientConfig) AddUser(config *humioapi.Config, req reconcile.Request, username string, isRoot bool) (*humioapi.User, error) { + h.apiClient.User = humioapi.User{ + ID: "id", + Username: username, + IsRoot: isRoot, + } + return &h.apiClient.User, nil +} diff --git a/pkg/kubernetes/humio_bootstrap_tokens.go b/pkg/kubernetes/humio_bootstrap_tokens.go new file mode 100644 index 00000000..d500f795 --- /dev/null +++ b/pkg/kubernetes/humio_bootstrap_tokens.go @@ -0,0 +1,79 @@ +package kubernetes + +import ( + "context" + + humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +/* +Copyright 2020 Humio https://humio.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const ( + BootstrapTokenSecretNameSuffix = "bootstrap-token" + BootstrapTokenManagedClusterNameLabelName = "managed-cluster-name" +) + +// LabelsForHumioBootstrapToken returns a map of labels which contains a common set of labels and additional user-defined humio bootstrap token labels. +// In case of overlap between the common labels and user-defined labels, the user-defined label will be ignored. +func LabelsForHumioBootstrapToken(clusterName string) map[string]string { + labels := LabelsForHumio(clusterName) + labels[BootstrapTokenManagedClusterNameLabelName] = clusterName + return labels +} + +// ConstructHumioBootstrapToken returns a HumioBootstrapToken +func ConstructHumioBootstrapToken(humioClusterName string, humioClusterNamespace string) *humiov1alpha1.HumioBootstrapToken { + return &humiov1alpha1.HumioBootstrapToken{ + ObjectMeta: metav1.ObjectMeta{ + Name: humioClusterName, + Namespace: humioClusterNamespace, + Labels: LabelsForHumioBootstrapToken(humioClusterName), + }, + Spec: humiov1alpha1.HumioBootstrapTokenSpec{ + ManagedClusterName: humioClusterName, + }, + } +} + +// ListHumioBootstrapTokens returns all HumioBootstrapTokens in a given namespace which matches the label selector +func ListHumioBootstrapTokens(ctx context.Context, c client.Client, humioClusterNamespace string, matchingLabels client.MatchingLabels) ([]humiov1alpha1.HumioBootstrapToken, error) { + var foundHumioBootstrapTokenList humiov1alpha1.HumioBootstrapTokenList + err := c.List(ctx, &foundHumioBootstrapTokenList, client.InNamespace(humioClusterNamespace), matchingLabels) + if err != nil { + return nil, err + } + + // If for some reason the HumioBootstrapToken is not labeled with the managed-cluster-name label, look at the spec + if len(foundHumioBootstrapTokenList.Items) == 0 { + if humioClusterName, ok := matchingLabels[BootstrapTokenManagedClusterNameLabelName]; ok { + var allHumioBootstrapTokensList humiov1alpha1.HumioBootstrapTokenList + err := c.List(ctx, &allHumioBootstrapTokensList, client.InNamespace(humioClusterNamespace)) + if err != nil { + return nil, err + } + for _, hbt := range allHumioBootstrapTokensList.Items { + if hbt.Spec.ManagedClusterName == humioClusterName { + foundHumioBootstrapTokenList.Items = append(foundHumioBootstrapTokenList.Items, hbt) + } + } + } + } + + return foundHumioBootstrapTokenList.Items, nil +} diff --git a/pkg/kubernetes/roles.go b/pkg/kubernetes/roles.go deleted file mode 100644 index 84522a24..00000000 --- a/pkg/kubernetes/roles.go +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2020 Humio https://humio.com - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package kubernetes - -import ( - "context" - - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// ConstructAuthRole returns the role used by the auth sidecar container to make an API token available for the -// humio-operator. This API token can be used to obtain insights into the health of the Humio cluster and make changes. -func ConstructAuthRole(roleName string, humioClusterNamespace string, labels map[string]string) *rbacv1.Role { - return &rbacv1.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: roleName, - Namespace: humioClusterNamespace, - Labels: labels, - }, - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets"}, - Verbs: []string{"get", "list", "watch", "create", "update", "delete"}, - }, - }, - } -} - -// GetRole returns the given role if it exists -func GetRole(ctx context.Context, c client.Client, roleName, roleNamespace string) (*rbacv1.Role, error) { - var existingRole rbacv1.Role - err := c.Get(ctx, types.NamespacedName{ - Name: roleName, - Namespace: roleNamespace, - }, &existingRole) - return &existingRole, err -}