diff --git a/.gitignore b/.gitignore
index 0d80e1790..6259c65b6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -84,4 +84,5 @@ bin/
testbin/
*-junit.xml
.envrc
-tmp/**
\ No newline at end of file
+tmp/**
+humio-operator.iml
diff --git a/api/v1alpha1/humiogroup_types.go b/api/v1alpha1/humiogroup_types.go
new file mode 100644
index 000000000..e0fcfc12d
--- /dev/null
+++ b/api/v1alpha1/humiogroup_types.go
@@ -0,0 +1,72 @@
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+ // HumioGroupStateUnknown is the Unknown state of the group
+ HumioGroupStateUnknown = "Unknown"
+ // HumioGroupStateExists is the Exists state of the group
+ HumioGroupStateExists = "Exists"
+ // HumioGroupStateNotFound is the NotFound state of the group
+ HumioGroupStateNotFound = "NotFound"
+ // HumioGroupStateConfigError is the state of the group when user-provided specification results in configuration error, such as non-existent humio cluster
+ HumioGroupStateConfigError = "ConfigError"
+)
+
+// HumioGroupSpec defines the desired state of HumioGroup.
+// +kubebuilder:validation:XValidation:rule="(has(self.managedClusterName) && self.managedClusterName != \"\") != (has(self.externalClusterName) && self.externalClusterName != \"\")",message="Must specify exactly one of managedClusterName or externalClusterName"
+type HumioGroupSpec 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"`
+ // Name is the display name of the HumioGroup
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
+ // +kubebuilder:validation:Required
+ Name string `json:"name"`
+ // ExternalMappingName is the mapping name from the external provider that will assign the user to this HumioGroup
+ // +kubebuilder:validation:MinLength=2
+ // +kubebuilder:validation:Optional
+ ExternalMappingName *string `json:"externalMappingName,omitempty"`
+}
+
+// HumioGroupStatus defines the observed state of HumioGroup.
+type HumioGroupStatus struct {
+ // State reflects the current state of the HumioGroup
+ State string `json:"state,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:resource:path=humiogroups,scope=Namespaced
+// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="The state of the group"
+// +operator-sdk:gen-csv:customresourcedefinitions.displayName="Humio Group"
+
+// HumioGroup is the Schema for the humiogroups API
+type HumioGroup struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ // +kubebuilder:validation:Required
+ Spec HumioGroupSpec `json:"spec,omitempty"`
+ Status HumioGroupStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// HumioGroupList contains a list of HumioGroup
+type HumioGroupList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []HumioGroup `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&HumioGroup{}, &HumioGroupList{})
+}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index e53f9b3df..cf2a8b63e 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -1171,6 +1171,100 @@ 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 *HumioGroup) DeepCopyInto(out *HumioGroup) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioGroup.
+func (in *HumioGroup) DeepCopy() *HumioGroup {
+ if in == nil {
+ return nil
+ }
+ out := new(HumioGroup)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HumioGroup) 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 *HumioGroupList) DeepCopyInto(out *HumioGroupList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]HumioGroup, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioGroupList.
+func (in *HumioGroupList) DeepCopy() *HumioGroupList {
+ if in == nil {
+ return nil
+ }
+ out := new(HumioGroupList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HumioGroupList) 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 *HumioGroupSpec) DeepCopyInto(out *HumioGroupSpec) {
+ *out = *in
+ if in.ExternalMappingName != nil {
+ in, out := &in.ExternalMappingName, &out.ExternalMappingName
+ *out = new(string)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioGroupSpec.
+func (in *HumioGroupSpec) DeepCopy() *HumioGroupSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(HumioGroupSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HumioGroupStatus) DeepCopyInto(out *HumioGroupStatus) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioGroupStatus.
+func (in *HumioGroupStatus) DeepCopy() *HumioGroupStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(HumioGroupStatus)
+ in.DeepCopyInto(out)
+ 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
diff --git a/charts/humio-operator/crds/core.humio.com_humiogroups.yaml b/charts/humio-operator/crds/core.humio.com_humiogroups.yaml
new file mode 100644
index 000000000..c3f217dfb
--- /dev/null
+++ b/charts/humio-operator/crds/core.humio.com_humiogroups.yaml
@@ -0,0 +1,96 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.17.0
+ name: humiogroups.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.28.2'
+spec:
+ group: core.humio.com
+ names:
+ kind: HumioGroup
+ listKind: HumioGroupList
+ plural: humiogroups
+ singular: humiogroup
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - description: The state of the group
+ jsonPath: .status.state
+ name: State
+ type: string
+ name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: HumioGroup is the Schema for the humiogroups 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: HumioGroupSpec defines the desired state of HumioGroup.
+ properties:
+ externalClusterName:
+ description: |-
+ ExternalClusterName refers to an object of type HumioExternalCluster where the Humio resources should be created.
+ This conflicts with ManagedClusterName.
+ type: string
+ externalMappingName:
+ description: ExternalMappingName is the mapping name from the external
+ provider that will assign the user to this HumioGroup
+ minLength: 2
+ type: string
+ 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
+ name:
+ description: Name is the display name of the HumioGroup
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: Value is immutable
+ rule: self == oldSelf
+ required:
+ - name
+ type: object
+ x-kubernetes-validations:
+ - message: Must specify exactly one of managedClusterName or externalClusterName
+ rule: (has(self.managedClusterName) && self.managedClusterName != "")
+ != (has(self.externalClusterName) && self.externalClusterName != "")
+ status:
+ description: HumioGroupStatus defines the observed state of HumioGroup.
+ properties:
+ state:
+ description: State reflects the current state of the HumioGroup
+ type: string
+ type: object
+ required:
+ - spec
+ 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 1e5746678..b2445e669 100644
--- a/charts/humio-operator/templates/operator-rbac.yaml
+++ b/charts/humio-operator/templates/operator-rbac.yaml
@@ -103,6 +103,9 @@ rules:
- humiofilteralerts
- humiofilteralerts/finalizers
- humiofilteralerts/status
+ - humiogroups
+ - humiogroups/finalizers
+ - humiogroups/status
- humiousers
- humiousers/finalizers
- humiousers/status
diff --git a/cmd/main.go b/cmd/main.go
index 54f871208..a7cb3343b 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -24,15 +24,15 @@ import (
"path/filepath"
"time"
- "github.com/humio/humio-operator/internal/controller"
- "github.com/humio/humio-operator/internal/humio"
-
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
"github.com/go-logr/logr"
"github.com/go-logr/zapr"
uberzap "go.uber.org/zap"
+ "github.com/humio/humio-operator/internal/controller"
"github.com/humio/humio-operator/internal/helpers"
+ "github.com/humio/humio-operator/internal/humio"
+
// 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"
@@ -417,6 +417,17 @@ func setupControllers(mgr ctrl.Manager, log logr.Logger, requeuePeriod time.Dura
ctrl.Log.Error(err, "unable to create controller", "controller", "HumioUser")
os.Exit(1)
}
+ if err = (&controller.HumioGroupReconciler{
+ Client: mgr.GetClient(),
+ CommonConfig: controller.CommonConfig{
+ RequeuePeriod: requeuePeriod,
+ },
+ HumioClient: humio.NewClient(log, userAgent),
+ BaseLogger: log,
+ }).SetupWithManager(mgr); err != nil {
+ ctrl.Log.Error(err, "unable to create controller", "controller", "HumioGroup")
+ os.Exit(1)
+ }
if err = (&controller.HumioViewPermissionRoleReconciler{
Client: mgr.GetClient(),
CommonConfig: controller.CommonConfig{
diff --git a/config/crd/bases/core.humio.com_humiogroups.yaml b/config/crd/bases/core.humio.com_humiogroups.yaml
new file mode 100644
index 000000000..c3f217dfb
--- /dev/null
+++ b/config/crd/bases/core.humio.com_humiogroups.yaml
@@ -0,0 +1,96 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.17.0
+ name: humiogroups.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.28.2'
+spec:
+ group: core.humio.com
+ names:
+ kind: HumioGroup
+ listKind: HumioGroupList
+ plural: humiogroups
+ singular: humiogroup
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - description: The state of the group
+ jsonPath: .status.state
+ name: State
+ type: string
+ name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: HumioGroup is the Schema for the humiogroups 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: HumioGroupSpec defines the desired state of HumioGroup.
+ properties:
+ externalClusterName:
+ description: |-
+ ExternalClusterName refers to an object of type HumioExternalCluster where the Humio resources should be created.
+ This conflicts with ManagedClusterName.
+ type: string
+ externalMappingName:
+ description: ExternalMappingName is the mapping name from the external
+ provider that will assign the user to this HumioGroup
+ minLength: 2
+ type: string
+ 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
+ name:
+ description: Name is the display name of the HumioGroup
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: Value is immutable
+ rule: self == oldSelf
+ required:
+ - name
+ type: object
+ x-kubernetes-validations:
+ - message: Must specify exactly one of managedClusterName or externalClusterName
+ rule: (has(self.managedClusterName) && self.managedClusterName != "")
+ != (has(self.externalClusterName) && self.externalClusterName != "")
+ status:
+ description: HumioGroupStatus defines the observed state of HumioGroup.
+ properties:
+ state:
+ description: State reflects the current state of the HumioGroup
+ type: string
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index bec2609e5..4937ca028 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -8,6 +8,7 @@ resources:
- bases/core.humio.com_humioparsers.yaml
- bases/core.humio.com_humiorepositories.yaml
- bases/core.humio.com_humioviews.yaml
+- bases/core.humio.com_humiogroups.yaml
- bases/core.humio.com_humioactions.yaml
- bases/core.humio.com_humioalerts.yaml
- bases/core.humio.com_humiofeatureflags.yaml
@@ -30,6 +31,7 @@ patchesStrategicMerge:
#- patches/webhook_in_humioparsers.yaml
#- patches/webhook_in_humiorepositories.yaml
#- patches/webhook_in_humioviews.yaml
+#- patches/webhook_in_humiogroups.yaml
#- patches/webhook_in_humioactions.yaml
#- patches/webhook_in_humioalerts.yaml
#- patches/webhook_in_humiofilteralerts.yaml
@@ -45,6 +47,7 @@ patchesStrategicMerge:
#- patches/cainjection_in_humioparsers.yaml
#- patches/cainjection_in_humiorepositories.yaml
#- patches/cainjection_in_humioviews.yaml
+#- patches/cainjection_in_humiogroups.yaml
#- patches/cainjection_in_humioactions.yaml
#- patches/cainjection_in_humioalerts.yaml
#- patches/cainjection_in_humiofilteralerts.yaml
diff --git a/config/crd/patches/cainjection_in_humiogroups.yaml b/config/crd/patches/cainjection_in_humiogroups.yaml
new file mode 100644
index 000000000..1d26d6340
--- /dev/null
+++ b/config/crd/patches/cainjection_in_humiogroups.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: humiogroups.core.humio.com
diff --git a/config/crd/patches/webhook_in_humiogroups.yaml b/config/crd/patches/webhook_in_humiogroups.yaml
new file mode 100644
index 000000000..478fdd04c
--- /dev/null
+++ b/config/crd/patches/webhook_in_humiogroups.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: humiogroups.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/rbac/humiogroup_admin_role.yaml b/config/rbac/humiogroup_admin_role.yaml
new file mode 100644
index 000000000..c467cefb0
--- /dev/null
+++ b/config/rbac/humiogroup_admin_role.yaml
@@ -0,0 +1,27 @@
+# This rule is not used by the project humio-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants full permissions ('*') over core.humio.com.
+# This role is intended for users authorized to modify roles and bindings within the cluster,
+# enabling them to delegate specific permissions to other users or groups as needed.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: humio-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: humiogroup-admin-role
+rules:
+- apiGroups:
+ - core.humio.com
+ resources:
+ - humiogroups
+ verbs:
+ - '*'
+- apiGroups:
+ - core.humio.com
+ resources:
+ - humiogroups/status
+ verbs:
+ - get
diff --git a/config/rbac/humiogroup_editor_role.yaml b/config/rbac/humiogroup_editor_role.yaml
new file mode 100644
index 000000000..8855dda50
--- /dev/null
+++ b/config/rbac/humiogroup_editor_role.yaml
@@ -0,0 +1,24 @@
+# permissions for end users to edit humiogroups.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: humiogroup-editor-role
+rules:
+- apiGroups:
+ - core.humio.com
+ resources:
+ - humiogroups
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - core.humio.com
+ resources:
+ - humiogroups/status
+ verbs:
+ - get
diff --git a/config/rbac/humiogroup_viewer_role.yaml b/config/rbac/humiogroup_viewer_role.yaml
new file mode 100644
index 000000000..0955e73e7
--- /dev/null
+++ b/config/rbac/humiogroup_viewer_role.yaml
@@ -0,0 +1,20 @@
+# permissions for end users to view humiogroups.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: humiogroup-viewer-role
+rules:
+- apiGroups:
+ - core.humio.com
+ resources:
+ - humiogroups
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - core.humio.com
+ resources:
+ - humiogroups/status
+ verbs:
+ - get
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 151f49cb0..316d02a9a 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -35,6 +35,7 @@ rules:
- humioexternalclusters
- humiofeatureflags
- humiofilteralerts
+ - humiogroups
- humioingesttokens
- humioorganizationpermissionroles
- humioparsers
@@ -63,6 +64,7 @@ rules:
- humioexternalclusters/finalizers
- humiofeatureflags/finalizers
- humiofilteralerts/finalizers
+ - humiogroups/finalizers
- humioingesttokens/finalizers
- humioorganizationpermissionroles/finalizers
- humioparsers/finalizers
@@ -85,6 +87,7 @@ rules:
- humioexternalclusters/status
- humiofeatureflags/status
- humiofilteralerts/status
+ - humiogroups/status
- humioingesttokens/status
- humioorganizationpermissionroles/status
- humioparsers/status
diff --git a/config/samples/core_v1alpha1_humiogroup.yaml b/config/samples/core_v1alpha1_humiogroup.yaml
new file mode 100644
index 000000000..80cdc8724
--- /dev/null
+++ b/config/samples/core_v1alpha1_humiogroup.yaml
@@ -0,0 +1,11 @@
+apiVersion: core.humio.com/v1alpha1
+kind: HumioGroup
+metadata:
+ name: example-humiogroup-managed
+spec:
+ managedClusterName: example-humiocluster
+ displayName: "example-group"
+ lookupName: "example-group-lookup-name"
+ assignments:
+ - roleName: "example-role"
+ viewName: "example-view"
diff --git a/docs/api.md b/docs/api.md
index 472c6e1de..0e5334761 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -24,6 +24,8 @@ Resource Types:
- [HumioFilterAlert](#humiofilteralert)
+- [HumioGroup](#humiogroup)
+
- [HumioIngestToken](#humioingesttoken)
- [HumioOrganizationPermissionRole](#humioorganizationpermissionrole)
@@ -36813,6 +36815,141 @@ HumioFilterAlertStatus defines the observed state of HumioFilterAlert.
+## HumioGroup
+[↩ Parent](#corehumiocomv1alpha1 )
+
+
+
+
+
+
+HumioGroup is the Schema for the humiogroups API
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | apiVersion |
+ string |
+ core.humio.com/v1alpha1 |
+ true |
+
+
+ | kind |
+ string |
+ HumioGroup |
+ true |
+
+
+ | metadata |
+ object |
+ Refer to the Kubernetes API documentation for the fields of the `metadata` field. |
+ true |
+
+ | spec |
+ object |
+
+ HumioGroupSpec defines the desired state of HumioGroup.
+
+ Validations:(has(self.managedClusterName) && self.managedClusterName != "") != (has(self.externalClusterName) && self.externalClusterName != ""): Must specify exactly one of managedClusterName or externalClusterName
+ |
+ true |
+
+ | status |
+ object |
+
+ HumioGroupStatus defines the observed state of HumioGroup.
+ |
+ false |
+
+
+
+
+### HumioGroup.spec
+[↩ Parent](#humiogroup)
+
+
+
+HumioGroupSpec defines the desired state of HumioGroup.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | name |
+ string |
+
+ Name is the display name of the HumioGroup
+
+ Validations:self == oldSelf: Value is immutable
+ |
+ true |
+
+ | externalClusterName |
+ string |
+
+ ExternalClusterName refers to an object of type HumioExternalCluster where the Humio resources should be created.
+This conflicts with ManagedClusterName.
+ |
+ false |
+
+ | externalMappingName |
+ string |
+
+ ExternalMappingName is the mapping name from the external provider that will assign the user to this HumioGroup
+ |
+ false |
+
+ | managedClusterName |
+ string |
+
+ 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.
+ |
+ false |
+
+
+
+
+### HumioGroup.status
+[↩ Parent](#humiogroup)
+
+
+
+HumioGroupStatus defines the observed state of HumioGroup.
+
+
+
+
+ | Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ | state |
+ string |
+
+ State reflects the current state of the HumioGroup
+ |
+ false |
+
+
+
## HumioIngestToken
[↩ Parent](#corehumiocomv1alpha1 )
diff --git a/internal/api/error.go b/internal/api/error.go
index a1d35204a..8b9abd8d8 100644
--- a/internal/api/error.go
+++ b/internal/api/error.go
@@ -10,6 +10,7 @@ const (
entityTypeSearchDomain entityType = "search-domain"
entityTypeRepository entityType = "repository"
entityTypeView entityType = "view"
+ entityTypeGroup entityType = "group"
entityTypeIngestToken entityType = "ingest-token"
entityTypeParser entityType = "parser"
entityTypeAction entityType = "action"
@@ -66,6 +67,13 @@ func ViewNotFound(name string) error {
}
}
+func GroupNotFound(name string) error {
+ return EntityNotFound{
+ entityType: entityTypeGroup,
+ key: name,
+ }
+}
+
func IngestTokenNotFound(name string) error {
return EntityNotFound{
entityType: entityTypeIngestToken,
diff --git a/internal/api/humiographql/genqlient.yaml b/internal/api/humiographql/genqlient.yaml
index d88d5c153..2a607bb8b 100644
--- a/internal/api/humiographql/genqlient.yaml
+++ b/internal/api/humiographql/genqlient.yaml
@@ -7,6 +7,7 @@ operations:
- graphql/feature-flags.graphql
- graphql/filter-alerts.graphql
- graphql/fragments.graphql
+ - graphql/groups.graphql
- graphql/ingest-tokens.graphql
- graphql/license.graphql
- graphql/parsers.graphql
diff --git a/internal/api/humiographql/graphql/groups.graphql b/internal/api/humiographql/graphql/groups.graphql
new file mode 100644
index 000000000..4f5abeafb
--- /dev/null
+++ b/internal/api/humiographql/graphql/groups.graphql
@@ -0,0 +1,59 @@
+fragment GroupDetails on Group {
+ id
+ displayName
+ lookupName
+}
+
+query GetGroupByDisplayName(
+ $DisplayName: String!
+) {
+ groupByDisplayName(
+ displayName: $DisplayName
+ ) {
+ ...GroupDetails
+ }
+}
+
+mutation CreateGroup(
+ $DisplayName: String!
+ $LookupName: String
+) {
+ addGroup(
+ displayName: $DisplayName
+ lookupName: $LookupName
+ ) {
+ group {
+ ...GroupDetails
+ }
+ }
+}
+
+mutation UpdateGroup(
+ $GroupId: String!
+ $DisplayName: String
+ $LookupName: String
+) {
+ updateGroup(
+ input: {
+ groupId: $GroupId
+ displayName: $DisplayName
+ lookupName: $LookupName
+ }
+ ) {
+ group {
+ ...GroupDetails
+ }
+ }
+}
+
+mutation DeleteGroup(
+ $GroupId: String!
+) {
+ removeGroup(
+ groupId: $GroupId
+ ) {
+ group {
+ ...GroupDetails
+ }
+ }
+}
\ No newline at end of file
diff --git a/internal/api/humiographql/humiographql.go b/internal/api/humiographql/humiographql.go
index 6994d2e26..75dfc5538 100644
--- a/internal/api/humiographql/humiographql.go
+++ b/internal/api/humiographql/humiographql.go
@@ -2123,6 +2123,98 @@ func (v *CreateFilterAlertResponse) GetCreateFilterAlert() CreateFilterAlertCrea
return v.CreateFilterAlert
}
+// CreateGroupAddGroupAddGroupMutation includes the requested fields of the GraphQL type AddGroupMutation.
+type CreateGroupAddGroupAddGroupMutation struct {
+ // Stability: Long-term
+ Group CreateGroupAddGroupAddGroupMutationGroup `json:"group"`
+}
+
+// GetGroup returns CreateGroupAddGroupAddGroupMutation.Group, and is useful for accessing the field via an interface.
+func (v *CreateGroupAddGroupAddGroupMutation) GetGroup() CreateGroupAddGroupAddGroupMutationGroup {
+ return v.Group
+}
+
+// CreateGroupAddGroupAddGroupMutationGroup includes the requested fields of the GraphQL type Group.
+// The GraphQL type's documentation follows.
+//
+// A group.
+type CreateGroupAddGroupAddGroupMutationGroup struct {
+ GroupDetails `json:"-"`
+}
+
+// GetId returns CreateGroupAddGroupAddGroupMutationGroup.Id, and is useful for accessing the field via an interface.
+func (v *CreateGroupAddGroupAddGroupMutationGroup) GetId() string { return v.GroupDetails.Id }
+
+// GetDisplayName returns CreateGroupAddGroupAddGroupMutationGroup.DisplayName, and is useful for accessing the field via an interface.
+func (v *CreateGroupAddGroupAddGroupMutationGroup) GetDisplayName() string {
+ return v.GroupDetails.DisplayName
+}
+
+// GetLookupName returns CreateGroupAddGroupAddGroupMutationGroup.LookupName, and is useful for accessing the field via an interface.
+func (v *CreateGroupAddGroupAddGroupMutationGroup) GetLookupName() *string {
+ return v.GroupDetails.LookupName
+}
+
+func (v *CreateGroupAddGroupAddGroupMutationGroup) UnmarshalJSON(b []byte) error {
+
+ if string(b) == "null" {
+ return nil
+ }
+
+ var firstPass struct {
+ *CreateGroupAddGroupAddGroupMutationGroup
+ graphql.NoUnmarshalJSON
+ }
+ firstPass.CreateGroupAddGroupAddGroupMutationGroup = v
+
+ err := json.Unmarshal(b, &firstPass)
+ if err != nil {
+ return err
+ }
+
+ err = json.Unmarshal(
+ b, &v.GroupDetails)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+type __premarshalCreateGroupAddGroupAddGroupMutationGroup struct {
+ Id string `json:"id"`
+
+ DisplayName string `json:"displayName"`
+
+ LookupName *string `json:"lookupName"`
+}
+
+func (v *CreateGroupAddGroupAddGroupMutationGroup) MarshalJSON() ([]byte, error) {
+ premarshaled, err := v.__premarshalJSON()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(premarshaled)
+}
+
+func (v *CreateGroupAddGroupAddGroupMutationGroup) __premarshalJSON() (*__premarshalCreateGroupAddGroupAddGroupMutationGroup, error) {
+ var retval __premarshalCreateGroupAddGroupAddGroupMutationGroup
+
+ retval.Id = v.GroupDetails.Id
+ retval.DisplayName = v.GroupDetails.DisplayName
+ retval.LookupName = v.GroupDetails.LookupName
+ return &retval, nil
+}
+
+// CreateGroupResponse is returned by CreateGroup on success.
+type CreateGroupResponse struct {
+ // Creates a new group.
+ // Stability: Long-term
+ AddGroup CreateGroupAddGroupAddGroupMutation `json:"addGroup"`
+}
+
+// GetAddGroup returns CreateGroupResponse.AddGroup, and is useful for accessing the field via an interface.
+func (v *CreateGroupResponse) GetAddGroup() CreateGroupAddGroupAddGroupMutation { return v.AddGroup }
+
// CreateHumioRepoActionCreateHumioRepoAction includes the requested fields of the GraphQL type HumioRepoAction.
// The GraphQL type's documentation follows.
//
@@ -3028,6 +3120,100 @@ type DeleteFilterAlertResponse struct {
// GetDeleteFilterAlert returns DeleteFilterAlertResponse.DeleteFilterAlert, and is useful for accessing the field via an interface.
func (v *DeleteFilterAlertResponse) GetDeleteFilterAlert() bool { return v.DeleteFilterAlert }
+// DeleteGroupRemoveGroupRemoveGroupMutation includes the requested fields of the GraphQL type RemoveGroupMutation.
+type DeleteGroupRemoveGroupRemoveGroupMutation struct {
+ // Stability: Long-term
+ Group DeleteGroupRemoveGroupRemoveGroupMutationGroup `json:"group"`
+}
+
+// GetGroup returns DeleteGroupRemoveGroupRemoveGroupMutation.Group, and is useful for accessing the field via an interface.
+func (v *DeleteGroupRemoveGroupRemoveGroupMutation) GetGroup() DeleteGroupRemoveGroupRemoveGroupMutationGroup {
+ return v.Group
+}
+
+// DeleteGroupRemoveGroupRemoveGroupMutationGroup includes the requested fields of the GraphQL type Group.
+// The GraphQL type's documentation follows.
+//
+// A group.
+type DeleteGroupRemoveGroupRemoveGroupMutationGroup struct {
+ GroupDetails `json:"-"`
+}
+
+// GetId returns DeleteGroupRemoveGroupRemoveGroupMutationGroup.Id, and is useful for accessing the field via an interface.
+func (v *DeleteGroupRemoveGroupRemoveGroupMutationGroup) GetId() string { return v.GroupDetails.Id }
+
+// GetDisplayName returns DeleteGroupRemoveGroupRemoveGroupMutationGroup.DisplayName, and is useful for accessing the field via an interface.
+func (v *DeleteGroupRemoveGroupRemoveGroupMutationGroup) GetDisplayName() string {
+ return v.GroupDetails.DisplayName
+}
+
+// GetLookupName returns DeleteGroupRemoveGroupRemoveGroupMutationGroup.LookupName, and is useful for accessing the field via an interface.
+func (v *DeleteGroupRemoveGroupRemoveGroupMutationGroup) GetLookupName() *string {
+ return v.GroupDetails.LookupName
+}
+
+func (v *DeleteGroupRemoveGroupRemoveGroupMutationGroup) UnmarshalJSON(b []byte) error {
+
+ if string(b) == "null" {
+ return nil
+ }
+
+ var firstPass struct {
+ *DeleteGroupRemoveGroupRemoveGroupMutationGroup
+ graphql.NoUnmarshalJSON
+ }
+ firstPass.DeleteGroupRemoveGroupRemoveGroupMutationGroup = v
+
+ err := json.Unmarshal(b, &firstPass)
+ if err != nil {
+ return err
+ }
+
+ err = json.Unmarshal(
+ b, &v.GroupDetails)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+type __premarshalDeleteGroupRemoveGroupRemoveGroupMutationGroup struct {
+ Id string `json:"id"`
+
+ DisplayName string `json:"displayName"`
+
+ LookupName *string `json:"lookupName"`
+}
+
+func (v *DeleteGroupRemoveGroupRemoveGroupMutationGroup) MarshalJSON() ([]byte, error) {
+ premarshaled, err := v.__premarshalJSON()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(premarshaled)
+}
+
+func (v *DeleteGroupRemoveGroupRemoveGroupMutationGroup) __premarshalJSON() (*__premarshalDeleteGroupRemoveGroupRemoveGroupMutationGroup, error) {
+ var retval __premarshalDeleteGroupRemoveGroupRemoveGroupMutationGroup
+
+ retval.Id = v.GroupDetails.Id
+ retval.DisplayName = v.GroupDetails.DisplayName
+ retval.LookupName = v.GroupDetails.LookupName
+ return &retval, nil
+}
+
+// DeleteGroupResponse is returned by DeleteGroup on success.
+type DeleteGroupResponse struct {
+ // Removes a group. Only usable if roles are not managed externally, e.g. in LDAP.
+ // Stability: Long-term
+ RemoveGroup DeleteGroupRemoveGroupRemoveGroupMutation `json:"removeGroup"`
+}
+
+// GetRemoveGroup returns DeleteGroupResponse.RemoveGroup, and is useful for accessing the field via an interface.
+func (v *DeleteGroupResponse) GetRemoveGroup() DeleteGroupRemoveGroupRemoveGroupMutation {
+ return v.RemoveGroup
+}
+
// DeleteParserByIDDeleteParserBooleanResultType includes the requested fields of the GraphQL type BooleanResultType.
type DeleteParserByIDDeleteParserBooleanResultType struct {
Typename *string `json:"__typename"`
@@ -5901,6 +6087,89 @@ func (v *GetFilterAlertByIDSearchDomainView) GetFilterAlert() GetFilterAlertByID
return v.FilterAlert
}
+// GetGroupByDisplayNameGroupByDisplayNameGroup includes the requested fields of the GraphQL type Group.
+// The GraphQL type's documentation follows.
+//
+// A group.
+type GetGroupByDisplayNameGroupByDisplayNameGroup struct {
+ GroupDetails `json:"-"`
+}
+
+// GetId returns GetGroupByDisplayNameGroupByDisplayNameGroup.Id, and is useful for accessing the field via an interface.
+func (v *GetGroupByDisplayNameGroupByDisplayNameGroup) GetId() string { return v.GroupDetails.Id }
+
+// GetDisplayName returns GetGroupByDisplayNameGroupByDisplayNameGroup.DisplayName, and is useful for accessing the field via an interface.
+func (v *GetGroupByDisplayNameGroupByDisplayNameGroup) GetDisplayName() string {
+ return v.GroupDetails.DisplayName
+}
+
+// GetLookupName returns GetGroupByDisplayNameGroupByDisplayNameGroup.LookupName, and is useful for accessing the field via an interface.
+func (v *GetGroupByDisplayNameGroupByDisplayNameGroup) GetLookupName() *string {
+ return v.GroupDetails.LookupName
+}
+
+func (v *GetGroupByDisplayNameGroupByDisplayNameGroup) UnmarshalJSON(b []byte) error {
+
+ if string(b) == "null" {
+ return nil
+ }
+
+ var firstPass struct {
+ *GetGroupByDisplayNameGroupByDisplayNameGroup
+ graphql.NoUnmarshalJSON
+ }
+ firstPass.GetGroupByDisplayNameGroupByDisplayNameGroup = v
+
+ err := json.Unmarshal(b, &firstPass)
+ if err != nil {
+ return err
+ }
+
+ err = json.Unmarshal(
+ b, &v.GroupDetails)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+type __premarshalGetGroupByDisplayNameGroupByDisplayNameGroup struct {
+ Id string `json:"id"`
+
+ DisplayName string `json:"displayName"`
+
+ LookupName *string `json:"lookupName"`
+}
+
+func (v *GetGroupByDisplayNameGroupByDisplayNameGroup) MarshalJSON() ([]byte, error) {
+ premarshaled, err := v.__premarshalJSON()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(premarshaled)
+}
+
+func (v *GetGroupByDisplayNameGroupByDisplayNameGroup) __premarshalJSON() (*__premarshalGetGroupByDisplayNameGroupByDisplayNameGroup, error) {
+ var retval __premarshalGetGroupByDisplayNameGroupByDisplayNameGroup
+
+ retval.Id = v.GroupDetails.Id
+ retval.DisplayName = v.GroupDetails.DisplayName
+ retval.LookupName = v.GroupDetails.LookupName
+ return &retval, nil
+}
+
+// GetGroupByDisplayNameResponse is returned by GetGroupByDisplayName on success.
+type GetGroupByDisplayNameResponse struct {
+ // Used to get information on groups by a given display name.
+ // Stability: Long-term
+ GroupByDisplayName GetGroupByDisplayNameGroupByDisplayNameGroup `json:"groupByDisplayName"`
+}
+
+// GetGroupByDisplayName returns GetGroupByDisplayNameResponse.GroupByDisplayName, and is useful for accessing the field via an interface.
+func (v *GetGroupByDisplayNameResponse) GetGroupByDisplayName() GetGroupByDisplayNameGroupByDisplayNameGroup {
+ return v.GroupByDisplayName
+}
+
// GetLicenseInstalledLicense includes the requested fields of the GraphQL interface License.
//
// GetLicenseInstalledLicense is implemented by the following types:
@@ -7064,6 +7333,28 @@ func (v *GetUsersByUsernameUsersUser) __premarshalJSON() (*__premarshalGetUsersB
return &retval, nil
}
+// GroupDetails includes the GraphQL fields of Group requested by the fragment GroupDetails.
+// The GraphQL type's documentation follows.
+//
+// A group.
+type GroupDetails struct {
+ // Stability: Long-term
+ Id string `json:"id"`
+ // Stability: Long-term
+ DisplayName string `json:"displayName"`
+ // Stability: Long-term
+ LookupName *string `json:"lookupName"`
+}
+
+// GetId returns GroupDetails.Id, and is useful for accessing the field via an interface.
+func (v *GroupDetails) GetId() string { return v.Id }
+
+// GetDisplayName returns GroupDetails.DisplayName, and is useful for accessing the field via an interface.
+func (v *GroupDetails) GetDisplayName() string { return v.DisplayName }
+
+// GetLookupName returns GroupDetails.LookupName, and is useful for accessing the field via an interface.
+func (v *GroupDetails) GetLookupName() *string { return v.LookupName }
+
// Http(s) Header entry.
type HttpHeaderEntryInput struct {
// Http(s) Header entry.
@@ -13183,6 +13474,100 @@ func (v *UpdateFilterAlertUpdateFilterAlert) __premarshalJSON() (*__premarshalUp
return &retval, nil
}
+// UpdateGroupResponse is returned by UpdateGroup on success.
+type UpdateGroupResponse struct {
+ // Updates the group.
+ // Stability: Long-term
+ UpdateGroup UpdateGroupUpdateGroupUpdateGroupMutation `json:"updateGroup"`
+}
+
+// GetUpdateGroup returns UpdateGroupResponse.UpdateGroup, and is useful for accessing the field via an interface.
+func (v *UpdateGroupResponse) GetUpdateGroup() UpdateGroupUpdateGroupUpdateGroupMutation {
+ return v.UpdateGroup
+}
+
+// UpdateGroupUpdateGroupUpdateGroupMutation includes the requested fields of the GraphQL type UpdateGroupMutation.
+type UpdateGroupUpdateGroupUpdateGroupMutation struct {
+ // Stability: Long-term
+ Group UpdateGroupUpdateGroupUpdateGroupMutationGroup `json:"group"`
+}
+
+// GetGroup returns UpdateGroupUpdateGroupUpdateGroupMutation.Group, and is useful for accessing the field via an interface.
+func (v *UpdateGroupUpdateGroupUpdateGroupMutation) GetGroup() UpdateGroupUpdateGroupUpdateGroupMutationGroup {
+ return v.Group
+}
+
+// UpdateGroupUpdateGroupUpdateGroupMutationGroup includes the requested fields of the GraphQL type Group.
+// The GraphQL type's documentation follows.
+//
+// A group.
+type UpdateGroupUpdateGroupUpdateGroupMutationGroup struct {
+ GroupDetails `json:"-"`
+}
+
+// GetId returns UpdateGroupUpdateGroupUpdateGroupMutationGroup.Id, and is useful for accessing the field via an interface.
+func (v *UpdateGroupUpdateGroupUpdateGroupMutationGroup) GetId() string { return v.GroupDetails.Id }
+
+// GetDisplayName returns UpdateGroupUpdateGroupUpdateGroupMutationGroup.DisplayName, and is useful for accessing the field via an interface.
+func (v *UpdateGroupUpdateGroupUpdateGroupMutationGroup) GetDisplayName() string {
+ return v.GroupDetails.DisplayName
+}
+
+// GetLookupName returns UpdateGroupUpdateGroupUpdateGroupMutationGroup.LookupName, and is useful for accessing the field via an interface.
+func (v *UpdateGroupUpdateGroupUpdateGroupMutationGroup) GetLookupName() *string {
+ return v.GroupDetails.LookupName
+}
+
+func (v *UpdateGroupUpdateGroupUpdateGroupMutationGroup) UnmarshalJSON(b []byte) error {
+
+ if string(b) == "null" {
+ return nil
+ }
+
+ var firstPass struct {
+ *UpdateGroupUpdateGroupUpdateGroupMutationGroup
+ graphql.NoUnmarshalJSON
+ }
+ firstPass.UpdateGroupUpdateGroupUpdateGroupMutationGroup = v
+
+ err := json.Unmarshal(b, &firstPass)
+ if err != nil {
+ return err
+ }
+
+ err = json.Unmarshal(
+ b, &v.GroupDetails)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+type __premarshalUpdateGroupUpdateGroupUpdateGroupMutationGroup struct {
+ Id string `json:"id"`
+
+ DisplayName string `json:"displayName"`
+
+ LookupName *string `json:"lookupName"`
+}
+
+func (v *UpdateGroupUpdateGroupUpdateGroupMutationGroup) MarshalJSON() ([]byte, error) {
+ premarshaled, err := v.__premarshalJSON()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(premarshaled)
+}
+
+func (v *UpdateGroupUpdateGroupUpdateGroupMutationGroup) __premarshalJSON() (*__premarshalUpdateGroupUpdateGroupUpdateGroupMutationGroup, error) {
+ var retval __premarshalUpdateGroupUpdateGroupUpdateGroupMutationGroup
+
+ retval.Id = v.GroupDetails.Id
+ retval.DisplayName = v.GroupDetails.DisplayName
+ retval.LookupName = v.GroupDetails.LookupName
+ return &retval, nil
+}
+
// UpdateHumioRepoActionResponse is returned by UpdateHumioRepoAction on success.
type UpdateHumioRepoActionResponse struct {
// Update a LogScale repository action.
@@ -14291,6 +14676,18 @@ func (v *__CreateFilterAlertInput) GetQueryOwnershipType() QueryOwnershipType {
return v.QueryOwnershipType
}
+// __CreateGroupInput is used internally by genqlient
+type __CreateGroupInput struct {
+ DisplayName string `json:"DisplayName"`
+ LookupName *string `json:"LookupName"`
+}
+
+// GetDisplayName returns __CreateGroupInput.DisplayName, and is useful for accessing the field via an interface.
+func (v *__CreateGroupInput) GetDisplayName() string { return v.DisplayName }
+
+// GetLookupName returns __CreateGroupInput.LookupName, and is useful for accessing the field via an interface.
+func (v *__CreateGroupInput) GetLookupName() *string { return v.LookupName }
+
// __CreateHumioRepoActionInput is used internally by genqlient
type __CreateHumioRepoActionInput struct {
SearchDomainName string `json:"SearchDomainName"`
@@ -14681,6 +15078,14 @@ func (v *__DeleteFilterAlertInput) GetSearchDomainName() string { return v.Searc
// GetFilterAlertID returns __DeleteFilterAlertInput.FilterAlertID, and is useful for accessing the field via an interface.
func (v *__DeleteFilterAlertInput) GetFilterAlertID() string { return v.FilterAlertID }
+// __DeleteGroupInput is used internally by genqlient
+type __DeleteGroupInput struct {
+ GroupId string `json:"GroupId"`
+}
+
+// GetGroupId returns __DeleteGroupInput.GroupId, and is useful for accessing the field via an interface.
+func (v *__DeleteGroupInput) GetGroupId() string { return v.GroupId }
+
// __DeleteParserByIDInput is used internally by genqlient
type __DeleteParserByIDInput struct {
RepositoryName string `json:"RepositoryName"`
@@ -14793,6 +15198,14 @@ func (v *__GetFilterAlertByIDInput) GetSearchDomainName() string { return v.Sear
// GetFilterAlertID returns __GetFilterAlertByIDInput.FilterAlertID, and is useful for accessing the field via an interface.
func (v *__GetFilterAlertByIDInput) GetFilterAlertID() string { return v.FilterAlertID }
+// __GetGroupByDisplayNameInput is used internally by genqlient
+type __GetGroupByDisplayNameInput struct {
+ DisplayName string `json:"DisplayName"`
+}
+
+// GetDisplayName returns __GetGroupByDisplayNameInput.DisplayName, and is useful for accessing the field via an interface.
+func (v *__GetGroupByDisplayNameInput) GetDisplayName() string { return v.DisplayName }
+
// __GetParserByIDInput is used internally by genqlient
type __GetParserByIDInput struct {
RepositoryName string `json:"RepositoryName"`
@@ -15203,6 +15616,22 @@ func (v *__UpdateFilterAlertInput) GetQueryOwnershipType() QueryOwnershipType {
return v.QueryOwnershipType
}
+// __UpdateGroupInput is used internally by genqlient
+type __UpdateGroupInput struct {
+ GroupId string `json:"GroupId"`
+ DisplayName *string `json:"DisplayName"`
+ LookupName *string `json:"LookupName"`
+}
+
+// GetGroupId returns __UpdateGroupInput.GroupId, and is useful for accessing the field via an interface.
+func (v *__UpdateGroupInput) GetGroupId() string { return v.GroupId }
+
+// GetDisplayName returns __UpdateGroupInput.DisplayName, and is useful for accessing the field via an interface.
+func (v *__UpdateGroupInput) GetDisplayName() *string { return v.DisplayName }
+
+// GetLookupName returns __UpdateGroupInput.LookupName, and is useful for accessing the field via an interface.
+func (v *__UpdateGroupInput) GetLookupName() *string { return v.LookupName }
+
// __UpdateHumioRepoActionInput is used internally by genqlient
type __UpdateHumioRepoActionInput struct {
SearchDomainName string `json:"SearchDomainName"`
@@ -15994,6 +16423,49 @@ func CreateFilterAlert(
return data_, err_
}
+// The mutation executed by CreateGroup.
+const CreateGroup_Operation = `
+mutation CreateGroup ($DisplayName: String!, $LookupName: String) {
+ addGroup(displayName: $DisplayName, lookupName: $LookupName) {
+ group {
+ ... GroupDetails
+ }
+ }
+}
+fragment GroupDetails on Group {
+ id
+ displayName
+ lookupName
+}
+`
+
+func CreateGroup(
+ ctx_ context.Context,
+ client_ graphql.Client,
+ DisplayName string,
+ LookupName *string,
+) (data_ *CreateGroupResponse, err_ error) {
+ req_ := &graphql.Request{
+ OpName: "CreateGroup",
+ Query: CreateGroup_Operation,
+ Variables: &__CreateGroupInput{
+ DisplayName: DisplayName,
+ LookupName: LookupName,
+ },
+ }
+
+ data_ = &CreateGroupResponse{}
+ resp_ := &graphql.Response{Data: data_}
+
+ err_ = client_.MakeRequest(
+ ctx_,
+ req_,
+ resp_,
+ )
+
+ return data_, err_
+}
+
// The mutation executed by CreateHumioRepoAction.
const CreateHumioRepoAction_Operation = `
mutation CreateHumioRepoAction ($SearchDomainName: String!, $ActionName: String!, $IngestToken: String!) {
@@ -16770,6 +17242,47 @@ func DeleteFilterAlert(
return data_, err_
}
+// The mutation executed by DeleteGroup.
+const DeleteGroup_Operation = `
+mutation DeleteGroup ($GroupId: String!) {
+ removeGroup(groupId: $GroupId) {
+ group {
+ ... GroupDetails
+ }
+ }
+}
+fragment GroupDetails on Group {
+ id
+ displayName
+ lookupName
+}
+`
+
+func DeleteGroup(
+ ctx_ context.Context,
+ client_ graphql.Client,
+ GroupId string,
+) (data_ *DeleteGroupResponse, err_ error) {
+ req_ := &graphql.Request{
+ OpName: "DeleteGroup",
+ Query: DeleteGroup_Operation,
+ Variables: &__DeleteGroupInput{
+ GroupId: GroupId,
+ },
+ }
+
+ data_ = &DeleteGroupResponse{}
+ resp_ := &graphql.Response{Data: data_}
+
+ err_ = client_.MakeRequest(
+ ctx_,
+ req_,
+ resp_,
+ )
+
+ return data_, err_
+}
+
// The mutation executed by DeleteParserByID.
const DeleteParserByID_Operation = `
mutation DeleteParserByID ($RepositoryName: RepoOrViewName!, $ParserID: String!) {
@@ -17371,6 +17884,45 @@ func GetFilterAlertByID(
return data_, err_
}
+// The query executed by GetGroupByDisplayName.
+const GetGroupByDisplayName_Operation = `
+query GetGroupByDisplayName ($DisplayName: String!) {
+ groupByDisplayName(displayName: $DisplayName) {
+ ... GroupDetails
+ }
+}
+fragment GroupDetails on Group {
+ id
+ displayName
+ lookupName
+}
+`
+
+func GetGroupByDisplayName(
+ ctx_ context.Context,
+ client_ graphql.Client,
+ DisplayName string,
+) (data_ *GetGroupByDisplayNameResponse, err_ error) {
+ req_ := &graphql.Request{
+ OpName: "GetGroupByDisplayName",
+ Query: GetGroupByDisplayName_Operation,
+ Variables: &__GetGroupByDisplayNameInput{
+ DisplayName: DisplayName,
+ },
+ }
+
+ data_ = &GetGroupByDisplayNameResponse{}
+ resp_ := &graphql.Response{Data: data_}
+
+ err_ = client_.MakeRequest(
+ ctx_,
+ req_,
+ resp_,
+ )
+
+ return data_, err_
+}
+
// The query executed by GetLicense.
const GetLicense_Operation = `
query GetLicense {
@@ -18870,6 +19422,51 @@ func UpdateFilterAlert(
return data_, err_
}
+// The mutation executed by UpdateGroup.
+const UpdateGroup_Operation = `
+mutation UpdateGroup ($GroupId: String!, $DisplayName: String, $LookupName: String) {
+ updateGroup(input: {groupId:$GroupId,displayName:$DisplayName,lookupName:$LookupName}) {
+ group {
+ ... GroupDetails
+ }
+ }
+}
+fragment GroupDetails on Group {
+ id
+ displayName
+ lookupName
+}
+`
+
+func UpdateGroup(
+ ctx_ context.Context,
+ client_ graphql.Client,
+ GroupId string,
+ DisplayName *string,
+ LookupName *string,
+) (data_ *UpdateGroupResponse, err_ error) {
+ req_ := &graphql.Request{
+ OpName: "UpdateGroup",
+ Query: UpdateGroup_Operation,
+ Variables: &__UpdateGroupInput{
+ GroupId: GroupId,
+ DisplayName: DisplayName,
+ LookupName: LookupName,
+ },
+ }
+
+ data_ = &UpdateGroupResponse{}
+ resp_ := &graphql.Response{Data: data_}
+
+ err_ = client_.MakeRequest(
+ ctx_,
+ req_,
+ resp_,
+ )
+
+ return data_, err_
+}
+
// The mutation executed by UpdateHumioRepoAction.
const UpdateHumioRepoAction_Operation = `
mutation UpdateHumioRepoAction ($SearchDomainName: String!, $ActionID: String!, $ActionName: String!, $IngestToken: String!) {
diff --git a/internal/controller/humiogroup_controller.go b/internal/controller/humiogroup_controller.go
new file mode 100644
index 000000000..960c5d048
--- /dev/null
+++ b/internal/controller/humiogroup_controller.go
@@ -0,0 +1,185 @@
+package controller
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/go-logr/logr"
+ "github.com/google/go-cmp/cmp"
+ humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1"
+ humioapi "github.com/humio/humio-operator/internal/api"
+ "github.com/humio/humio-operator/internal/api/humiographql"
+ "github.com/humio/humio-operator/internal/helpers"
+ "github.com/humio/humio-operator/internal/humio"
+ "github.com/humio/humio-operator/internal/kubernetes"
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+// HumioGroupReconciler reconciles a HumioGroup object
+type HumioGroupReconciler struct {
+ client.Client
+ CommonConfig
+ BaseLogger logr.Logger
+ Log logr.Logger
+ HumioClient humio.Client
+ Namespace string
+}
+
+// +kubebuilder:rbac:groups=core.humio.com,resources=humiogroups,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=core.humio.com,resources=humiogroups/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=core.humio.com,resources=humiogroups/finalizers,verbs=update
+
+func (r *HumioGroupReconciler) 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 HumioGroup")
+
+ // Fetch the HumioGroup instance
+ hg := &humiov1alpha1.HumioGroup{}
+ err := r.Get(ctx, req.NamespacedName, hg)
+ 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
+ }
+
+ r.Log = r.Log.WithValues("Request.UID", hg.UID)
+
+ cluster, err := helpers.NewCluster(ctx, r, hg.Spec.ManagedClusterName, hg.Spec.ExternalClusterName, hg.Namespace, helpers.UseCertManager(), true, false)
+ if err != nil || cluster == nil || cluster.Config() == nil {
+ setStateErr := r.setState(ctx, humiov1alpha1.HumioGroupStateConfigError, hg)
+ if setStateErr != nil {
+ return reconcile.Result{}, r.logErrorAndReturn(setStateErr, "unable to set cluster state")
+ }
+ return reconcile.Result{RequeueAfter: 5 * time.Second}, r.logErrorAndReturn(err, "unable to obtain humio client config")
+ }
+ humioHttpClient := r.HumioClient.GetHumioHttpClient(cluster.Config(), req)
+
+ // delete
+ r.Log.Info("checking if group is marked to be deleted")
+ isMarkedForDeletion := hg.GetDeletionTimestamp() != nil
+ if isMarkedForDeletion {
+ r.Log.Info("group marked to be deleted")
+ if helpers.ContainsElement(hg.GetFinalizers(), humioFinalizer) {
+ _, err := r.HumioClient.GetGroup(ctx, humioHttpClient, hg)
+ if errors.As(err, &humioapi.EntityNotFound{}) {
+ hg.SetFinalizers(helpers.RemoveElement(hg.GetFinalizers(), humioFinalizer))
+ err := r.Update(ctx, hg)
+ if err != nil {
+ return reconcile.Result{}, err
+ }
+ r.Log.Info("Finalizer removed successfully")
+ return reconcile.Result{Requeue: true}, nil
+ }
+
+ // 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("Deleting Group")
+ if err := r.HumioClient.DeleteGroup(ctx, humioHttpClient, hg); err != nil {
+ return reconcile.Result{}, r.logErrorAndReturn(err, "Delete group returned error")
+ }
+ }
+ return reconcile.Result{}, nil
+ }
+
+ // Add finalizer for this CR
+ if !helpers.ContainsElement(hg.GetFinalizers(), humioFinalizer) {
+ r.Log.Info("Finalizer not present, adding finalizer to group")
+ hg.SetFinalizers(append(hg.GetFinalizers(), humioFinalizer))
+ err := r.Update(ctx, hg)
+ if err != nil {
+ return reconcile.Result{}, err
+ }
+ }
+ defer func(ctx context.Context, hg *humiov1alpha1.HumioGroup) {
+ _, err := r.HumioClient.GetGroup(ctx, humioHttpClient, hg)
+ if errors.As(err, &humioapi.EntityNotFound{}) {
+ _ = r.setState(ctx, humiov1alpha1.HumioGroupStateNotFound, hg)
+ return
+ }
+ if err != nil {
+ _ = r.setState(ctx, humiov1alpha1.HumioGroupStateUnknown, hg)
+ return
+ }
+ _ = r.setState(ctx, humiov1alpha1.HumioGroupStateExists, hg)
+ }(ctx, hg)
+
+ r.Log.Info("get current group")
+ curGroup, err := r.HumioClient.GetGroup(ctx, humioHttpClient, hg)
+ if err != nil {
+ if errors.As(err, &humioapi.EntityNotFound{}) {
+ r.Log.Info("Group doesn't exist. Now adding group")
+ addErr := r.HumioClient.AddGroup(ctx, humioHttpClient, hg)
+ if addErr != nil {
+ return reconcile.Result{}, r.logErrorAndReturn(addErr, "could not create group")
+ }
+ r.Log.Info("created group", "GroupName", hg.Spec.Name)
+ return reconcile.Result{Requeue: true}, nil
+ }
+ return reconcile.Result{}, r.logErrorAndReturn(err, "could not check if group exists")
+ }
+
+ if asExpected, diffKeysAndValues := groupAlreadyAsExpected(hg, curGroup); !asExpected {
+ r.Log.Info("information differs, triggering update",
+ "diff", diffKeysAndValues,
+ )
+ updateErr := r.HumioClient.UpdateGroup(ctx, humioHttpClient, hg)
+ if updateErr != nil {
+ return reconcile.Result{}, r.logErrorAndReturn(updateErr, "could not update group")
+ }
+ }
+
+ r.Log.Info("done reconciling, will requeue", "requeuePeriod", r.RequeuePeriod.String())
+ return reconcile.Result{RequeueAfter: r.RequeuePeriod}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *HumioGroupReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&humiov1alpha1.HumioGroup{}).
+ Named("humiogroup").
+ Complete(r)
+}
+
+func (r *HumioGroupReconciler) setState(ctx context.Context, state string, hg *humiov1alpha1.HumioGroup) error {
+ if hg.Status.State == state {
+ return nil
+ }
+ r.Log.Info(fmt.Sprintf("setting group state to %s", state))
+ hg.Status.State = state
+ return r.Status().Update(ctx, hg)
+}
+
+func (r *HumioGroupReconciler) logErrorAndReturn(err error, msg string) error {
+ r.Log.Error(err, msg)
+ return fmt.Errorf("%s: %w", msg, err)
+}
+
+// groupAlreadyAsExpected compares the group from the custom resource with the group from the GraphQL API.
+// It returns a boolean indicating if the details from GraphQL already matches what is in the desired state of the custom resource.
+// If they do not match, a map is returned with details on what the diff is.
+func groupAlreadyAsExpected(fromKubernetesCustomResource *humiov1alpha1.HumioGroup, fromGraphQL *humiographql.GroupDetails) (bool, map[string]string) {
+ keyValues := map[string]string{}
+
+ if diff := cmp.Diff(fromGraphQL.GetLookupName(), fromKubernetesCustomResource.Spec.ExternalMappingName); diff != "" {
+ keyValues["externalMappingName"] = diff
+ }
+
+ return len(keyValues) == 0, keyValues
+}
diff --git a/internal/controller/humioview_controller.go b/internal/controller/humioview_controller.go
index 1f7af68eb..77144f34b 100644
--- a/internal/controller/humioview_controller.go
+++ b/internal/controller/humioview_controller.go
@@ -79,7 +79,7 @@ func (r *HumioViewReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
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)
+ setStateErr := r.setState(ctx, humiov1alpha1.HumioViewStateConfigError, hv)
if setStateErr != nil {
return reconcile.Result{}, r.logErrorAndReturn(setStateErr, "unable to set cluster state")
}
diff --git a/internal/controller/suite/resources/humioresources_controller_test.go b/internal/controller/suite/resources/humioresources_controller_test.go
index 6502ffa50..c3b85a5db 100644
--- a/internal/controller/suite/resources/humioresources_controller_test.go
+++ b/internal/controller/suite/resources/humioresources_controller_test.go
@@ -3886,6 +3886,111 @@ var _ = Describe("Humio Resources Controllers", func() {
})
+ Context("HumioGroup", Label("envtest", "dummy", "real"), func() {
+ It("Should successfully create, update and delete group with valid configuration", func() {
+ ctx := context.Background()
+ key := types.NamespacedName{
+ Name: "humio-group",
+ Namespace: clusterKey.Namespace,
+ }
+ toCreateGroup := &humiov1alpha1.HumioGroup{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: key.Name,
+ Namespace: key.Namespace,
+ },
+ Spec: humiov1alpha1.HumioGroupSpec{
+ ManagedClusterName: clusterKey.Name,
+ Name: "example-group",
+ ExternalMappingName: nil, // default, empty value
+ },
+ }
+ humioHttpClient := humioClient.GetHumioHttpClient(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey})
+
+ suite.UsingClusterBy(clusterKey.Name, "Confirming the group does not exist in LogScale before we start")
+ Eventually(func() error {
+ _, err := humioClient.GetGroup(ctx, humioHttpClient, toCreateGroup)
+ return err
+ }, testTimeout, suite.TestInterval).ShouldNot(Succeed())
+
+ suite.UsingClusterBy(clusterKey.Name, "Creating the group custom resource")
+ Expect(k8sClient.Create(ctx, toCreateGroup)).Should(Succeed())
+
+ suite.UsingClusterBy(clusterKey.Name, "Custom resource for group should be marked with Exists")
+ Eventually(func() string {
+ updatedHumioGroup := humiov1alpha1.HumioGroup{}
+ err = k8sClient.Get(ctx, key, &updatedHumioGroup)
+ if err != nil {
+ return err.Error()
+ }
+ return updatedHumioGroup.Status.State
+ }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioGroupStateExists))
+
+ suite.UsingClusterBy(clusterKey.Name, "Confirming the group does exist in LogScale after custom resource indicates that it does")
+ var fetchedGroupDetails *humiographql.GroupDetails
+ Eventually(func() error {
+ fetchedGroupDetails, err = humioClient.GetGroup(ctx, humioHttpClient, toCreateGroup)
+ return err
+ }, testTimeout, suite.TestInterval).Should(Succeed())
+ Expect(fetchedGroupDetails.LookupName).Should(Equal(toCreateGroup.Spec.ExternalMappingName))
+
+ suite.UsingClusterBy(clusterKey.Name, "Set lookup name to custom resource using k8sClient")
+ newExternalMappingName := "some-ad-group"
+ Eventually(func() error {
+ updatedHumioGroup := humiov1alpha1.HumioGroup{}
+ err = k8sClient.Get(ctx, key, &updatedHumioGroup)
+ if err != nil {
+ return err
+ }
+ updatedHumioGroup.Spec.ExternalMappingName = &newExternalMappingName
+ return k8sClient.Update(ctx, &updatedHumioGroup)
+ }, testTimeout, suite.TestInterval).Should(Succeed())
+
+ suite.UsingClusterBy(clusterKey.Name, "verify it was updated according to humioClient")
+ Eventually(func() (*string, error) {
+ fetchedGroupDetails, err = humioClient.GetGroup(ctx, humioHttpClient, toCreateGroup)
+ if err != nil {
+ return nil, err
+ }
+ Expect(fetchedGroupDetails).ToNot(BeNil())
+ return fetchedGroupDetails.LookupName, err
+ }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(&newExternalMappingName))
+
+ suite.UsingClusterBy(clusterKey.Name, "Remove lookup name to custom resource using k8sClient")
+ Eventually(func() error {
+ updatedHumioGroup := humiov1alpha1.HumioGroup{}
+ err = k8sClient.Get(ctx, key, &updatedHumioGroup)
+ if err != nil {
+ return err
+ }
+ updatedHumioGroup.Spec.ExternalMappingName = nil
+ return k8sClient.Update(ctx, &updatedHumioGroup)
+ }, testTimeout, suite.TestInterval).Should(Succeed())
+
+ suite.UsingClusterBy(clusterKey.Name, "verify it was updated according to humioClient")
+ Eventually(func() (*string, error) {
+ fetchedGroupDetails, err = humioClient.GetGroup(ctx, humioHttpClient, toCreateGroup)
+ if err != nil {
+ return nil, err
+ }
+ Expect(fetchedGroupDetails).ToNot(BeNil())
+ return fetchedGroupDetails.LookupName, err
+ }, testTimeout, suite.TestInterval).Should(BeNil())
+
+ suite.UsingClusterBy(clusterKey.Name, "Delete custom resource using k8sClient")
+ Expect(k8sClient.Delete(ctx, toCreateGroup)).To(Succeed())
+ Eventually(func() bool {
+ err := k8sClient.Get(ctx, key, toCreateGroup)
+ return k8serrors.IsNotFound(err)
+ }, testTimeout, suite.TestInterval).Should(BeTrue())
+
+ suite.UsingClusterBy(clusterKey.Name, "Verify group was removed using humioClient")
+ Eventually(func() string {
+ fetchedGroupDetails, err = humioClient.GetGroup(ctx, humioHttpClient, toCreateGroup)
+ return err.Error()
+ }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(humioapi.GroupNotFound(toCreateGroup.Spec.Name).Error()))
+ })
+ })
+
Context("Humio User", Label("envtest", "dummy", "real"), func() {
It("HumioUser: Should handle user correctly", func() {
ctx := context.Background()
@@ -4029,7 +4134,7 @@ var _ = Describe("Humio Resources Controllers", func() {
}
// Verify we validate this for all our CRD's
- Expect(resources).To(HaveLen(17)) // Bump this as we introduce new CRD's
+ Expect(resources).To(HaveLen(18)) // Bump this as we introduce new CRD's
for i := range resources {
// Get the GVK information
diff --git a/internal/controller/suite/resources/suite_test.go b/internal/controller/suite/resources/suite_test.go
index a8609d667..56363681f 100644
--- a/internal/controller/suite/resources/suite_test.go
+++ b/internal/controller/suite/resources/suite_test.go
@@ -337,6 +337,17 @@ var _ = BeforeSuite(func() {
}).SetupWithManager(k8sManager)
Expect(err).NotTo(HaveOccurred())
+ err = (&controller.HumioGroupReconciler{
+ Client: k8sManager.GetClient(),
+ CommonConfig: controller.CommonConfig{
+ RequeuePeriod: requeuePeriod,
+ },
+ HumioClient: humioClient,
+ BaseLogger: log,
+ Namespace: clusterKey.Namespace,
+ }).SetupWithManager(k8sManager)
+ Expect(err).NotTo(HaveOccurred())
+
ctx, cancel = context.WithCancel(context.TODO())
go func() {
diff --git a/internal/humio/client.go b/internal/humio/client.go
index debb03fe3..97ceb7073 100644
--- a/internal/humio/client.go
+++ b/internal/humio/client.go
@@ -43,6 +43,7 @@ type Client interface {
ParsersClient
RepositoriesClient
ViewsClient
+ GroupsClient
LicenseClient
ActionsClient
AlertsClient
@@ -96,6 +97,13 @@ type ViewsClient interface {
DeleteView(context.Context, *humioapi.Client, reconcile.Request, *humiov1alpha1.HumioView) error
}
+type GroupsClient interface {
+ AddGroup(context.Context, *humioapi.Client, *humiov1alpha1.HumioGroup) error
+ GetGroup(context.Context, *humioapi.Client, *humiov1alpha1.HumioGroup) (*humiographql.GroupDetails, error)
+ UpdateGroup(context.Context, *humioapi.Client, *humiov1alpha1.HumioGroup) error
+ DeleteGroup(context.Context, *humioapi.Client, *humiov1alpha1.HumioGroup) error
+}
+
type ActionsClient interface {
AddAction(context.Context, *humioapi.Client, reconcile.Request, *humiov1alpha1.HumioAction) error
GetAction(context.Context, *humioapi.Client, reconcile.Request, *humiov1alpha1.HumioAction) (humiographql.ActionDetails, error)
@@ -823,6 +831,73 @@ func validateSearchDomain(ctx context.Context, client *humioapi.Client, searchDo
return humioapi.SearchDomainNotFound(searchDomainName)
}
+func (h *ClientConfig) AddGroup(ctx context.Context, client *humioapi.Client, hg *humiov1alpha1.HumioGroup) error {
+ _, err := humiographql.CreateGroup(
+ ctx,
+ client,
+ hg.Spec.Name,
+ hg.Spec.ExternalMappingName,
+ )
+ return err
+}
+
+func (h *ClientConfig) GetGroup(ctx context.Context, client *humioapi.Client, hg *humiov1alpha1.HumioGroup) (*humiographql.GroupDetails, error) {
+ getGroupResp, err := humiographql.GetGroupByDisplayName(
+ ctx,
+ client,
+ hg.Spec.Name,
+ )
+ if err != nil {
+ return nil, humioapi.GroupNotFound(hg.Spec.Name)
+ }
+
+ group := getGroupResp.GetGroupByDisplayName()
+ return &humiographql.GroupDetails{
+ Id: group.GetId(),
+ DisplayName: group.GetDisplayName(),
+ LookupName: group.GetLookupName(),
+ }, nil
+}
+
+func (h *ClientConfig) UpdateGroup(ctx context.Context, client *humioapi.Client, hg *humiov1alpha1.HumioGroup) error {
+ curGroup, err := h.GetGroup(ctx, client, hg)
+ if err != nil {
+ return err
+ }
+
+ newLookupName := hg.Spec.ExternalMappingName
+ if hg.Spec.ExternalMappingName == nil {
+ // LogScale returns null from graphql when lookup name is updated to empty string
+ newLookupName = helpers.StringPtr("")
+ }
+
+ _, err = humiographql.UpdateGroup(
+ ctx,
+ client,
+ curGroup.GetId(),
+ &hg.Spec.Name,
+ newLookupName,
+ )
+ return err
+}
+
+func (h *ClientConfig) DeleteGroup(ctx context.Context, client *humioapi.Client, hg *humiov1alpha1.HumioGroup) error {
+ group, err := h.GetGroup(ctx, client, hg)
+ if err != nil {
+ if errors.As(err, &humioapi.EntityNotFound{}) {
+ return nil
+ }
+ return err
+ }
+
+ _, err = humiographql.DeleteGroup(
+ ctx,
+ client,
+ group.Id,
+ )
+ return err
+}
+
func (h *ClientConfig) GetAction(ctx context.Context, client *humioapi.Client, _ reconcile.Request, ha *humiov1alpha1.HumioAction) (humiographql.ActionDetails, error) {
err := validateSearchDomain(ctx, client, ha.Spec.ViewName)
if err != nil {
diff --git a/internal/humio/client_mock.go b/internal/humio/client_mock.go
index bcd55bafd..0f4133ba3 100644
--- a/internal/humio/client_mock.go
+++ b/internal/humio/client_mock.go
@@ -50,6 +50,7 @@ type ClientMock struct {
LicenseUID map[resourceKey]string
Repository map[resourceKey]humiographql.RepositoryDetails
View map[resourceKey]humiographql.GetSearchDomainSearchDomainView
+ Group map[resourceKey]humiographql.GroupDetails
IngestToken map[resourceKey]humiographql.IngestTokenDetails
Parser map[resourceKey]humiographql.ParserDetails
Action map[resourceKey]humiographql.ActionDetails
@@ -73,6 +74,7 @@ func NewMockClient() *MockClientConfig {
LicenseUID: make(map[resourceKey]string),
Repository: make(map[resourceKey]humiographql.RepositoryDetails),
View: make(map[resourceKey]humiographql.GetSearchDomainSearchDomainView),
+ Group: make(map[resourceKey]humiographql.GroupDetails),
IngestToken: make(map[resourceKey]humiographql.IngestTokenDetails),
Parser: make(map[resourceKey]humiographql.ParserDetails),
Action: make(map[resourceKey]humiographql.ActionDetails),
@@ -100,6 +102,8 @@ func (h *MockClientConfig) ClearHumioClientConnections(repoNameToKeep string) {
}
}
h.apiClient.View = make(map[resourceKey]humiographql.GetSearchDomainSearchDomainView)
+ h.apiClient.Group = make(map[resourceKey]humiographql.GroupDetails)
+ h.apiClient.Role = make(map[resourceKey]humiographql.RoleDetails)
h.apiClient.IngestToken = make(map[resourceKey]humiographql.IngestTokenDetails)
h.apiClient.Parser = make(map[resourceKey]humiographql.ParserDetails)
h.apiClient.Action = make(map[resourceKey]humiographql.ActionDetails)
@@ -110,7 +114,6 @@ func (h *MockClientConfig) ClearHumioClientConnections(repoNameToKeep string) {
h.apiClient.ScheduledSearch = make(map[resourceKey]humiographql.ScheduledSearchDetails)
h.apiClient.User = make(map[resourceKey]humiographql.UserDetails)
h.apiClient.AdminUserID = make(map[resourceKey]string)
- h.apiClient.Role = make(map[resourceKey]humiographql.RoleDetails)
}
func (h *MockClientConfig) Status(_ context.Context, _ *humioapi.Client, _ reconcile.Request) (*humioapi.StatusResponse, error) {
@@ -538,6 +541,85 @@ func (h *MockClientConfig) DeleteView(_ context.Context, _ *humioapi.Client, _ r
return nil
}
+func (h *MockClientConfig) AddGroup(_ context.Context, _ *humioapi.Client, group *humiov1alpha1.HumioGroup) error {
+ humioClientMu.Lock()
+ defer humioClientMu.Unlock()
+
+ clusterName := fmt.Sprintf("%s%s", group.Spec.ManagedClusterName, group.Spec.ExternalClusterName)
+ key := resourceKey{
+ clusterName: clusterName,
+ resourceName: group.Spec.Name,
+ }
+ if _, found := h.apiClient.Group[key]; found {
+ return fmt.Errorf("group already exists with name %s", group.Spec.Name)
+ }
+
+ value := &humiographql.GroupDetails{
+ Id: kubernetes.RandomString(),
+ DisplayName: group.Spec.Name,
+ LookupName: group.Spec.ExternalMappingName,
+ }
+
+ h.apiClient.Group[key] = *value
+ return nil
+}
+
+func (h *MockClientConfig) GetGroup(_ context.Context, _ *humioapi.Client, group *humiov1alpha1.HumioGroup) (*humiographql.GroupDetails, error) {
+ humioClientMu.Lock()
+ defer humioClientMu.Unlock()
+
+ key := resourceKey{
+ clusterName: fmt.Sprintf("%s%s", group.Spec.ManagedClusterName, group.Spec.ExternalClusterName),
+ resourceName: group.Spec.Name,
+ }
+ if value, found := h.apiClient.Group[key]; found {
+ return &value, nil
+ }
+ return nil, humioapi.GroupNotFound(group.Spec.Name)
+}
+
+func (h *MockClientConfig) UpdateGroup(_ context.Context, _ *humioapi.Client, group *humiov1alpha1.HumioGroup) error {
+ humioClientMu.Lock()
+ defer humioClientMu.Unlock()
+
+ key := resourceKey{
+ clusterName: fmt.Sprintf("%s%s", group.Spec.ManagedClusterName, group.Spec.ExternalClusterName),
+ resourceName: group.Spec.Name,
+ }
+ currentGroup, found := h.apiClient.Group[key]
+
+ if !found {
+ return humioapi.GroupNotFound(group.Spec.Name)
+ }
+
+ newLookupName := group.Spec.ExternalMappingName
+ if group.Spec.ExternalMappingName != nil && *group.Spec.ExternalMappingName == "" {
+ // LogScale returns null from graphql when lookup name is updated to empty string
+ newLookupName = nil
+ }
+
+ value := &humiographql.GroupDetails{
+ Id: currentGroup.GetId(),
+ DisplayName: group.Spec.Name,
+ LookupName: newLookupName,
+ }
+
+ h.apiClient.Group[key] = *value
+ return nil
+}
+
+func (h *MockClientConfig) DeleteGroup(_ context.Context, _ *humioapi.Client, group *humiov1alpha1.HumioGroup) error {
+ humioClientMu.Lock()
+ defer humioClientMu.Unlock()
+
+ key := resourceKey{
+ clusterName: fmt.Sprintf("%s%s", group.Spec.ManagedClusterName, group.Spec.ExternalClusterName),
+ resourceName: group.Spec.Name,
+ }
+ delete(h.apiClient.Group, key)
+ return nil
+}
+
func (h *MockClientConfig) GetLicenseUIDAndExpiry(_ context.Context, _ *humioapi.Client, req reconcile.Request) (string, time.Time, error) {
humioClientMu.Lock()
defer humioClientMu.Unlock()