diff --git a/PROJECT b/PROJECT index 5b51d8f4..94238dd2 100644 --- a/PROJECT +++ b/PROJECT @@ -76,4 +76,13 @@ resources: kind: HumioView path: github.com/humio/humio-operator/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: humio.com + group: core + kind: HumioUser + path: github.com/humio/humio-operator/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/humiouser_types.go b/api/v1alpha1/humiouser_types.go new file mode 100644 index 00000000..f4859c61 --- /dev/null +++ b/api/v1alpha1/humiouser_types.go @@ -0,0 +1,93 @@ +/* +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"` +} + +// 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 d4573670..e9c46dec 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1496,6 +1496,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 00000000..d21aa508 --- /dev/null +++ b/charts/humio-operator/crds/core.humio.com_humiousers.yaml @@ -0,0 +1,100 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + 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.23.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 + 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: {} diff --git a/charts/humio-operator/templates/operator-rbac.yaml b/charts/humio-operator/templates/operator-rbac.yaml index eac1d57b..8bb6ba34 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 @@ -234,6 +237,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 00000000..d21aa508 --- /dev/null +++ b/config/crd/bases/core.humio.com_humiousers.yaml @@ -0,0 +1,100 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + 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.23.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 + 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: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index b31fad43..ea301f7a 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -11,6 +11,7 @@ resources: - bases/core.humio.com_humioactions.yaml - bases/core.humio.com_humioalerts.yaml - bases/core.humio.com_humiofilteralerts.yaml +- bases/core.humio.com_humiousers.yaml # +kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -25,6 +26,7 @@ patchesStrategicMerge: #- patches/webhook_in_humioactions.yaml #- patches/webhook_in_humioalerts.yaml #- patches/webhook_in_humiofilteralerts.yaml +#- patches/webhook_in_humiousers.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. @@ -38,6 +40,7 @@ patchesStrategicMerge: #- patches/cainjection_in_humioactions.yaml #- patches/cainjection_in_humioalerts.yaml #- patches/cainjection_in_humiofilteralerts.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 00000000..97fdf7c0 --- /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 00000000..a74aa57e --- /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 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/rbac/humiouser_editor_role.yaml b/config/rbac/humiouser_editor_role.yaml new file mode 100644 index 00000000..571e79a3 --- /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 00000000..2442b008 --- /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 52bf3f36..b1777b7f 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -320,6 +320,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 00000000..b82c4b53 --- /dev/null +++ b/config/samples/core_v1alpha1_humiouser.yaml @@ -0,0 +1,12 @@ +apiVersion: core.humio.com/v1alpha1 +kind: HumioUser +metadata: + name: example-humiouser +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 00000000..7582e387 --- /dev/null +++ b/controllers/humiouser_controller.go @@ -0,0 +1,240 @@ +/* +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, _ := 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") + err = r.setState(ctx, humiov1alpha1.HumioUserStateConfigError, hu) + if err != nil { + return reconcile.Result{}, r.logErrorAndReturn(err, "unable to set user state") + } + 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 { + err = r.setState(ctx, humiov1alpha1.HumioUserStateConfigError, hu) + if err != nil { + return reconcile.Result{}, r.logErrorAndReturn(err, "unable to set user state") + } + 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 c8411c2c..75ad07dc 100644 --- a/controllers/suite/clusters/suite_test.go +++ b/controllers/suite/clusters/suite_test.go @@ -74,6 +74,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 @@ -111,6 +112,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 @@ -127,6 +129,7 @@ var _ = BeforeSuite(func() { humioClientForHumioIngestToken = humio.NewMockClient(humioapi.Cluster{}, nil) humioClientForHumioParser = humio.NewMockClient(humioapi.Cluster{}, nil) humioClientForHumioRepository = humio.NewMockClient(humioapi.Cluster{}, nil) + humioClientForHumioUser = humio.NewMockClient(humioapi.Cluster{}, nil) humioClientForHumioView = humio.NewMockClient(humioapi.Cluster{}, nil) } @@ -218,6 +221,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 5058e9b0..5b3479f4 100644 --- a/controllers/suite/resources/humioresources_controller_test.go +++ b/controllers/suite/resources/humioresources_controller_test.go @@ -2873,6 +2873,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()) + }) }) Context("Humio Filter Alert", func() { diff --git a/controllers/suite/resources/suite_test.go b/controllers/suite/resources/suite_test.go index 9df27b1a..2fd401f9 100644 --- a/controllers/suite/resources/suite_test.go +++ b/controllers/suite/resources/suite_test.go @@ -210,6 +210,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/examples/humiouser.yaml b/examples/humiouser.yaml new file mode 100644 index 00000000..e048fb24 --- /dev/null +++ b/examples/humiouser.yaml @@ -0,0 +1,25 @@ +apiVersion: core.humio.com/v1alpha1 +kind: HumioUser +metadata: + name: example-humiouser-managed +spec: + managedClusterName: example-humiocluster + fullName: "users name" + email: "user@example.com" + company: "example company" + countryCode: "DK" + picture: "http://example.com/user.png" + isRoot: false +--- +apiVersion: core.humio.com/v1alpha1 +kind: HumioUser +metadata: + name: example-humiouser-external +spec: + externalClusterName: example-humioexternalcluster + fullName: "users name" + email: "user@example.com" + company: "example company" + countryCode: "DK" + picture: "http://example.com/user.png" + isRoot: false diff --git a/main.go b/main.go index d4782502..11f7f98e 100644 --- a/main.go +++ b/main.go @@ -154,6 +154,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 9625a9a0..9d21760f 100644 --- a/pkg/humio/client.go +++ b/pkg/humio/client.go @@ -40,6 +40,7 @@ type Client interface { IngestTokensClient ParsersClient RepositoriesClient + UsersClient ViewsClient LicenseClient ActionsClient @@ -77,6 +78,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) @@ -767,3 +775,60 @@ func (h *ClientConfig) ValidateActionsForFilterAlert(config *humioapi.Config, re } return 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, humioapi.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) { + 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, + humioapi.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 2ef66fe2..16c1c664 100644 --- a/pkg/humio/client_mock.go +++ b/pkg/humio/client_mock.go @@ -42,6 +42,7 @@ type ClientMock struct { Action humioapi.Action Alert humioapi.Alert FilterAlert humioapi.FilterAlert + User humioapi.User } type MockClientConfig struct { @@ -175,6 +176,33 @@ 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, + } + 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) { h.apiClient.View.AutomaticSearch = helpers.BoolTrue(hv.Spec.AutomaticSearch) return &h.apiClient.View, nil