diff --git a/PROJECT b/PROJECT index c863646d..331f5ee6 100644 --- a/PROJECT +++ b/PROJECT @@ -72,4 +72,13 @@ resources: kind: CustomRole path: github.com/coralogix/coralogix-operator/api/coralogix/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: coralogix.com + group: coralogix + kind: Scope + path: github.com/coralogix/coralogix-operator/api/coralogix/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/coralogix/v1alpha1/scope_types.go b/api/coralogix/v1alpha1/scope_types.go new file mode 100644 index 00000000..5c171bf4 --- /dev/null +++ b/api/coralogix/v1alpha1/scope_types.go @@ -0,0 +1,120 @@ +// Copyright 2024 Coralogix Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1 + +import ( + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + cxsdk "github.com/coralogix/coralogix-management-sdk/go" +) + +// ScopeSpec defines the desired state of Scope. +type ScopeSpec struct { + Name string `json:"name"` + + // +optional + Description *string `json:"description,omitempty"` + + Filters []ScopeFilter `json:"filters"` + + // +kubebuilder:validation:Enum=true;false + DefaultExpression string `json:"defaultExpression"` +} + +// ScopeFilter defines a filter for a scope +type ScopeFilter struct { + // +kubebuilder:validation:Enum=logs;spans;unspecified + EntityType string `json:"entityType"` + + Expression string `json:"expression"` +} + +func (s *ScopeSpec) ExtractCreateScopeRequest() (*cxsdk.CreateScopeRequest, error) { + filters, err := s.ExtractScopeFilters() + if err != nil { + return nil, err + } + + return &cxsdk.CreateScopeRequest{ + DisplayName: s.Name, + Description: s.Description, + Filters: filters, + DefaultExpression: s.DefaultExpression, + }, nil +} + +func (s *ScopeSpec) ExtractUpdateScopeRequest(id string) (*cxsdk.UpdateScopeRequest, error) { + filters, err := s.ExtractScopeFilters() + if err != nil { + return nil, err + } + + return &cxsdk.UpdateScopeRequest{ + Id: id, + DisplayName: s.Name, + Description: s.Description, + Filters: filters, + DefaultExpression: s.DefaultExpression, + }, nil +} + +func (s *ScopeSpec) ExtractScopeFilters() ([]*cxsdk.ScopeFilter, error) { + var filters []*cxsdk.ScopeFilter + for _, f := range s.Filters { + entityType, ok := cxsdk.EntityTypeValueLookup["ENTITY_TYPE_"+strings.ToUpper(f.EntityType)] + if !ok { + return nil, fmt.Errorf("invalid entity type: %s", f.EntityType) + } + filters = append(filters, &cxsdk.ScopeFilter{ + EntityType: cxsdk.EntityType(entityType), + Expression: f.Expression, + }) + } + + return filters, nil +} + +// ScopeStatus defines the observed state of Scope. +type ScopeStatus struct { + ID *string `json:"id"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Scope is the Schema for the scopes API. +type Scope struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ScopeSpec `json:"spec,omitempty"` + Status ScopeStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ScopeList contains a list of Scope. +type ScopeList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Scope `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Scope{}, &ScopeList{}) +} diff --git a/api/coralogix/v1alpha1/zz_generated.deepcopy.go b/api/coralogix/v1alpha1/zz_generated.deepcopy.go index 25dd457c..78181219 100644 --- a/api/coralogix/v1alpha1/zz_generated.deepcopy.go +++ b/api/coralogix/v1alpha1/zz_generated.deepcopy.go @@ -1845,6 +1845,125 @@ func (in *Scheduling) DeepCopy() *Scheduling { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Scope) DeepCopyInto(out *Scope) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Scope. +func (in *Scope) DeepCopy() *Scope { + if in == nil { + return nil + } + out := new(Scope) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Scope) 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 *ScopeFilter) DeepCopyInto(out *ScopeFilter) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeFilter. +func (in *ScopeFilter) DeepCopy() *ScopeFilter { + if in == nil { + return nil + } + out := new(ScopeFilter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScopeList) DeepCopyInto(out *ScopeList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Scope, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeList. +func (in *ScopeList) DeepCopy() *ScopeList { + if in == nil { + return nil + } + out := new(ScopeList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ScopeList) 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 *ScopeSpec) DeepCopyInto(out *ScopeSpec) { + *out = *in + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } + if in.Filters != nil { + in, out := &in.Filters, &out.Filters + *out = make([]ScopeFilter, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeSpec. +func (in *ScopeSpec) DeepCopy() *ScopeSpec { + if in == nil { + return nil + } + out := new(ScopeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScopeStatus) DeepCopyInto(out *ScopeStatus) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeStatus. +func (in *ScopeStatus) DeepCopy() *ScopeStatus { + if in == nil { + return nil + } + out := new(ScopeStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SendLog) DeepCopyInto(out *SendLog) { *out = *in diff --git a/charts/coralogix-operator/templates/cluster_role.yaml b/charts/coralogix-operator/templates/cluster_role.yaml index a60959ef..69a372f8 100644 --- a/charts/coralogix-operator/templates/cluster_role.yaml +++ b/charts/coralogix-operator/templates/cluster_role.yaml @@ -177,6 +177,32 @@ rules: - get - patch - update +- apiGroups: + - coralogix.com + resources: + - scopes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - coralogix.com + resources: + - scopes/finalizers + verbs: + - update +- apiGroups: + - coralogix.com + resources: + - scopes/status + verbs: + - get + - patch + - update - apiGroups: - monitoring.coreos.com resources: diff --git a/charts/coralogix-operator/templates/crds/coralogix.com_scopes.yaml b/charts/coralogix-operator/templates/crds/coralogix.com_scopes.yaml new file mode 100644 index 00000000..74fff20a --- /dev/null +++ b/charts/coralogix-operator/templates/crds/coralogix.com_scopes.yaml @@ -0,0 +1,85 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: scopes.coralogix.com +spec: + group: coralogix.com + names: + kind: Scope + listKind: ScopeList + plural: scopes + singular: scope + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Scope is the Schema for the scopes 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: ScopeSpec defines the desired state of Scope. + properties: + defaultExpression: + enum: + - true + - false + type: string + description: + type: string + filters: + items: + description: ScopeFilter defines a filter for a scope + properties: + entityType: + enum: + - logs + - spans + - unspecified + type: string + expression: + type: string + required: + - entityType + - expression + type: object + type: array + name: + type: string + required: + - defaultExpression + - filters + - name + type: object + status: + description: ScopeStatus defines the observed state of Scope. + properties: + id: + type: string + required: + - id + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/cmd/main.go b/cmd/main.go index 6c193926..2262417a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -267,6 +267,14 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "CustomRole") os.Exit(1) } + if err = (&coralogixcontrollers.ScopeReconciler{ + ScopesClient: sdkClientSet.Scopes(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Scope") + os.Exit(1) + } if prometheusRuleController { if err = (&controllers.AlertmanagerConfigReconciler{ diff --git a/config/crd/bases/coralogix.com_scopes.yaml b/config/crd/bases/coralogix.com_scopes.yaml new file mode 100644 index 00000000..74fff20a --- /dev/null +++ b/config/crd/bases/coralogix.com_scopes.yaml @@ -0,0 +1,85 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: scopes.coralogix.com +spec: + group: coralogix.com + names: + kind: Scope + listKind: ScopeList + plural: scopes + singular: scope + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Scope is the Schema for the scopes 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: ScopeSpec defines the desired state of Scope. + properties: + defaultExpression: + enum: + - true + - false + type: string + description: + type: string + filters: + items: + description: ScopeFilter defines a filter for a scope + properties: + entityType: + enum: + - logs + - spans + - unspecified + type: string + expression: + type: string + required: + - entityType + - expression + type: object + type: array + name: + type: string + required: + - defaultExpression + - filters + - name + type: object + status: + description: ScopeStatus defines the observed state of Scope. + properties: + id: + type: string + required: + - id + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 219d1b09..d7af49ce 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -8,6 +8,7 @@ resources: - bases/coralogix.com_outboundwebhooks.yaml - bases/coralogix.com_apikeys.yaml - bases/coralogix.com_customroles.yaml + - bases/coralogix.com_scopes.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -27,6 +28,7 @@ patchesStrategicMerge: #- patches/cainjection_in_outboundwebhooks.yaml #- patches/cainjection_in_coralogix_apikeys.yaml #- patches/cainjection_in_coralogix_customroles.yaml +#- patches/cainjection_in_coralogix_scopes.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/rbac/coralogix_scope_editor_role.yaml b/config/rbac/coralogix_scope_editor_role.yaml new file mode 100644 index 00000000..7749abf9 --- /dev/null +++ b/config/rbac/coralogix_scope_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit scopes. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: coralogix-operator + app.kubernetes.io/managed-by: kustomize + name: coralogix-scope-editor-role +rules: +- apiGroups: + - coralogix.com + resources: + - scopes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - coralogix.com + resources: + - scopes/status + verbs: + - get diff --git a/config/rbac/coralogix_scope_viewer_role.yaml b/config/rbac/coralogix_scope_viewer_role.yaml new file mode 100644 index 00000000..22a5f208 --- /dev/null +++ b/config/rbac/coralogix_scope_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view scopes. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: coralogix-operator + app.kubernetes.io/managed-by: kustomize + name: coralogix-scope-viewer-role +rules: +- apiGroups: + - coralogix.com + resources: + - scopes + verbs: + - get + - list + - watch +- apiGroups: + - coralogix.com + resources: + - scopes/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 0d848305..160fee0b 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -172,6 +172,32 @@ rules: - get - patch - update +- apiGroups: + - coralogix.com + resources: + - scopes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - coralogix.com + resources: + - scopes/finalizers + verbs: + - update +- apiGroups: + - coralogix.com + resources: + - scopes/status + verbs: + - get + - patch + - update - apiGroups: - monitoring.coreos.com resources: diff --git a/config/samples/scopes/example-scope.yaml b/config/samples/scopes/example-scope.yaml new file mode 100644 index 00000000..1852544e --- /dev/null +++ b/config/samples/scopes/example-scope.yaml @@ -0,0 +1,19 @@ +apiVersion: coralogix.com/v1alpha1 +kind: Scope +metadata: + labels: + app.kubernetes.io/name: coralogix-operator + app.kubernetes.io/instance: scope-sample + app.kubernetes.io/part-of: coralogix-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: coralogix-operator + name: scope-sample +spec: + name: scope-sample + description: This is a sample scope + filters: + - entityType: logs + expression: (subsystemName == 'purchases') || (subsystemName == 'signups') + - entityType: spans + expression: (subsystemName == 'clothing') || (subsystemName == 'electronics') + defaultExpression: true diff --git a/docs/api.md b/docs/api.md index be858a7c..7d8607ce 100644 --- a/docs/api.md +++ b/docs/api.md @@ -20,6 +20,8 @@ Resource Types: - [RuleGroup](#rulegroup) +- [Scope](#scope) + @@ -4226,3 +4228,169 @@ Important: Run "make" to regenerate code after modifying this file
true + +## Scope +[↩ Parent](#coralogixcomv1alpha1 ) + + + + + + +Scope is the Schema for the scopes API. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstringcoralogix.com/v1alpha1true
kindstringScopetrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject + ScopeSpec defines the desired state of Scope.
+
false
statusobject + ScopeStatus defines the observed state of Scope.
+
false
+ + +### Scope.spec +[↩ Parent](#scope) + + + +ScopeSpec defines the desired state of Scope. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
defaultExpressionenum +
+
+ Enum: true, false
+
true
filters[]object +
+
true
namestring +
+
true
descriptionstring +
+
false
+ + +### Scope.spec.filters[index] +[↩ Parent](#scopespec) + + + +ScopeFilter defines a filter for a scope + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
entityTypeenum +
+
+ Enum: logs, spans, unspecified
+
true
expressionstring +
+
true
+ + +### Scope.status +[↩ Parent](#scope) + + + +ScopeStatus defines the observed state of Scope. + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
idstring +
+
true
diff --git a/internal/controller/coralogix/scope_controller.go b/internal/controller/coralogix/scope_controller.go new file mode 100644 index 00000000..91eed02a --- /dev/null +++ b/internal/controller/coralogix/scope_controller.go @@ -0,0 +1,185 @@ +// Copyright 2024 Coralogix Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package coralogix + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/encoding/protojson" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + cxsdk "github.com/coralogix/coralogix-management-sdk/go" + + coralogixv1alpha1 "github.com/coralogix/coralogix-operator/api/coralogix/v1alpha1" +) + +// ScopeReconciler reconciles a Scope object +type ScopeReconciler struct { + client.Client + ScopesClient *cxsdk.ScopesClient + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=coralogix.com,resources=scopes,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=coralogix.com,resources=scopes/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=coralogix.com,resources=scopes/finalizers,verbs=update + +var ( + scopeFinalizerName = "scope.coralogix.com/finalizer" +) + +func (r *ScopeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx).WithValues( + "scope", req.NamespacedName.Name, + "namespace", req.NamespacedName.Namespace, + ) + + scope := &coralogixv1alpha1.Scope{} + if err := r.Get(ctx, req.NamespacedName, scope); err != nil { + if errors.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 ctrl.Result{}, nil + } + return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err + } + + if ptr.Deref(scope.Status.ID, "") == "" { + err := r.create(ctx, log, scope) + if err != nil { + log.Error(err, "Error on creating Scope") + return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err + } + return ctrl.Result{}, nil + } + + if !scope.ObjectMeta.DeletionTimestamp.IsZero() { + err := r.delete(ctx, log, scope) + if err != nil { + log.Error(err, "Error on deleting Scope") + return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err + } + return ctrl.Result{}, nil + } + + err := r.update(ctx, log, scope) + if err != nil { + log.Error(err, "Error on updating Scope") + return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err + } + + return ctrl.Result{}, nil +} + +func (r *ScopeReconciler) create(ctx context.Context, log logr.Logger, scope *coralogixv1alpha1.Scope) error { + createRequest, err := scope.Spec.ExtractCreateScopeRequest() + if err != nil { + return fmt.Errorf("error on extracting create request: %w", err) + } + log.V(1).Info("Creating remote scope", "scope", protojson.Format(createRequest)) + createResponse, err := r.ScopesClient.Create(ctx, createRequest) + if err != nil { + return fmt.Errorf("error on creating remote scope: %w", err) + } + log.V(1).Info("Remote scope created", "response", protojson.Format(createResponse)) + + scope.Status = coralogixv1alpha1.ScopeStatus{ + ID: &createResponse.Scope.Id, + } + + log.V(1).Info("Updating Scope status", "id", createResponse.Scope.Id) + if err = r.Status().Update(ctx, scope); err != nil { + if deleteErr := r.deleteRemoteScope(ctx, log, *scope.Status.ID); deleteErr != nil { + return fmt.Errorf("error to delete scope after status update error. Update error: %w. Deletion error: %w", err, deleteErr) + } + return fmt.Errorf("error to update scope status: %w", err) + } + + if !controllerutil.ContainsFinalizer(scope, scopeFinalizerName) { + log.V(1).Info("Updating Scope to add finalizer", "id", createResponse.Scope.Id) + controllerutil.AddFinalizer(scope, scopeFinalizerName) + if err := r.Update(ctx, scope); err != nil { + return fmt.Errorf("error on updating Scope: %w", err) + } + } + + return nil +} + +func (r *ScopeReconciler) update(ctx context.Context, log logr.Logger, scope *coralogixv1alpha1.Scope) error { + updateRequest, err := scope.Spec.ExtractUpdateScopeRequest(*scope.Status.ID) + if err != nil { + return fmt.Errorf("error on extracting update request: %w", err) + } + log.V(1).Info("Updating remote scope", "scope", protojson.Format(updateRequest)) + updateResponse, err := r.ScopesClient.Update(ctx, updateRequest) + if err != nil { + if cxsdk.Code(err) == codes.NotFound { + log.V(1).Info("scope not found on remote, removing id from status") + scope.Status = coralogixv1alpha1.ScopeStatus{ + ID: ptr.To(""), + } + if err = r.Status().Update(ctx, scope); err != nil { + return fmt.Errorf("error on updating Scope status: %w", err) + } + return fmt.Errorf("scope not found on remote: %w", err) + } + return fmt.Errorf("error on updating scope: %w", err) + } + log.V(1).Info("Remote scope updated", "scope", protojson.Format(updateResponse)) + + return nil +} + +func (r *ScopeReconciler) delete(ctx context.Context, log logr.Logger, scope *coralogixv1alpha1.Scope) error { + if err := r.deleteRemoteScope(ctx, log, *scope.Status.ID); err != nil { + return fmt.Errorf("error on deleting remote scope: %w", err) + } + + log.V(1).Info("Removing finalizer from Scope") + controllerutil.RemoveFinalizer(scope, scopeFinalizerName) + if err := r.Update(ctx, scope); err != nil { + return fmt.Errorf("error on updating Scope: %w", err) + } + + return nil +} + +func (r *ScopeReconciler) deleteRemoteScope(ctx context.Context, log logr.Logger, scopeID string) error { + log.V(1).Info("Deleting scope from remote", "id", scopeID) + if _, err := r.ScopesClient.Delete(ctx, &cxsdk.DeleteScopeRequest{Id: scopeID}); err != nil && cxsdk.Code(err) != codes.NotFound { + log.V(1).Error(err, "Error on deleting remote scope", "id", scopeID) + return fmt.Errorf("error to delete remote scope %s: %w", scopeID, err) + } + log.V(1).Info("scope was deleted from remote", "id", scopeID) + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ScopeReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&coralogixv1alpha1.Scope{}). + Complete(r) +} diff --git a/tests/e2e/scope_test.go b/tests/e2e/scope_test.go new file mode 100644 index 00000000..a34247e0 --- /dev/null +++ b/tests/e2e/scope_test.go @@ -0,0 +1,123 @@ +// Copyright 2024 Coralogix Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + cxsdk "github.com/coralogix/coralogix-management-sdk/go" + + coralogixv1alpha1 "github.com/coralogix/coralogix-operator/api/coralogix/v1alpha1" +) + +var _ = Describe("Scope", Ordered, func() { + var ( + crClient client.Client + scopesClient *cxsdk.ScopesClient + scopeID string + scope *coralogixv1alpha1.Scope + scopeName = "scope-sample" + ) + + BeforeEach(func() { + crClient = ClientsInstance.GetControllerRuntimeClient() + scopesClient = ClientsInstance.GetCoralogixClientSet().Scopes() + scope = &coralogixv1alpha1.Scope{ + ObjectMeta: metav1.ObjectMeta{ + Name: scopeName, + Namespace: testNamespace, + }, + Spec: coralogixv1alpha1.ScopeSpec{ + Name: scopeName, + Description: ptr.To("This is a sample scope"), + Filters: []coralogixv1alpha1.ScopeFilter{ + { + EntityType: "logs", + Expression: "(subsystemName == 'purchases') || (subsystemName == 'signups')", + }, + { + EntityType: "spans", + Expression: "(subsystemName == 'clothing') || (subsystemName == 'electronics')", + }, + }, + DefaultExpression: "true", + }, + } + }) + + It("Should be created successfully", func(ctx context.Context) { + By("Creating Scope") + Expect(crClient.Create(ctx, scope)).To(Succeed()) + + By("Fetching the Scope ID") + fetchedScope := &coralogixv1alpha1.Scope{} + Eventually(func(g Gomega) error { + g.Expect(crClient.Get(ctx, types.NamespacedName{Name: scopeName, Namespace: testNamespace}, fetchedScope)).To(Succeed()) + if fetchedScope.Status.ID != nil { + scopeID = *fetchedScope.Status.ID + return nil + } + return fmt.Errorf("scope ID is not set") + }, time.Minute, time.Second).Should(Succeed()) + + By("Verifying Scope exists in Coralogix backend") + Eventually(func() error { + _, err := scopesClient.Get(ctx, &cxsdk.GetTeamScopesByIDsRequest{ + Ids: []string{scopeID}, + }) + return err + }, time.Minute, time.Second).Should(Succeed()) + }) + + It("Should be updated successfully", func(ctx context.Context) { + By("Patching the Scope") + newScopeName := "scope-sample-updated" + modifiedScope := scope.DeepCopy() + modifiedScope.Spec.Name = newScopeName + Expect(crClient.Patch(ctx, modifiedScope, client.MergeFrom(scope))).To(Succeed()) + + By("Verifying Scope is updated in Coralogix backend") + Eventually(func() string { + getScopeRes, err := scopesClient.Get(ctx, &cxsdk.GetTeamScopesByIDsRequest{ + Ids: []string{scopeID}, + }) + Expect(err).ToNot(HaveOccurred()) + return getScopeRes.Scopes[0].DisplayName + }, time.Minute, time.Second).Should(Equal(newScopeName)) + }) + + It("Should be deleted successfully", func(ctx context.Context) { + By("Deleting the Scope") + Expect(crClient.Delete(ctx, scope)).To(Succeed()) + + By("Verifying Scope is deleted from Coralogix backend") + Eventually(func() int { + getScopeRes, err := scopesClient.Get(ctx, &cxsdk.GetTeamScopesByIDsRequest{ + Ids: []string{scopeID}, + }) + Expect(err).ToNot(HaveOccurred()) + return len(getScopeRes.Scopes) + }, time.Minute, time.Second).Should(Equal(0)) + }) +})