From e917e9383cb6abbbdce26ca2876b098b3bde4105 Mon Sep 17 00:00:00 2001 From: Grant Schofield Date: Fri, 30 Jun 2023 10:37:01 -0700 Subject: [PATCH] Adds HumioUsers CRD --- api/v1alpha1/humiouser_types.go | 95 +++++++ api/v1alpha1/zz_generated.deepcopy.go | 89 +++++++ .../crds/core.humio.com_humiousers.yaml | 105 ++++++++ .../templates/operator-rbac.yaml | 6 + .../crd/bases/core.humio.com_humiousers.yaml | 105 ++++++++ config/crd/kustomization.yaml | 5 +- .../patches/cainjection_in_humiousers.yaml | 8 + config/crd/patches/webhook_in_humiousers.yaml | 17 ++ config/manager/kustomization.yaml | 6 + config/rbac/humiouser_editor_role.yaml | 24 ++ config/rbac/humiouser_viewer_role.yaml | 20 ++ config/rbac/role.yaml | 26 ++ config/samples/core_v1alpha1_humiouser.yaml | 17 ++ controllers/humiouser_controller.go | 234 ++++++++++++++++++ controllers/suite/clusters/suite_test.go | 11 + .../humioresources_controller_test.go | 66 +++++ controllers/suite/resources/suite_test.go | 9 + go.sum | 1 + main.go | 9 + pkg/humio/client.go | 77 ++++++ pkg/humio/client_mock.go | 29 +++ 21 files changed, 958 insertions(+), 1 deletion(-) create mode 100644 api/v1alpha1/humiouser_types.go create mode 100644 charts/humio-operator/crds/core.humio.com_humiousers.yaml create mode 100644 config/crd/bases/core.humio.com_humiousers.yaml create mode 100644 config/crd/patches/cainjection_in_humiousers.yaml create mode 100644 config/crd/patches/webhook_in_humiousers.yaml create mode 100644 config/rbac/humiouser_editor_role.yaml create mode 100644 config/rbac/humiouser_viewer_role.yaml create mode 100644 config/samples/core_v1alpha1_humiouser.yaml create mode 100644 controllers/humiouser_controller.go diff --git a/api/v1alpha1/humiouser_types.go b/api/v1alpha1/humiouser_types.go new file mode 100644 index 000000000..0a14ec20d --- /dev/null +++ b/api/v1alpha1/humiouser_types.go @@ -0,0 +1,95 @@ +/* +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 ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // HumioUserStateUnknown is the Unknown state of the user + HumioUserStateUnknown = "Unknown" + // HumioUserStateExists is the Exists state of the user + HumioUserStateExists = "Exists" + // HumioUserStateNotFound is the NotFound state of the user + HumioUserStateNotFound = "NotFound" + // HumioUserStateConfigError is the state of the user when user-provided specification results in configuration error, such as non-existent humio cluster + HumioUserStateConfigError = "ConfigError" +) + +// HumioUserSpec defines the desired state of HumioUser +type HumioUserSpec struct { + // ManagedClusterName refers to an object of type HumioCluster that is managed by the operator where the Humio + // resources should be created. + // This conflicts with ExternalClusterName. + ManagedClusterName string `json:"managedClusterName,omitempty"` + // ExternalClusterName refers to an object of type HumioExternalCluster where the Humio resources should be created. + // This conflicts with ManagedClusterName. + ExternalClusterName string `json:"externalClusterName,omitempty"` + // Username of the user in humio + Username string `json:"username,omitempty"` + // User ID of the user in humio + ID string `json:"id,omitempty"` + // FullName is the full name of the user + FullName string `json:"fullName,omitempty"` + // Email is the email of the user + Email string `json:"email,omitempty"` + // Company is the compnay of the user + Company string `json:"company,omitempty"` + // CountryCode is the compnay of the user + CountryCode string `json:"countryCode,omitempty"` + // Picture is the url to the user's profile picture + Picture string `json:"picture,omitempty"` + // IsRoot is the root setting for the user + IsRoot bool `json:"isRoot,omitempty"` + // CreatedAt is date when the user was created + CreatedAt string `json:"createdAt,omitempty"` +} + +// HumioUserStatus defines the observed state of HumioUser +type HumioUserStatus struct { + // State reflects the current state of the HumioUser + State string `json:"state,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:path=humiousers,scope=Namespaced +//+kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="The state of the user" +//+operator-sdk:gen-csv:customresourcedefinitions.displayName="Humio User" + +// HumioUser is the Schema for the humiousers API +type HumioUser struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec HumioUserSpec `json:"spec,omitempty"` + Status HumioUserStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// HumioUserList contains a list of HumioUser +type HumioUserList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []HumioUser `json:"items"` +} + +func init() { + SchemeBuilder.Register(&HumioUser{}, &HumioUserList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 44fa552cf..d202451be 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1359,6 +1359,95 @@ func (in *HumioUpdateStrategy) DeepCopy() *HumioUpdateStrategy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HumioUser) DeepCopyInto(out *HumioUser) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioUser. +func (in *HumioUser) DeepCopy() *HumioUser { + if in == nil { + return nil + } + out := new(HumioUser) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HumioUser) 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 *HumioUserList) DeepCopyInto(out *HumioUserList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HumioUser, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioUserList. +func (in *HumioUserList) DeepCopy() *HumioUserList { + if in == nil { + return nil + } + out := new(HumioUserList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HumioUserList) 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 *HumioUserSpec) DeepCopyInto(out *HumioUserSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioUserSpec. +func (in *HumioUserSpec) DeepCopy() *HumioUserSpec { + if in == nil { + return nil + } + out := new(HumioUserSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HumioUserStatus) DeepCopyInto(out *HumioUserStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioUserStatus. +func (in *HumioUserStatus) DeepCopy() *HumioUserStatus { + if in == nil { + return nil + } + out := new(HumioUserStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HumioView) DeepCopyInto(out *HumioView) { *out = *in diff --git a/charts/humio-operator/crds/core.humio.com_humiousers.yaml b/charts/humio-operator/crds/core.humio.com_humiousers.yaml new file mode 100644 index 000000000..165e7cb0c --- /dev/null +++ b/charts/humio-operator/crds/core.humio.com_humiousers.yaml @@ -0,0 +1,105 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.2 + creationTimestamp: null + name: humiousers.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.19.0' +spec: + group: core.humio.com + names: + kind: HumioUser + listKind: HumioUserList + plural: humiousers + singular: humiouser + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The state of the user + jsonPath: .status.state + name: State + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: HumioUser is the Schema for the humiousers API + 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: HumioUserSpec defines the desired state of HumioUser + properties: + company: + description: Company is the compnay of the user + type: string + countryCode: + description: CountryCode is the compnay of the user + type: string + createdAt: + description: CreatedAt is date when the user was created + type: string + email: + description: Email is the email of the user + type: string + externalClusterName: + description: ExternalClusterName refers to an object of type HumioExternalCluster + where the Humio resources should be created. This conflicts with + ManagedClusterName. + type: string + fullName: + description: FullName is the full name of the user + type: string + id: + description: User ID of the user in humio + type: string + isRoot: + description: IsRoot is the root setting for the user + type: boolean + managedClusterName: + description: ManagedClusterName refers to an object of type HumioCluster + that is managed by the operator where the Humio resources should + be created. This conflicts with ExternalClusterName. + type: string + picture: + description: Picture is the url to the user's profile picture + type: string + username: + description: Username of the user in humio + type: string + type: object + status: + description: HumioUserStatus defines the observed state of HumioUser + properties: + state: + description: State reflects the current state of the HumioUser + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/charts/humio-operator/templates/operator-rbac.yaml b/charts/humio-operator/templates/operator-rbac.yaml index 6fe8760c2..661c42811 100644 --- a/charts/humio-operator/templates/operator-rbac.yaml +++ b/charts/humio-operator/templates/operator-rbac.yaml @@ -76,6 +76,9 @@ rules: - humiorepositories - humiorepositories/finalizers - humiorepositories/status + - humiousers + - humiousers/finalizers + - humiousers/status - humioviews - humioviews/finalizers - humioviews/status @@ -225,6 +228,9 @@ rules: - humiorepositories - humiorepositories/finalizers - humiorepositories/status + - humiousers + - humiousers/finalizers + - humiousers/status - humioviews - humioviews/finalizers - humioviews/status diff --git a/config/crd/bases/core.humio.com_humiousers.yaml b/config/crd/bases/core.humio.com_humiousers.yaml new file mode 100644 index 000000000..165e7cb0c --- /dev/null +++ b/config/crd/bases/core.humio.com_humiousers.yaml @@ -0,0 +1,105 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.2 + creationTimestamp: null + name: humiousers.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.19.0' +spec: + group: core.humio.com + names: + kind: HumioUser + listKind: HumioUserList + plural: humiousers + singular: humiouser + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The state of the user + jsonPath: .status.state + name: State + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: HumioUser is the Schema for the humiousers API + 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: HumioUserSpec defines the desired state of HumioUser + properties: + company: + description: Company is the compnay of the user + type: string + countryCode: + description: CountryCode is the compnay of the user + type: string + createdAt: + description: CreatedAt is date when the user was created + type: string + email: + description: Email is the email of the user + type: string + externalClusterName: + description: ExternalClusterName refers to an object of type HumioExternalCluster + where the Humio resources should be created. This conflicts with + ManagedClusterName. + type: string + fullName: + description: FullName is the full name of the user + type: string + id: + description: User ID of the user in humio + type: string + isRoot: + description: IsRoot is the root setting for the user + type: boolean + managedClusterName: + description: ManagedClusterName refers to an object of type HumioCluster + that is managed by the operator where the Humio resources should + be created. This conflicts with ExternalClusterName. + type: string + picture: + description: Picture is the url to the user's profile picture + type: string + username: + description: Username of the user in humio + type: string + type: object + status: + description: HumioUserStatus defines the observed state of HumioUser + properties: + state: + description: State reflects the current state of the HumioUser + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index d8e7ded66..a7d653041 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -10,9 +10,10 @@ resources: - bases/core.humio.com_humioviews.yaml - bases/core.humio.com_humioactions.yaml - bases/core.humio.com_humioalerts.yaml +- bases/core.humio.com_humiousers.yaml # +kubebuilder:scaffold:crdkustomizeresource -patchesStrategicMerge: +# patchesStrategicMerge: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- patches/webhook_in_humioexternalclusters.yaml @@ -23,6 +24,7 @@ patchesStrategicMerge: #- patches/webhook_in_humioviews.yaml #- patches/webhook_in_humioactions.yaml #- patches/webhook_in_humioalerts.yaml +#- patches/webhook_in_humiousers.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. @@ -35,6 +37,7 @@ patchesStrategicMerge: #- patches/cainjection_in_humioviews.yaml #- patches/cainjection_in_humioactions.yaml #- patches/cainjection_in_humioalerts.yaml +#- patches/cainjection_in_humiousers.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_humiousers.yaml b/config/crd/patches/cainjection_in_humiousers.yaml new file mode 100644 index 000000000..97fdf7c01 --- /dev/null +++ b/config/crd/patches/cainjection_in_humiousers.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: humiousers.core.humio.com diff --git a/config/crd/patches/webhook_in_humiousers.yaml b/config/crd/patches/webhook_in_humiousers.yaml new file mode 100644 index 000000000..a74aa57eb --- /dev/null +++ b/config/crd/patches/webhook_in_humiousers.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: humiousers.core.humio.com +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 5c5f0b84c..96532c80b 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/rbac/humiouser_editor_role.yaml b/config/rbac/humiouser_editor_role.yaml new file mode 100644 index 000000000..571e79a37 --- /dev/null +++ b/config/rbac/humiouser_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit humiousers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: humiouser-editor-role +rules: +- apiGroups: + - core.humio.com + resources: + - humiousers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.humio.com + resources: + - humiousers/status + verbs: + - get diff --git a/config/rbac/humiouser_viewer_role.yaml b/config/rbac/humiouser_viewer_role.yaml new file mode 100644 index 000000000..2442b0085 --- /dev/null +++ b/config/rbac/humiouser_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view humiousers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: humiouser-viewer-role +rules: +- apiGroups: + - core.humio.com + resources: + - humiousers + verbs: + - get + - list + - watch +- apiGroups: + - core.humio.com + resources: + - humiousers/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 9fcade670..a5769a129 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -296,6 +296,32 @@ rules: - get - patch - update +- apiGroups: + - core.humio.com + resources: + - humiousers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.humio.com + resources: + - humiousers/finalizers + verbs: + - update +- apiGroups: + - core.humio.com + resources: + - humiousers/status + verbs: + - get + - patch + - update - apiGroups: - core.humio.com resources: diff --git a/config/samples/core_v1alpha1_humiouser.yaml b/config/samples/core_v1alpha1_humiouser.yaml new file mode 100644 index 000000000..21330c36b --- /dev/null +++ b/config/samples/core_v1alpha1_humiouser.yaml @@ -0,0 +1,17 @@ +apiVersion: core.humio.com/v1alpha1 +kind: HumioUser +metadata: + name: example-humiouser + labels: + app: 'humiouser' + app.kubernetes.io/name: 'humiouser' + app.kubernetes.io/instance: 'example-humiouser' + app.kubernetes.io/managed-by: 'manual' +spec: + managedClusterName: example-humiocluster + fullName: "users name" + email: "user@example.com" + company: "example company" + countryCode: "DK" + picture: "http://example.com/user.png" + isRoot: false \ No newline at end of file diff --git a/controllers/humiouser_controller.go b/controllers/humiouser_controller.go new file mode 100644 index 000000000..8b45b6e24 --- /dev/null +++ b/controllers/humiouser_controller.go @@ -0,0 +1,234 @@ +/* +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 ( + "context" + "fmt" + "reflect" + "time" + + humioapi "github.com/humio/cli/api" + "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" + "github.com/humio/humio-operator/pkg/humio" +) + +// HumioUserReconciler reconciles a HumioUser object +type HumioUserReconciler struct { + client.Client + BaseLogger logr.Logger + Log logr.Logger + HumioClient humio.Client + Namespace string +} + +//+kubebuilder:rbac:groups=core.humio.com,resources=humiousers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.humio.com,resources=humiousers/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=core.humio.com,resources=humiousers/finalizers,verbs=update + +func (r *HumioUserReconciler) 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 HumioUser") + + // Fetch the HumioUser instance + hu := &humiov1alpha1.HumioUser{} + err := r.Get(ctx, req.NamespacedName, hu) + if 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 + } + + cluster, err := helpers.NewCluster(ctx, r, hu.Spec.ManagedClusterName, hu.Spec.ExternalClusterName, hu.Namespace, helpers.UseCertManager(), true) + if err != nil || cluster == nil || cluster.Config() == nil { + r.Log.Error(err, "unable to obtain humio client config") + err = r.setState(ctx, humiov1alpha1.HumioUserStateConfigError, hu) + if err != nil { + return reconcile.Result{}, r.logErrorAndReturn(err, "unable to set cluster state") + } + return reconcile.Result{RequeueAfter: time.Second * 15}, nil + } + + r.Log.Info("Checking if user is marked to be deleted") + // Check if the HumioUser instance is marked to be deleted, which is + // indicated by the deletion timestamp being set. + isHumioUserMarkedToBeDeleted := hu.GetDeletionTimestamp() != nil + if isHumioUserMarkedToBeDeleted { + r.Log.Info("User marked to be deleted") + if helpers.ContainsElement(hu.GetFinalizers(), humioFinalizer) { + // Run finalization logic for humioFinalizer. If the + // finalization logic fails, don't remove the finalizer so + // that we can retry during the next reconciliation. + r.Log.Info("User contains finalizer so run finalizer method") + if err := r.finalize(ctx, cluster.Config(), req, hu); err != nil { + return reconcile.Result{}, r.logErrorAndReturn(err, "Finalizer method returned error") + } + + // Remove humioFinalizer. Once all finalizers have been + // removed, the object will be deleted. + r.Log.Info("Finalizer done. Removing finalizer") + hu.SetFinalizers(helpers.RemoveElement(hu.GetFinalizers(), humioFinalizer)) + err := r.Update(ctx, hu) + if err != nil { + return reconcile.Result{}, err + } + r.Log.Info("Finalizer removed successfully") + } + return reconcile.Result{}, nil + } + + // Add finalizer for this CR + if !helpers.ContainsElement(hu.GetFinalizers(), humioFinalizer) { + r.Log.Info("Finalizer not present, adding finalizer to user") + if err := r.addFinalizer(ctx, hu); err != nil { + return reconcile.Result{}, err + } + } + + defer func(ctx context.Context, humioClient humio.Client, hu *humiov1alpha1.HumioUser) { + if hu.Status.State == humiov1alpha1.HumioAlertStateConfigError { + return + } + curUser, err := humioClient.GetUser(cluster.Config(), req, hu) + if err != nil { + _ = r.setState(ctx, humiov1alpha1.HumioUserStateUnknown, hu) + return + } + emptyUser := humioapi.User{} + if reflect.DeepEqual(emptyUser, *curUser) { + _ = r.setState(ctx, humiov1alpha1.HumioUserStateNotFound, hu) + return + } + _ = r.setState(ctx, humiov1alpha1.HumioUserStateExists, hu) + }(ctx, r.HumioClient, hu) + + // Get current user + r.Log.Info("get current user") + curUser, err := r.HumioClient.GetUser(cluster.Config(), req, hu) + emptyUser := humioapi.User{} + + // if this is a new user check that the username doesn't exist in humio first + if (len(hu.Status.State) == 0) && (*curUser != emptyUser) { + r.Log.Info("No state for user but user exists in Humio") + r.setState(ctx, humiov1alpha1.HumioUserStateConfigError, hu) + return reconcile.Result{}, r.logErrorAndReturn(err, "could not create user resource, username exists") + + } + if emptyUser == *curUser { + r.Log.Info("user doesn't exist. Now adding user") + // create user + _, err := r.HumioClient.AddUser(cluster.Config(), req, hu) + if err != nil { + r.setState(ctx, humiov1alpha1.HumioUserStateConfigError, hu) + return reconcile.Result{}, r.logErrorAndReturn(err, "could not create user") + } + r.Log.Info("created user", "Username", hu.Spec.Username) + return reconcile.Result{Requeue: true}, nil + + } + + if (curUser.FullName != hu.Spec.FullName) || + (curUser.Email != hu.Spec.Email) || + (curUser.Company != hu.Spec.Company) || + (curUser.CountryCode != hu.Spec.CountryCode) || + (curUser.Picture != hu.Spec.Picture) || + (curUser.IsRoot != bool(hu.Spec.IsRoot)) { + r.Log.Info(fmt.Sprintf("user information differs, triggering update, expected %v/%v/%v/%v/%v/%v/%v, got: %v/%v/%v/%v/%v/%v/%v", + hu.Spec.Username, + hu.Spec.FullName, + hu.Spec.Email, + hu.Spec.Company, + hu.Spec.CountryCode, + hu.Spec.Picture, + hu.Spec.IsRoot, + curUser.Username, + curUser.FullName, + curUser.Email, + curUser.Company, + curUser.CountryCode, + curUser.Picture, + curUser.IsRoot)) + _, err = r.HumioClient.UpdateUser(cluster.Config(), req, hu) + if err != nil { + return reconcile.Result{}, r.logErrorAndReturn(err, "could not update user") + } + } + + r.Log.Info("done reconciling, will requeue after 15 seconds") + return reconcile.Result{RequeueAfter: time.Second * 15}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *HumioUserReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&humiov1alpha1.HumioUser{}). + Complete(r) +} + +func (r *HumioUserReconciler) finalize(ctx context.Context, config *humioapi.Config, req reconcile.Request, hu *humiov1alpha1.HumioUser) error { + _, err := helpers.NewCluster(ctx, r, hu.Spec.ManagedClusterName, hu.Spec.ExternalClusterName, hu.Namespace, helpers.UseCertManager(), true) + if k8serrors.IsNotFound(err) { + return nil + } + return r.HumioClient.DeleteUser(config, req, hu) +} + +func (r *HumioUserReconciler) addFinalizer(ctx context.Context, hu *humiov1alpha1.HumioUser) error { + r.Log.Info("Adding Finalizer for the HumioUser") + hu.SetFinalizers(append(hu.GetFinalizers(), humioFinalizer)) + + // Update CR + err := r.Update(ctx, hu) + if err != nil { + return r.logErrorAndReturn(err, "Failed to update HumioUser with finalizer") + } + return nil +} + +func (r *HumioUserReconciler) setState(ctx context.Context, state string, hu *humiov1alpha1.HumioUser) error { + if hu.Status.State == state { + return nil + } + r.Log.Info(fmt.Sprintf("setting user state to %s", state)) + hu.Status.State = state + return r.Status().Update(ctx, hu) +} + +func (r *HumioUserReconciler) logErrorAndReturn(err error, msg string) error { + r.Log.Error(err, msg) + return fmt.Errorf("%s: %w", msg, err) +} diff --git a/controllers/suite/clusters/suite_test.go b/controllers/suite/clusters/suite_test.go index a86314c54..79ff669f0 100644 --- a/controllers/suite/clusters/suite_test.go +++ b/controllers/suite/clusters/suite_test.go @@ -76,6 +76,7 @@ var humioClientForHumioExternalCluster humio.Client var humioClientForHumioIngestToken humio.Client var humioClientForHumioParser humio.Client var humioClientForHumioRepository humio.Client +var humioClientForHumioUser humio.Client var humioClientForHumioView humio.Client var humioClientForTestSuite humio.Client var testTimeout time.Duration @@ -113,6 +114,7 @@ var _ = BeforeSuite(func() { humioClientForHumioIngestToken = humio.NewClient(log, &humioapi.Config{}, "") humioClientForHumioParser = humio.NewClient(log, &humioapi.Config{}, "") humioClientForHumioRepository = humio.NewClient(log, &humioapi.Config{}, "") + humioClientForHumioUser = humio.NewClient(log, &humioapi.Config{}, "") humioClientForHumioView = humio.NewClient(log, &humioapi.Config{}, "") } else { testTimeout = time.Second * 30 @@ -129,6 +131,7 @@ var _ = BeforeSuite(func() { humioClientForHumioIngestToken = humio.NewMockClient(humioapi.Cluster{}, nil, nil, nil) humioClientForHumioParser = humio.NewMockClient(humioapi.Cluster{}, nil, nil, nil) humioClientForHumioRepository = humio.NewMockClient(humioapi.Cluster{}, nil, nil, nil) + humioClientForHumioUser = humio.NewMockClient(humioapi.Cluster{}, nil, nil, nil) humioClientForHumioView = humio.NewMockClient(humioapi.Cluster{}, nil, nil, nil) } @@ -234,6 +237,14 @@ var _ = BeforeSuite(func() { }).SetupWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) + err = (&controllers.HumioUserReconciler{ + Client: k8sManager.GetClient(), + HumioClient: humioClientForHumioUser, + BaseLogger: log, + Namespace: testProcessNamespace, + }).SetupWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + err = (&controllers.HumioViewReconciler{ Client: k8sManager.GetClient(), HumioClient: humioClientForHumioView, diff --git a/controllers/suite/resources/humioresources_controller_test.go b/controllers/suite/resources/humioresources_controller_test.go index 7340a2274..2f3e49026 100644 --- a/controllers/suite/resources/humioresources_controller_test.go +++ b/controllers/suite/resources/humioresources_controller_test.go @@ -2211,6 +2211,72 @@ var _ = Describe("Humio Resources Controllers", func() { suite.UsingClusterBy(clusterKey.Name, "HumioAlert: Creating the invalid alert") Expect(k8sClient.Create(ctx, toCreateInvalidAlert)).Should(Not(Succeed())) }) + + It("HumioUser: Creating user non-existent managed cluster", func() { + ctx := context.Background() + keyErr := types.NamespacedName{ + Name: "humiouser-non-existent-managed-cluster", + Namespace: clusterKey.Namespace, + } + toCreateUser := &humiov1alpha1.HumioUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: keyErr.Name, + Namespace: keyErr.Namespace, + }, + Spec: humiov1alpha1.HumioUserSpec{ + ManagedClusterName: "non-existent-managed-cluster", + Email: "user@example.com", + }, + } + Expect(k8sClient.Create(ctx, toCreateUser)).Should(Succeed()) + + suite.UsingClusterBy(clusterKey.Name, fmt.Sprintf("HumioUser: Validates resource enters state %s", humiov1alpha1.HumioUserStateConfigError)) + fetchedUser := &humiov1alpha1.HumioUser{} + Eventually(func() string { + k8sClient.Get(ctx, keyErr, fetchedUser) + return fetchedUser.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioUserStateConfigError)) + + suite.UsingClusterBy(clusterKey.Name, "HumioUser: Successfully deleting it") + Expect(k8sClient.Delete(ctx, fetchedUser)).To(Succeed()) + Eventually(func() bool { + err := k8sClient.Get(ctx, keyErr, fetchedUser) + return k8serrors.IsNotFound(err) + }, testTimeout, suite.TestInterval).Should(BeTrue()) + }) + + It("HumioUser: Creating user pointing to non-existent external cluster", func() { + ctx := context.Background() + keyErr := types.NamespacedName{ + Name: "humiouser-non-existent-external-cluster", + Namespace: clusterKey.Namespace, + } + toCreateUser := &humiov1alpha1.HumioUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: keyErr.Name, + Namespace: keyErr.Namespace, + }, + Spec: humiov1alpha1.HumioUserSpec{ + ExternalClusterName: "non-existent-external-cluster", + Email: "user@example.com", + }, + } + Expect(k8sClient.Create(ctx, toCreateUser)).Should(Succeed()) + + suite.UsingClusterBy(clusterKey.Name, fmt.Sprintf("HumioUser: Validates resource enters state %s", humiov1alpha1.HumioUserStateConfigError)) + fetchedUser := &humiov1alpha1.HumioUser{} + Eventually(func() string { + k8sClient.Get(ctx, keyErr, fetchedUser) + return fetchedUser.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioUserStateConfigError)) + + suite.UsingClusterBy(clusterKey.Name, "HumioUser: Successfully deleting it") + Expect(k8sClient.Delete(ctx, fetchedUser)).To(Succeed()) + Eventually(func() bool { + err := k8sClient.Get(ctx, keyErr, fetchedUser) + return k8serrors.IsNotFound(err) + }, testTimeout, suite.TestInterval).Should(BeTrue()) + }) }) }) diff --git a/controllers/suite/resources/suite_test.go b/controllers/suite/resources/suite_test.go index 240e3a585..2c98b76a3 100644 --- a/controllers/suite/resources/suite_test.go +++ b/controllers/suite/resources/suite_test.go @@ -74,6 +74,7 @@ var testNamespace corev1.Namespace var testRepo corev1alpha1.HumioRepository var testService1 corev1.Service var testService2 corev1.Service +var testUser corev1alpha1.HumioUser var clusterKey types.NamespacedName var cluster = &corev1alpha1.HumioCluster{} var sharedCluster helpers.ClusterInterface @@ -219,6 +220,14 @@ var _ = BeforeSuite(func() { }).SetupWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) + err = (&controllers.HumioUserReconciler{ + Client: k8sManager.GetClient(), + HumioClient: humioClient, + BaseLogger: log, + Namespace: clusterKey.Namespace, + }).SetupWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + err = (&controllers.HumioViewReconciler{ Client: k8sManager.GetClient(), HumioClient: humioClient, diff --git a/go.sum b/go.sum index cef33c155..1bcca7fe2 100644 --- a/go.sum +++ b/go.sum @@ -436,6 +436,7 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/main.go b/main.go index 3a157e0bf..ba9d9d6e7 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ import ( humioapi "github.com/humio/cli/api" cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" openshiftsecurityv1 "github.com/openshift/api/security/v1" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -168,6 +169,14 @@ func main() { ctrl.Log.Error(err, "unable to create controller", "controller", "HumioRepository") os.Exit(1) } + if err = (&controllers.HumioUserReconciler{ + Client: mgr.GetClient(), + HumioClient: humio.NewClient(log, &humioapi.Config{}, userAgent), + BaseLogger: log, + }).SetupWithManager(mgr); err != nil { + ctrl.Log.Error(err, "unable to create controller", "controller", "HumioUser") + os.Exit(1) + } if err = (&controllers.HumioViewReconciler{ Client: mgr.GetClient(), HumioClient: humio.NewClient(log, &humioapi.Config{}, userAgent), diff --git a/pkg/humio/client.go b/pkg/humio/client.go index b3550f5fc..949d8a1e5 100644 --- a/pkg/humio/client.go +++ b/pkg/humio/client.go @@ -28,6 +28,7 @@ import ( "github.com/go-logr/logr" + "github.com/humio/cli/api" humioapi "github.com/humio/cli/api" humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1" "github.com/humio/humio-operator/pkg/helpers" @@ -39,6 +40,7 @@ type Client interface { IngestTokensClient ParsersClient RepositoriesClient + UsersClient ViewsClient LicenseClient ActionsClient @@ -79,6 +81,13 @@ type RepositoriesClient interface { DeleteRepository(*humioapi.Config, reconcile.Request, *humiov1alpha1.HumioRepository) error } +type UsersClient interface { + AddUser(*humioapi.Config, reconcile.Request, *humiov1alpha1.HumioUser) (*humioapi.User, error) + GetUser(*humioapi.Config, reconcile.Request, *humiov1alpha1.HumioUser) (*humioapi.User, error) + UpdateUser(*humioapi.Config, reconcile.Request, *humiov1alpha1.HumioUser) (*humioapi.User, error) + DeleteUser(*humioapi.Config, reconcile.Request, *humiov1alpha1.HumioUser) error +} + type ViewsClient interface { AddView(*humioapi.Config, reconcile.Request, *humiov1alpha1.HumioView) (*humioapi.View, error) GetView(*humioapi.Config, reconcile.Request, *humiov1alpha1.HumioView) (*humioapi.View, error) @@ -651,3 +660,71 @@ func (h *ClientConfig) GetActionIDsMapForAlerts(config *humioapi.Config, req rec } return actionIdMap, nil } + +func (h *ClientConfig) AddUser(config *humioapi.Config, req reconcile.Request, hu *humiov1alpha1.HumioUser) (*humioapi.User, error) { + user := humioapi.User{Username: hu.Spec.Username} + _, err := h.GetHumioClient(config, req).Users().Add(hu.Spec.Username, api.UserChangeSet{ + IsRoot: &hu.Spec.IsRoot, + FullName: &hu.Spec.FullName, + Company: &hu.Spec.Company, + CountryCode: &hu.Spec.CountryCode, + Email: &hu.Spec.Email, + Picture: &hu.Spec.Picture, + }) + return &user, err +} + +func (h *ClientConfig) GetUser(config *humioapi.Config, req reconcile.Request, hu *humiov1alpha1.HumioUser) (*humioapi.User, error) { + // userList, err := h.GetHumioClient(config, req).Users().List() + // if err != nil { + // return &humioapi.User{}, fmt.Errorf("could not list users: %w", err) + // } + // for _, user := range userList { + // if user.Username == hu.Spec.Username { + // // we now know the user exists + // user, err := h.GetHumioClient(config, req).Users().Get(hu.Spec.Username) + // return &user, err + // } + // } + user, err := h.GetHumioClient(config, req).Users().Get(hu.Spec.Username) + return &user, err +} + +func (h *ClientConfig) UpdateUser(config *humioapi.Config, req reconcile.Request, hu *humiov1alpha1.HumioUser) (*humioapi.User, error) { + curUser, err := h.GetUser(config, req, hu) + if err != nil { + return &humioapi.User{}, err + } + + if curUser.Email != hu.Spec.Email || + curUser.FullName != hu.Spec.FullName || + curUser.Company != hu.Spec.Company || + curUser.CountryCode != hu.Spec.CountryCode || + curUser.Picture != hu.Spec.Picture || + curUser.IsRoot != hu.Spec.IsRoot { + _, err = h.GetHumioClient(config, req).Users().Update( + hu.Spec.Username, + api.UserChangeSet{ + Email: &hu.Spec.Email, + FullName: &hu.Spec.FullName, + Company: &hu.Spec.Company, + CountryCode: &hu.Spec.CountryCode, + Picture: &hu.Spec.Picture, + IsRoot: &hu.Spec.IsRoot, + }, + ) + if err != nil { + return &humioapi.User{}, err + } + } + + return h.GetUser(config, req, hu) +} + +func (h *ClientConfig) DeleteUser(config *humioapi.Config, req reconcile.Request, hu *humiov1alpha1.HumioUser) error { + _, err := h.GetHumioClient(config, req).Users().Remove(hu.Spec.Username) + if err != nil { + return fmt.Errorf("could not delete user: %w", err) + } + return err +} diff --git a/pkg/humio/client_mock.go b/pkg/humio/client_mock.go index 87a718916..037bf6b91 100644 --- a/pkg/humio/client_mock.go +++ b/pkg/humio/client_mock.go @@ -42,6 +42,7 @@ type ClientMock struct { OnPremLicense humioapi.OnPremLicense Action humioapi.Action Alert humioapi.Alert + User humioapi.User } type MockClientConfig struct { @@ -215,6 +216,34 @@ func (h *MockClientConfig) DeleteRepository(config *humioapi.Config, req reconci return nil } +func (h *MockClientConfig) AddUser(config *humioapi.Config, req reconcile.Request, hu *humiov1alpha1.HumioUser) (*humioapi.User, error) { + h.apiClient.User = humioapi.User{ + Username: hu.Spec.Username, + ID: kubernetes.RandomString(), + FullName: hu.Spec.FullName, + Email: hu.Spec.Email, + Company: hu.Spec.Company, + CountryCode: hu.Spec.CountryCode, + Picture: hu.Spec.Picture, + IsRoot: hu.Spec.IsRoot, + CreatedAt: hu.Spec.CreatedAt, + } + return &h.apiClient.User, nil +} + +func (h *MockClientConfig) GetUser(config *humioapi.Config, req reconcile.Request, hu *humiov1alpha1.HumioUser) (*humioapi.User, error) { + return &h.apiClient.User, nil +} + +func (h *MockClientConfig) UpdateUser(config *humioapi.Config, req reconcile.Request, hu *humiov1alpha1.HumioUser) (*humioapi.User, error) { + return h.AddUser(config, req, hu) +} + +func (h *MockClientConfig) DeleteUser(config *humioapi.Config, req reconcile.Request, hu *humiov1alpha1.HumioUser) error { + h.apiClient.User = humioapi.User{} + return nil +} + func (h *MockClientConfig) GetView(config *humioapi.Config, req reconcile.Request, hv *humiov1alpha1.HumioView) (*humioapi.View, error) { return &h.apiClient.View, nil }