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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstringcore.humio.com/v1alpha1true
kindstringHumioGrouptrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject + 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
    statusobject + HumioGroupStatus defines the observed state of HumioGroup.
    +
    false
    + + +### HumioGroup.spec +[↩ Parent](#humiogroup) + + + +HumioGroupSpec defines the desired state of HumioGroup. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    namestring + Name is the display name of the HumioGroup
    +
    + Validations:
  • self == oldSelf: Value is immutable
  • +
    true
    externalClusterNamestring + ExternalClusterName refers to an object of type HumioExternalCluster where the Humio resources should be created. +This conflicts with ManagedClusterName.
    +
    false
    externalMappingNamestring + ExternalMappingName is the mapping name from the external provider that will assign the user to this HumioGroup
    +
    false
    managedClusterNamestring + 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. + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    statestring + 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()