diff --git a/docs/data-sources/view.md b/docs/data-sources/view.md new file mode 100644 index 000000000..aa03569c2 --- /dev/null +++ b/docs/data-sources/view.md @@ -0,0 +1,77 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coralogix_view Data Source - terraform-provider-coralogix" +subcategory: "" +description: |- + +--- + +# coralogix_view (Data Source) + + + + + + +## Schema + +### Required + +- `id` (String) id + +### Read-Only + +- `filters` (Attributes) (see [below for nested schema](#nestedatt--filters)) +- `folder_id` (String) Unique identifier for folders +- `name` (String) View name +- `search_query` (Attributes) (see [below for nested schema](#nestedatt--search_query)) +- `time_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection)) + + +### Nested Schema for `filters` + +Read-Only: + +- `filters` (Attributes List) (see [below for nested schema](#nestedatt--filters--filters)) + + +### Nested Schema for `filters.filters` + +Read-Only: + +- `name` (String) Filter name +- `selected_values` (Map of Boolean) Filter selected values + + + + +### Nested Schema for `search_query` + +Read-Only: + +- `query` (String) + + + +### Nested Schema for `time_selection` + +Read-Only: + +- `custom_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection--custom_selection)) +- `quick_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection--quick_selection)) + + +### Nested Schema for `time_selection.custom_selection` + +Read-Only: + +- `from_time` (String) +- `to_time` (String) + + + +### Nested Schema for `time_selection.quick_selection` + +Read-Only: + +- `seconds` (Number) Folder name diff --git a/docs/data-sources/views_folder.md b/docs/data-sources/views_folder.md new file mode 100644 index 000000000..aff8d4ee2 --- /dev/null +++ b/docs/data-sources/views_folder.md @@ -0,0 +1,24 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coralogix_views_folder Data Source - terraform-provider-coralogix" +subcategory: "" +description: |- + +--- + +# coralogix_views_folder (Data Source) + + + + + + +## Schema + +### Required + +- `id` (String) id + +### Read-Only + +- `name` (String) Name of the views-folder diff --git a/docs/resources/view.md b/docs/resources/view.md new file mode 100644 index 000000000..f03f867bd --- /dev/null +++ b/docs/resources/view.md @@ -0,0 +1,81 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coralogix_view Resource - terraform-provider-coralogix" +subcategory: "" +description: |- + +--- + +# coralogix_view (Resource) + + + + + + +## Schema + +### Required + +- `name` (String) View name +- `time_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection)) + +### Optional + +- `filters` (Attributes) (see [below for nested schema](#nestedatt--filters)) +- `folder_id` (String) Unique identifier for folders +- `search_query` (Attributes) (see [below for nested schema](#nestedatt--search_query)) + +### Read-Only + +- `id` (String) id + + +### Nested Schema for `time_selection` + +Optional: + +- `custom_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection--custom_selection)) +- `quick_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection--quick_selection)) + + +### Nested Schema for `time_selection.custom_selection` + +Required: + +- `from_time` (String) +- `to_time` (String) + + + +### Nested Schema for `time_selection.quick_selection` + +Required: + +- `seconds` (Number) Folder name + + + + +### Nested Schema for `filters` + +Optional: + +- `filters` (Attributes List) (see [below for nested schema](#nestedatt--filters--filters)) + + +### Nested Schema for `filters.filters` + +Required: + +- `name` (String) Filter name +- `selected_values` (Map of Boolean) Filter selected values + + + + +### Nested Schema for `search_query` + +Required: + +- `query` (String) diff --git a/docs/resources/views_folder.md b/docs/resources/views_folder.md new file mode 100644 index 000000000..bb4058963 --- /dev/null +++ b/docs/resources/views_folder.md @@ -0,0 +1,24 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coralogix_views_folder Resource - terraform-provider-coralogix" +subcategory: "" +description: |- + +--- + +# coralogix_views_folder (Resource) + + + + + + +## Schema + +### Required + +- `name` (String) Name of the views-folder + +### Read-Only + +- `id` (String) id diff --git a/examples/resources/coralogix_view/main.tf b/examples/resources/coralogix_view/main.tf new file mode 100644 index 000000000..93e4376fa --- /dev/null +++ b/examples/resources/coralogix_view/main.tf @@ -0,0 +1,44 @@ +terraform { + required_providers { + coralogix = { + version = "~> 2.0" + source = "coralogix/coralogix" + } + } +} + +provider "coralogix" { + #api_key = "" + #env = "" +} + +resource "coralogix_view" "example_view" { + name = "Example View" + time_selection = { + custom_selection = { + from_time = "2023-01-01T00:00:00Z" + to_time = "2023-01-02T00:00:00Z" + } + } + search_query = { + query = "error OR warning" + } + filters = { + filters = [ + { + name = "severity" + selected_values = { + "ERROR" = true + "WARNING" = true + } + }, + { + name = "application" + selected_values = { + "my-app" = true + "another-app" = true + } + } + ] + } +} \ No newline at end of file diff --git a/examples/resources/coralogix_views_folder/main.tf b/examples/resources/coralogix_views_folder/main.tf new file mode 100644 index 000000000..dcd3ee6d9 --- /dev/null +++ b/examples/resources/coralogix_views_folder/main.tf @@ -0,0 +1,27 @@ +terraform { + required_providers { + coralogix = { + version = "~> 2.0" + source = "coralogix/coralogix" + } + } +} + +provider "coralogix" { + #api_key = "" + #env = "" +} + +resource "coralogix_views_folder" "example_view_folder" { + name = "Example View Folder" +} + +resource "coralogix_view" "example_view" { + name = "Example View" + time_selection = { + quick_selection = { + seconds = 3600 + } + } + folder_id = coralogix_views_folder.example_view_folder.id +} \ No newline at end of file diff --git a/go.mod b/go.mod index f2bc2b692..f8e84f97b 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ replace github.com/grpc-ecosystem/grpc-gateway/v2 => github.com/coralogix/grpc-g require ( github.com/ahmetalpbalkan/go-linq v3.0.0+incompatible - github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251016142149-cfbf6680f3aa + github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251020091618-075a0c2c07d9 github.com/google/uuid v1.6.0 github.com/grafana/grafana-api-golang-client v0.27.0 github.com/hashicorp/terraform-plugin-docs v0.20.1 diff --git a/go.sum b/go.sum index 4369a3072..d521a1bc7 100644 --- a/go.sum +++ b/go.sum @@ -38,12 +38,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251013075013-a70c0bbaecaf h1:TnlB5YHvpQctMhhh2/eqCFnxz8qr9nfzOPI31ESeuUM= -github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251013075013-a70c0bbaecaf/go.mod h1:rxWZfvnH6aXq2zZildOH98EzTLrzQIPSKffz4kf8bkY= -github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251015105058-3e0ead18dfb8 h1:tdoMTI+K5c79TPh7TaMQ1EPJeK66p7zSZIMaIf5f1eU= -github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251015105058-3e0ead18dfb8/go.mod h1:rxWZfvnH6aXq2zZildOH98EzTLrzQIPSKffz4kf8bkY= -github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251016142149-cfbf6680f3aa h1:e4ZtOfkzOtY4+ZkRA2wmTG5+GgyNm40VxTW8N+nbYV0= -github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251016142149-cfbf6680f3aa/go.mod h1:Q7eSBDvWFb6JNfigQXGoWO3XIs6ap1CXfElaZeUrZec= +github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251020091618-075a0c2c07d9 h1:aPHnQzRcxUfg8A8ch9bbhl6EA7TAO8ckPivp/Lw25lA= +github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251020091618-075a0c2c07d9/go.mod h1:Q7eSBDvWFb6JNfigQXGoWO3XIs6ap1CXfElaZeUrZec= github.com/coralogix/grpc-gateway/v2 v2.0.0-20251015134251-4d8694a21a7c h1:aOfG9Pwe7Fp/m+tdybObODwgeK5st/+9df/QUw0dzBQ= github.com/coralogix/grpc-gateway/v2 v2.0.0-20251015134251-4d8694a21a7c/go.mod h1:bqGO/kNOHTFiDIfPmXLQcox10aWQs5Q3b9sXUFoaFFk= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= diff --git a/internal/clientset/clientset.go b/internal/clientset/clientset.go index 2d0209475..b4781b379 100644 --- a/internal/clientset/clientset.go +++ b/internal/clientset/clientset.go @@ -21,6 +21,7 @@ import ( ipaccess "github.com/coralogix/coralogix-management-sdk/go/openapi/gen/ip_access_service" cxsdkOpenapi "github.com/coralogix/coralogix-management-sdk/go/openapi/cxsdk" + views "github.com/coralogix/coralogix-management-sdk/go/openapi/gen/views_service" cxsdk "github.com/coralogix/coralogix-management-sdk/go" ) @@ -51,6 +52,8 @@ type ClientSet struct { groupGrpc *cxsdk.GroupsClient notifications *cxsdk.NotificationsClient ipaccess *ipaccess.IPAccessServiceAPIService + views *views.ViewsServiceAPIService + viewsFolders *cxsdk.ViewFoldersClient grafana *GrafanaClient groups *GroupsClient @@ -164,6 +167,14 @@ func (c *ClientSet) LegacySLOs() *cxsdk.LegacySLOsClient { return c.legacySlos } +func (c *ClientSet) Views() *views.ViewsServiceAPIService { + return c.views +} + +func (c *ClientSet) ViewsFolders() *cxsdk.ViewFoldersClient { + return c.viewsFolders +} + func NewClientSet(region string, apiKey string, targetUrl string) *ClientSet { apiKeySdk := cxsdk.NewSDKCallPropertiesCreatorTerraform(strings.ToLower(region), cxsdk.NewAuthContext(apiKey, apiKey), TF_PROVIDER_VERSION) apikeyCPC := NewCallPropertiesCreator(targetUrl, apiKey) @@ -197,6 +208,8 @@ func NewClientSet(region string, apiKey string, targetUrl string) *ClientSet { groupGrpc: cxsdk.NewGroupsClient(apiKeySdk), notifications: cxsdk.NewNotificationsClient(apiKeySdk), ipaccess: cxsdkOpenapi.NewIPAccessClient(oasTfCPC), + views: cxsdkOpenapi.NewViewsClient(oasTfCPC), + viewsFolders: cxsdk.NewViewFoldersClient(apiKeySdk), grafana: NewGrafanaClient(apikeyCPC), groups: NewGroupsClient(apikeyCPC), diff --git a/internal/provider/data_exploration/data_source_coralogix_view.go b/internal/provider/data_exploration/data_source_coralogix_view.go new file mode 100644 index 000000000..15c5545b1 --- /dev/null +++ b/internal/provider/data_exploration/data_source_coralogix_view.go @@ -0,0 +1,112 @@ +// 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 +// +// https://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 data_exploration + +import ( + "context" + "fmt" + "log" + "strconv" + + "github.com/coralogix/terraform-provider-coralogix/internal/clientset" + "github.com/coralogix/terraform-provider-coralogix/internal/utils" + + views "github.com/coralogix/coralogix-management-sdk/go/openapi/gen/views_service" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ datasource.DataSourceWithConfigure = &ViewDataSource{} + +func NewViewDataSource() datasource.DataSource { + return &ViewDataSource{} +} + +type ViewDataSource struct { + client *views.ViewsServiceAPIService +} + +func (d *ViewDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_view" +} + +func (d *ViewDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + clientSet, ok := req.ProviderData.(*clientset.ClientSet) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *clientset.ClientSet, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = clientSet.Views() +} + +func (d *ViewDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + var r ViewResource + var resourceResp resource.SchemaResponse + r.Schema(ctx, resource.SchemaRequest{}, &resourceResp) + + resp.Schema = utils.FrameworkDatasourceSchemaFromFrameworkResourceSchema(resourceResp.Schema) +} + +func (d *ViewDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data *ViewModel + diags := req.Config.Get(ctx, &data) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + //Get refreshed View value from Coralogix + idStr := data.Id.ValueString() + id, err := strconv.ParseInt(idStr, 10, 32) + + if err != nil { + resp.Diagnostics.AddError( + "Invalid View ID", + fmt.Sprintf("ID '%s' is not a valid 32-bit integer: %s", idStr, err.Error()), + ) + return + } + rq := d.client.ViewsServiceGetView(ctx, int32(id)) + log.Printf("[INFO] Reading new resource: %s", utils.FormatJSON(rq)) + result, _, err := rq.Execute() + + if err != nil { + log.Printf("[ERROR] Received error: %s", err.Error()) + resp.Diagnostics.AddError( + "Error reading View", + utils.FormatOpenAPIErrors(err, "Read", nil), + ) + return + } + log.Printf("[INFO] Read resource: %s", utils.FormatJSON(result)) + + data, diags = flattenView(ctx, result) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/provider/data_exploration/data_source_coralogix_views_folder.go b/internal/provider/data_exploration/data_source_coralogix_views_folder.go new file mode 100644 index 000000000..605c96d74 --- /dev/null +++ b/internal/provider/data_exploration/data_source_coralogix_views_folder.go @@ -0,0 +1,106 @@ +// Copyright 2025 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 +// +// https://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 data_exploration + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/coralogix/terraform-provider-coralogix/coralogix/clientset" + "github.com/coralogix/terraform-provider-coralogix/coralogix/utils" + + "google.golang.org/protobuf/types/known/wrapperspb" + + cxsdk "github.com/coralogix/coralogix-management-sdk/go" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/resource" + "google.golang.org/grpc/codes" +) + +var _ datasource.DataSourceWithConfigure = &ViewsFolderDataSource{} + +func NewViewsFolderDataSource() datasource.DataSource { + return &ViewsFolderDataSource{} +} + +type ViewsFolderDataSource struct { + client *cxsdk.ViewFoldersClient +} + +func (d *ViewsFolderDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_views_folder" +} + +func (d *ViewsFolderDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + clientSet, ok := req.ProviderData.(*clientset.ClientSet) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *clientset.ClientSet, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = clientSet.ViewsFolders() +} + +func (d *ViewsFolderDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + var r ViewsFolderResource + var resourceResp resource.SchemaResponse + r.Schema(ctx, resource.SchemaRequest{}, &resourceResp) + + resp.Schema = utils.FrameworkDatasourceSchemaFromFrameworkResourceSchema(resourceResp.Schema) +} + +func (d *ViewsFolderDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data *ViewsFolderModel + diags := req.Config.Get(ctx, &data) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + //Get refreshed Views-Folder value from Coralogix + id := data.Id.ValueString() + log.Printf("[INFO] Reading views-folder: %s", id) + getViewsFolderResp, err := d.client.Get(ctx, &cxsdk.GetViewFolderRequest{Id: wrapperspb.String(id)}) + if err != nil { + log.Printf("[ERROR] Received error: %s", err.Error()) + if cxsdk.Code(err) == codes.NotFound { + resp.Diagnostics.AddWarning( + err.Error(), + fmt.Sprintf("Views-Folder %q is in state, but no longer exists in Coralogix backend", id), + ) + } else { + resp.Diagnostics.AddError( + "Error reading Views-Folder", + utils.FormatRpcErrors(err, fmt.Sprintf("%s/%s", cxsdk.GetViewFolderRPC, id), ""), + ) + } + return + } + respStr, _ := json.Marshal(getViewsFolderResp) + log.Printf("[INFO] Received View: %s", string(respStr)) + + data = flattenViewsFolder(getViewsFolderResp.Folder) + + // Set state to fully populated data + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/data_exploration/resource_coralogix_view.go b/internal/provider/data_exploration/resource_coralogix_view.go new file mode 100644 index 000000000..db9c40a1e --- /dev/null +++ b/internal/provider/data_exploration/resource_coralogix_view.go @@ -0,0 +1,876 @@ +// Copyright 2025 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 +// +// https://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 data_exploration + +import ( + "context" + "fmt" + "log" + "regexp" + "strconv" + "time" + + views "github.com/coralogix/coralogix-management-sdk/go/openapi/gen/views_service" + "github.com/coralogix/terraform-provider-coralogix/coralogix/clientset" + "github.com/coralogix/terraform-provider-coralogix/coralogix/utils" + + cxsdk "github.com/coralogix/coralogix-management-sdk/go" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +var _ resource.Resource = (*ViewResource)(nil) + +func NewViewResource() resource.Resource { + return &ViewResource{} +} + +type ViewResource struct { + client *views.ViewsServiceAPIService +} + +func (r *ViewResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_view" +} + +func (r *ViewResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + clientSet, ok := req.ProviderData.(*clientset.ClientSet) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *clientset.ClientSet, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = clientSet.Views() +} + +func (r *ViewResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.Int32Attribute{ + Computed: true, + Description: "id", + MarkdownDescription: "id", + PlanModifiers: []planmodifier.Int32{ + int32planmodifier.UseStateForUnknown(), + }, + }, + "folder_id": schema.StringAttribute{ + Optional: true, + Description: "Unique identifier for folders", + MarkdownDescription: "Unique identifier for folders", + Validators: []validator.String{ + stringvalidator.LengthBetween(36, 36), + stringvalidator.RegexMatches(regexp.MustCompile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"), ""), + }, + }, + "name": schema.StringAttribute{ + Required: true, + Description: "View name", + MarkdownDescription: "View name", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "search_query": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "query": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + }, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, + }, + "filters": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "filters": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Description: "Filter name", + MarkdownDescription: "Filter name", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "selected_values": schema.MapAttribute{ + ElementType: types.BoolType, + Required: true, + Description: "Filter selected values", + MarkdownDescription: "Filter selected values", + }, + }, + }, + Optional: true, + Computed: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + }, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, + }, + "time_selection": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "custom_selection": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "from_time": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "to_time": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + }, + Validators: []validator.Object{ + objectvalidator.ExactlyOneOf( + path.MatchRoot("time_selection").AtName("quick_selection"), + ), + }, + Optional: true, + }, + "quick_selection": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "seconds": schema.Int64Attribute{ + Required: true, + Description: "Folder name", + MarkdownDescription: "Folder name", + }, + }, + Optional: true, + }, + }, + Required: true, + }, + }, + } +} + +func (r *ViewResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ViewResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *ViewModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Create API call logic + createViewRequest, diags := extractCreateView(ctx, data) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + viewStr := protojson.Format(createViewRequest) + log.Printf("[INFO] Creating new view: %s", viewStr) + createViewResponse, err := r.client.Create(ctx, createViewRequest) + if err != nil { + log.Printf("[ERROR] Received error: %s", err) + resp.Diagnostics.AddError("Error creating View", + utils.FormatRpcErrors(err, cxsdk.CreateActionRPC, viewStr), + ) + return + } + log.Printf("[INFO] View created successfully: %s", protojson.Format(createViewResponse.View)) + + // Save data into Terraform state + data, diags = flattenView(ctx, createViewResponse.View) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) +} + +func flattenView(ctx context.Context, view *views.View) (*ViewModel, diag.Diagnostics) { + filters, diags := flattenViewFilter(ctx, view.Filters) + if diags.HasError() { + return nil, diags + } + + searchQuery, diags := flattenSearchQuery(ctx, view.SearchQuery) + if diags.HasError() { + return nil, diags + } + + timeSelection, diags := flattenViewTimeSelection(ctx, &view.TimeSelection) + if diags.HasError() { + return nil, diags + } + + return &ViewModel{ + Filters: filters, + FolderId: utils.WrapperspbStringToTypeString(view.FolderId), + Id: types.Int32Value(view.Id), + Name: utils.WrapperspbStringToTypeString(view.Name), + SearchQuery: searchQuery, + TimeSelection: timeSelection, + }, nil +} + +func flattenViewTimeSelection(ctx context.Context, selection *views.TimeSelection) (types.Object, diag.Diagnostics) { + if selection == nil { + return TimeSelectionModel{ + CustomSelection: types.ObjectNull(CustomSelectionModel{}.AttributeTypes(ctx)), + QuickSelection: types.ObjectNull(QuickSelectionModel{}.AttributeTypes(ctx)), + }.ToObjectValue(ctx) + } + + if quickSelection := selection.GetQuickSelection(); quickSelection != nil { + qs, diags := flattenQuickSelection(ctx, quickSelection) + if diags.HasError() { + return types.ObjectNull(TimeSelectionModel{}.AttributeTypes(ctx)), diags + } + + return TimeSelectionModel{ + QuickSelection: qs, + CustomSelection: types.ObjectNull(CustomSelectionModel{}.AttributeTypes(ctx)), + }.ToObjectValue(ctx) + } + + if customSelection := selection.GetCustomSelection(); customSelection != nil { + cs, diags := flattenCustomSelection(ctx, customSelection) + if diags.HasError() { + return types.ObjectNull(TimeSelectionModel{}.AttributeTypes(ctx)), diags + } + return TimeSelectionModel{ + CustomSelection: cs, + QuickSelection: types.ObjectNull(QuickSelectionModel{}.AttributeTypes(ctx)), + }.ToObjectValue(ctx) + } + + return types.ObjectNull(TimeSelectionModel{}.AttributeTypes(ctx)), diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Time Selection", + "Time selection must have either quick selection or custom selection defined.", + ), + } +} + +func flattenCustomSelection(ctx context.Context, selection *cxsdk.CustomTimeSelection) (types.Object, diag.Diagnostics) { + if selection == nil { + return types.ObjectNull(CustomSelectionModel{}.AttributeTypes(ctx)), nil + } + + customSelectionModel := CustomSelectionModel{ + FromTime: types.StringValue(selection.FromTime.AsTime().Format(time.RFC3339)), + ToTime: types.StringValue(selection.ToTime.AsTime().Format(time.RFC3339)), + } + + return customSelectionModel.ToObjectValue(ctx) +} + +func flattenQuickSelection(ctx context.Context, selection *cxsdk.QuickTimeSelection) (types.Object, diag.Diagnostics) { + if selection == nil { + return types.ObjectNull(QuickSelectionModel{}.AttributeTypes(ctx)), nil + } + + quickSelectionModel := QuickSelectionModel{ + Seconds: types.Int64Value(int64(selection.Seconds)), + } + + return quickSelectionModel.ToObjectValue(ctx) +} + +func flattenSearchQuery(ctx context.Context, query *views.SearchQuery) (types.Object, diag.Diagnostics) { + if query == nil { + return types.ObjectNull(SearchQueryModel{}.AttributeTypes(ctx)), nil + } + + return SearchQueryModel{ + Query: utils.WrapperspbStringToTypeString(query.Query), + }.ToObjectValue(ctx) +} + +func flattenViewFilter(ctx context.Context, filters *views.SelectedFilters) (types.Object, diag.Diagnostics) { + if filters == nil { + return types.ObjectNull(FiltersModel{}.AttributeTypes()), nil + } + + innerFilters, diags := flattenInnerViewFilters(ctx, filters.Filters) + if diags.HasError() { + return types.ObjectNull(FiltersModel{}.AttributeTypes()), diags + } + + return FiltersModel{ + Filters: innerFilters, + }.ToObjectValue(ctx) +} + +func flattenInnerViewFilters(ctx context.Context, filters []views.ViewsV1Filter) (basetypes.ListValue, diag.Diagnostics) { + if filters == nil { + return types.ListNull(types.ObjectType{AttrTypes: InnerFiltersModel{}.AttributeTypes()}), nil + } + + innerFilters := make([]InnerFiltersModel, 0, len(filters)) + var diags diag.Diagnostics + for i := range filters { + var selectedValues basetypes.MapValue + if filters[i].SelectedValues == nil { + selectedValues = types.MapNull(types.BoolType) + } else { + var dgs diag.Diagnostics + selectedValues, dgs = types.MapValueFrom(ctx, types.BoolType, filters[i].SelectedValues) + if dgs.HasError() { + diags.Append(dgs...) + continue + } + } + + innerFilters = append(innerFilters, InnerFiltersModel{ + Name: utils.WrapperspbStringToTypeString(filters[i].Name), + SelectedValues: selectedValues, + }) + } + + if diags.HasError() { + return types.ListNull(types.ObjectType{AttrTypes: InnerFiltersModel{}.AttributeTypes()}), diags + } + + return types.ListValueFrom(ctx, types.ObjectType{AttrTypes: InnerFiltersModel{}.AttributeTypes()}, innerFilters) +} + +func extractCreateView(ctx context.Context, data *ViewModel) (*cxsdk.CreateViewRequest, diag.Diagnostics) { + filters, diags := expandSelectedFilters(ctx, data.Filters) + if diags.HasError() { + return nil, diags + } + + timeSelection, diags := expandViewTimeSelection(ctx, data.TimeSelection) + if diags.HasError() { + return nil, diags + } + + searchQuery, diags := expandViewSearchQuery(ctx, data.SearchQuery) + if diags.HasError() { + return nil, diags + } + + return &cxsdk.CreateViewRequest{ + Name: utils.TypeStringToWrapperspbString(data.Name), + SearchQuery: searchQuery, + TimeSelection: timeSelection, + Filters: filters, + FolderId: utils.TypeStringToWrapperspbString(data.FolderId), + }, nil +} + +func extractUpdateView(ctx context.Context, data *ViewModel) (*cxsdk.ReplaceViewRequest, diag.Diagnostics) { + filters, diags := expandSelectedFilters(ctx, data.Filters) + if diags.HasError() { + return nil, diags + } + + timeSelection, diags := expandViewTimeSelection(ctx, data.TimeSelection) + if diags.HasError() { + return nil, diags + } + + searchQuery, diags := expandViewSearchQuery(ctx, data.SearchQuery) + if diags.HasError() { + return nil, diags + } + + return &cxsdk.ReplaceViewRequest{ + View: &cxsdk.View{ + Id: wrapperspb.Int32(int32(data.Id.ValueInt32())), + Name: utils.TypeStringToWrapperspbString(data.Name), + SearchQuery: searchQuery, + TimeSelection: timeSelection, + Filters: filters, + FolderId: utils.TypeStringToWrapperspbString(data.FolderId), + }, + }, nil +} + +func expandSelectedFilters(ctx context.Context, filtersObject types.Object) (*cxsdk.SelectedFilters, diag.Diagnostics) { + if filtersObject.IsNull() || filtersObject.IsUnknown() { + return nil, nil + } + + ov, _ := filtersObject.ToObjectValue(ctx) + var filters FiltersModel + if dg := ov.As(ctx, &filters, basetypes.ObjectAsOptions{}); dg.HasError() { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Filters Object", + fmt.Sprintf("Expected FiltersModel, got: %T. Please report this issue to the provider developers.", filtersObject), + ), + } + } + innerFilters, diags := expandViewFilters(ctx, filters.Filters) + if diags.HasError() { + return nil, diags + } + + return &cxsdk.SelectedFilters{ + Filters: innerFilters, + }, nil + +} + +func expandViewFilters(ctx context.Context, filters basetypes.ListValue) ([]*cxsdk.ViewFilter, diag.Diagnostics) { + if filters.IsNull() || filters.IsUnknown() { + return nil, nil + } + + var diags diag.Diagnostics + var filtersObjects []types.Object + diags = filters.ElementsAs(ctx, &filtersObjects, true) + innerFilters := make([]*cxsdk.ViewFilter, 0, len(filtersObjects)) + for _, fo := range filtersObjects { + var innerFilterValue InnerFiltersModel + if dg := fo.As(ctx, &innerFilterValue, basetypes.ObjectAsOptions{}); dg.HasError() { + diags.Append(dg...) + continue + } + + var selectedValues map[string]bool + if dg := innerFilterValue.SelectedValues.ElementsAs(ctx, &selectedValues, true); dg.HasError() { + diags.Append(dg...) + continue + } + innerFilters = append(innerFilters, &cxsdk.ViewFilter{ + Name: utils.TypeStringToWrapperspbString(innerFilterValue.Name), + SelectedValues: selectedValues, + }) + } + + if diags.HasError() { + return nil, diags + } + + return innerFilters, nil +} + +func expandViewSearchQuery(ctx context.Context, queryObject types.Object) (*cxsdk.SearchQuery, diag.Diagnostics) { + if queryObject.IsNull() || queryObject.IsUnknown() { + return nil, nil + } + + ov, _ := queryObject.ToObjectValue(ctx) + var query SearchQueryModel + if dg := ov.As(ctx, &query, basetypes.ObjectAsOptions{}); dg.HasError() { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Search Query Object", + fmt.Sprintf("Expected SearchQueryModel, got: %T. Please report this issue to the provider developers.", queryObject), + ), + } + } + + return &cxsdk.SearchQuery{ + Query: utils.TypeStringToWrapperspbString(query.Query), + }, nil +} + +func expandViewTimeSelection(ctx context.Context, selectionObject types.Object) (*cxsdk.TimeSelection, diag.Diagnostics) { + if selectionObject.IsNull() || selectionObject.IsUnknown() { + return nil, nil + } + + var selection TimeSelectionModel + if dg := selectionObject.As(ctx, &selection, basetypes.ObjectAsOptions{}); dg.HasError() { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Time Selection Object", + fmt.Sprintf("Expected TimeSelectionModel, got: %T. Please report this issue to the provider developers.", selectionObject), + ), + } + } + + if quickSelection := selection.QuickSelection; !(quickSelection.IsNull() || quickSelection.IsUnknown()) { + qs, diags := expandQuickSelection(ctx, selection.QuickSelection) + if diags.HasError() { + return nil, diags + } + return &cxsdk.TimeSelection{ + SelectionType: &cxsdk.ViewTimeSelectionQuick{ + QuickSelection: qs, + }, + }, nil + } else if customSelection := selection.CustomSelection; !(customSelection.IsNull() || customSelection.IsUnknown()) { + cs, diags := expandCustomSelection(ctx, selection.CustomSelection) + if diags.HasError() { + return nil, diags + } + return &cxsdk.TimeSelection{ + SelectionType: &cxsdk.ViewTimeSelectionCustom{ + CustomSelection: cs, + }, + }, nil + } + + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Time Selection", + "Time selection must have either quick selection or custom selection defined.", + ), + } +} + +func expandCustomSelection(ctx context.Context, selection types.Object) (*cxsdk.CustomTimeSelection, diag.Diagnostics) { + if selection.IsNull() || selection.IsUnknown() { + return nil, nil + } + + attributes := selection.Attributes() + fromTimeAttr, ok := attributes["from_time"] + if !ok { + return nil, nil + } + toTimeAttr, ok := attributes["to_time"] + if !ok { + return nil, nil + } + + fromTime, ok := fromTimeAttr.(types.String) + if !ok || fromTime.IsNull() || fromTime.IsUnknown() { + return nil, nil + } + toTime, ok := toTimeAttr.(types.String) + if !ok || toTime.IsNull() || toTime.IsUnknown() { + return nil, nil + } + + ft, err := time.Parse(time.RFC3339, fromTime.ValueString()) + if err != nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid From Time Format", + fmt.Sprintf("From time '%s' is not in RFC3339 format: %s", fromTime.ValueString(), err.Error()), + ), + } + } + tt, err := time.Parse(time.RFC3339, toTime.ValueString()) + if err != nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid To Time Format", + fmt.Sprintf("To time '%s' is not in RFC3339 format: %s", toTime.ValueString(), err.Error()), + ), + } + } + + return &cxsdk.CustomTimeSelection{ + FromTime: timestamppb.New(ft), + ToTime: timestamppb.New(tt), + }, nil +} + +func expandQuickSelection(ctx context.Context, selection types.Object) (*cxsdk.QuickTimeSelection, diag.Diagnostics) { + if selection.IsNull() || selection.IsUnknown() { + return nil, nil + } + + attributes := selection.Attributes() + secondsAttr, ok := attributes["seconds"] + if !ok { + return nil, nil + } + + seconds, ok := secondsAttr.(types.Int64) + if !ok || seconds.IsNull() || seconds.IsUnknown() { + return nil, nil + } + + if seconds.ValueInt64() < 0 { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Seconds Value", + fmt.Sprintf("Seconds value '%d' cannot be negative.", seconds.ValueInt64()), + ), + } + } + + return &cxsdk.QuickTimeSelection{ + Seconds: uint32(seconds.ValueInt64()), + }, nil +} + +func (r *ViewResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *ViewModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + idStr := data.Id.ValueString() + id, err := strconv.Atoi(idStr) + if err != nil { + resp.Diagnostics.AddError( + "Invalid View ID", + fmt.Sprintf("ID '%s' is not a valid integer: %s", idStr, err.Error()), + ) + return + } + + readReq := &cxsdk.GetViewRequest{ + Id: wrapperspb.Int32(int32(id)), + } + log.Printf("[INFO] Reading view with ID: %s", idStr) + readResp, err := r.client.Get(ctx, readReq) + if err != nil { + log.Printf("[ERROR] Received error: %s", err.Error()) + if cxsdk.Code(err) == codes.NotFound { + resp.Diagnostics.AddWarning( + fmt.Sprintf("View %q is in state, but no longer exists in Coralogix backend", idStr), + fmt.Sprintf("%s will be recreated when you apply", idStr), + ) + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError( + "Error reading view", + utils.FormatRpcErrors(err, cxsdk.GetViewRPC, protojson.Format(readReq)), + ) + } + return + } + log.Printf("[INFO] View read successfully: %s", protojson.Format(readResp.View)) + + // Flatten the response into the model + data, diags := flattenView(ctx, readResp.View) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ViewResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data *ViewModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + updateReq, diags := extractUpdateView(ctx, data) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + log.Printf("[INFO] Updating view in state: %s", protojson.Format(updateReq)) + updateResp, err := r.client.Replace(ctx, updateReq) + if err != nil { + log.Printf("[ERROR] Received error: %s", err.Error()) + resp.Diagnostics.AddError( + "Error updating view in state", + utils.FormatRpcErrors(err, cxsdk.ReplaceViewRPC, protojson.Format(updateReq)), + ) + return + } + log.Printf("[INFO] View updated in state successfully: %s", protojson.Format(updateResp.View)) + + // Flatten the response into the model + data, diags = flattenView(ctx, updateResp.View) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ViewResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *ViewModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + id := data.Id + rq := r.client.ViewsServiceDeleteView(ctx, id.ValueInt32()) + _, _, err := rq.Execute() + if err != nil { + log.Printf("[ERROR] Received error: %s", err.Error()) + if cxsdk.Code(err) == codes.NotFound { + resp.Diagnostics.AddWarning( + fmt.Sprintf("View %q is in state, but no longer exists in Coralogix backend", id), + fmt.Sprintf("%d will be removed from state", id), + ) + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError( + "Error deleting view", + utils.FormatRpcErrors(err, cxsdk.DeleteViewRPC, fmt.Sprintf("ID: %d", id)), + ) + } + return + } + if resp.Diagnostics.HasError() { + return + } +} + +type ViewModel struct { + Filters types.Object `tfsdk:"filters"` //FiltersModel + FolderId types.String `tfsdk:"folder_id"` + Id types.Int32 `tfsdk:"id"` + Name types.String `tfsdk:"name"` + SearchQuery types.Object `tfsdk:"search_query"` //SearchQueryModel + TimeSelection types.Object `tfsdk:"time_selection"` // TimeSelectionModel +} + +type QuickSelectionModel struct { + Seconds types.Int64 `tfsdk:"seconds"` +} + +func (v QuickSelectionModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) { + return types.ObjectValueFrom(ctx, v.AttributeTypes(ctx), v) +} + +func (v QuickSelectionModel) AttributeTypes(ctx context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "seconds": basetypes.Int64Type{}, + } +} + +type FiltersModel struct { + Filters types.List `tfsdk:"filters"` // InnerFiltersModel +} + +func (v FiltersModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) { + return types.ObjectValueFrom(ctx, v.AttributeTypes(), v) +} + +func (v FiltersModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "filters": basetypes.ListType{ + ElemType: basetypes.ObjectType{ + AttrTypes: InnerFiltersModel{}.AttributeTypes(), + }, + }, + } +} + +type InnerFiltersModel struct { + Name types.String `tfsdk:"name"` + SelectedValues types.Map `tfsdk:"selected_values"` +} + +func (v InnerFiltersModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) { + return types.ObjectValueFrom(ctx, v.AttributeTypes(), v) +} + +func (v InnerFiltersModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "name": basetypes.StringType{}, + "selected_values": basetypes.MapType{ + ElemType: types.BoolType, + }, + } +} + +type SearchQueryModel struct { + Query types.String `tfsdk:"query"` +} + +func (v SearchQueryModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) { + return types.ObjectValueFrom(ctx, v.AttributeTypes(ctx), v) +} + +func (v SearchQueryModel) AttributeTypes(ctx context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "query": basetypes.StringType{}, + } +} + +type TimeSelectionModel struct { + CustomSelection types.Object `tfsdk:"custom_selection"` //CustomSelectionModel + QuickSelection types.Object `tfsdk:"quick_selection"` //QuickSelectionModel +} + +func (v TimeSelectionModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) { + return types.ObjectValueFrom(ctx, v.AttributeTypes(ctx), v) +} + +func (v TimeSelectionModel) AttributeTypes(ctx context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "custom_selection": basetypes.ObjectType{ + AttrTypes: CustomSelectionModel{}.AttributeTypes(ctx), + }, + "quick_selection": basetypes.ObjectType{ + AttrTypes: QuickSelectionModel{}.AttributeTypes(ctx), + }, + } +} + +type CustomSelectionModel struct { + FromTime types.String `tfsdk:"from_time"` + ToTime types.String `tfsdk:"to_time"` +} + +func (v CustomSelectionModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) { + return types.ObjectValueFrom(ctx, v.AttributeTypes(ctx), v) +} + +func (v CustomSelectionModel) AttributeTypes(context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "from_time": basetypes.StringType{}, + "to_time": basetypes.StringType{}, + } +} diff --git a/internal/provider/data_exploration/resource_coralogix_views_folder.go b/internal/provider/data_exploration/resource_coralogix_views_folder.go new file mode 100644 index 000000000..c0ec19f66 --- /dev/null +++ b/internal/provider/data_exploration/resource_coralogix_views_folder.go @@ -0,0 +1,248 @@ +// 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 +// +// https://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 data_exploration + +import ( + "context" + "fmt" + "log" + + "github.com/coralogix/terraform-provider-coralogix/coralogix/clientset" + "github.com/coralogix/terraform-provider-coralogix/coralogix/utils" + + cxsdk "github.com/coralogix/coralogix-management-sdk/go" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +var _ resource.Resource = (*ViewsFolderResource)(nil) + +func NewViewsFolderResource() resource.Resource { + return &ViewsFolderResource{} +} + +type ViewsFolderResource struct { + client *cxsdk.ViewFoldersClient +} + +func (r *ViewsFolderResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_views_folder" +} + +func (r *ViewsFolderResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + clientSet, ok := req.ProviderData.(*clientset.ClientSet) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *clientset.ClientSet, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = clientSet.ViewsFolders() +} + +func (r *ViewsFolderResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = ViewsFolderResourceSchema(ctx) +} + +func (r *ViewsFolderResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ViewsFolderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *ViewsFolderModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Create API call logic + createRequest := &cxsdk.CreateViewFolderRequest{ + Name: utils.TypeStringToWrapperspbString(data.Name), + } + viewFolderStr := protojson.Format(createRequest) + log.Printf("[INFO] Creating new views-folder: %s", viewFolderStr) + createResponse, err := r.client.Create(ctx, createRequest) + if err != nil { + log.Printf("[ERROR] Received error: %s", err) + resp.Diagnostics.AddError("Error creating Views-Folder", + utils.FormatRpcErrors(err, cxsdk.CreateActionRPC, viewFolderStr), + ) + return + } + log.Printf("[INFO] Views-Folder created successfully: %s", protojson.Format(createResponse)) + + // Save data into Terraform state + data = flattenViewsFolder(createResponse.Folder) + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) +} + +func extractViewsFolder(data *ViewsFolderModel) *cxsdk.ViewFolder { + return &cxsdk.ViewFolder{ + Id: utils.TypeStringToWrapperspbString(data.Id), + Name: utils.TypeStringToWrapperspbString(data.Name), + } +} + +func flattenViewsFolder(viewsFolder *cxsdk.ViewFolder) *ViewsFolderModel { + return &ViewsFolderModel{ + Id: utils.WrapperspbStringToTypeString(viewsFolder.Id), + Name: utils.WrapperspbStringToTypeString(viewsFolder.Name), + } +} + +func (r *ViewsFolderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *ViewsFolderModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id := data.Id.ValueString() + readReq := &cxsdk.GetViewFolderRequest{ + Id: wrapperspb.String(id), + } + log.Printf("[INFO] Reading views-folder with ID: %s", id) + readResp, err := r.client.Get(ctx, readReq) + if err != nil { + log.Printf("[ERROR] Received error: %s", err.Error()) + if cxsdk.Code(err) == codes.NotFound { + resp.Diagnostics.AddWarning( + fmt.Sprintf("Views-Folder %q is in state, but no longer exists in Coralogix backend", id), + fmt.Sprintf("%s will be recreated when you apply", id), + ) + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError( + "Error reading views-folder", + utils.FormatRpcErrors(err, cxsdk.GetViewFolderRPC, protojson.Format(readReq)), + ) + } + return + } + log.Printf("[INFO] Views-Folder read successfully: %s", protojson.Format(readResp.Folder)) + + // Flatten the response into the model + data = flattenViewsFolder(readResp.Folder) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ViewsFolderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data *ViewsFolderModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + updateReq := &cxsdk.ReplaceViewFolderRequest{ + Folder: extractViewsFolder(data), + } + + log.Printf("[INFO] Updating views-folder in state: %s", protojson.Format(updateReq)) + updateResp, err := r.client.Replace(ctx, updateReq) + if err != nil { + log.Printf("[ERROR] Received error: %s", err.Error()) + resp.Diagnostics.AddError( + "Error updating views-folder in state", + utils.FormatRpcErrors(err, cxsdk.ReplaceViewFolderRPC, protojson.Format(updateReq)), + ) + return + } + log.Printf("[INFO] Views-Folder updated in state successfully: %s", protojson.Format(updateResp.Folder)) + + // Flatten the response into the model + data = flattenViewsFolder(updateResp.Folder) + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ViewsFolderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *ViewsFolderModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + id := data.Id.ValueString() + _, err := r.client.Delete(ctx, &cxsdk.DeleteViewFolderRequest{Id: wrapperspb.String(id)}) + if err != nil { + log.Printf("[ERROR] Received error: %s", err.Error()) + if cxsdk.Code(err) == codes.NotFound { + resp.Diagnostics.AddWarning( + fmt.Sprintf("Views-Folder %q is in state, but no longer exists in Coralogix backend", id), + fmt.Sprintf("%s will be removed from state", id), + ) + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError( + "Error deleting views-folder", + utils.FormatRpcErrors(err, cxsdk.DeleteViewFolderRPC, id), + ) + } + return + } + if resp.Diagnostics.HasError() { + return + } +} + +func ViewsFolderResourceSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "id", + MarkdownDescription: "id", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Required: true, + Description: "Name of the views-folder", + MarkdownDescription: "Name of the views-folder", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + }, + } +} + +type ViewsFolderModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` +} diff --git a/internal/provider/data_source_coralogix_view_test.go b/internal/provider/data_source_coralogix_view_test.go new file mode 100644 index 000000000..509444d49 --- /dev/null +++ b/internal/provider/data_source_coralogix_view_test.go @@ -0,0 +1,44 @@ +// 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 +// +// https://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 provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccCoralogixDataSourceView_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccCoralogixResourceView() + + testAccCoralogixDataSourceView_read(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.coralogix_view.test", "name", "Example View"), + ), + }, + }, + }) +} + +func testAccCoralogixDataSourceView_read() string { + return `data "coralogix_view" "test" { + id = coralogix_view.test.id + } +` +} diff --git a/internal/provider/data_source_coralogix_views_folder_test.go b/internal/provider/data_source_coralogix_views_folder_test.go new file mode 100644 index 000000000..cac2c165c --- /dev/null +++ b/internal/provider/data_source_coralogix_views_folder_test.go @@ -0,0 +1,44 @@ +// 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 +// +// https://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 provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccCoralogixDataSourceViewsFolder_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccCoralogixResourceViewsFolder() + + testAccCoralogixDataSourceViewsFolder_read(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.coralogix_views_folder.test", "name", "Example Views Folder"), + ), + }, + }, + }) +} + +func testAccCoralogixDataSourceViewsFolder_read() string { + return `data "coralogix_views_folder" "test" { + id = coralogix_views_folder.test.id + } +` +} diff --git a/internal/provider/resource_coralogix_view_test.go b/internal/provider/resource_coralogix_view_test.go new file mode 100644 index 000000000..2b196c686 --- /dev/null +++ b/internal/provider/resource_coralogix_view_test.go @@ -0,0 +1,122 @@ +// Copyright 2025 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 +// +// https://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 provider + +import ( + "context" + "fmt" + "strconv" + "testing" + + "github.com/coralogix/terraform-provider-coralogix/coralogix/clientset" + + cxsdk "github.com/coralogix/coralogix-management-sdk/go" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func TestAccCoralogixResourceView(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckViewDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCoralogixResourceView(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("coralogix_view.test", "id"), + resource.TestCheckResourceAttr("coralogix_view.test", "name", "Example View"), + resource.TestCheckResourceAttr("coralogix_view.test", "time_selection.custom_selection.from_time", "2023-01-01T00:00:00Z"), + resource.TestCheckResourceAttr("coralogix_view.test", "time_selection.custom_selection.to_time", "2023-01-02T00:00:00Z"), + resource.TestCheckResourceAttr("coralogix_view.test", "search_query.query", "error OR warning"), + ), + }, + { + ResourceName: "coralogix_view.test", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccCoralogixResourceUpdatedView(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("coralogix_view.test", "id"), + resource.TestCheckResourceAttr("coralogix_view.test", "name", "Example View Updated"), + resource.TestCheckResourceAttr("coralogix_view.test", "time_selection.quick_selection.seconds", "86400"), // 24 hours in seconds + resource.TestCheckResourceAttr("coralogix_view.test", "search_query.query", "error OR warning"), + ), + }, + }, + }) +} + +func testAccCheckViewDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*clientset.ClientSet).Views() + ctx := context.TODO() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "coralogix_view" { + continue + } + + if rs.Primary.ID == "" { + return nil + } + + intID, err := strconv.Atoi(rs.Primary.ID) + if err != nil { + return fmt.Errorf("invalid ID format: %s", rs.Primary.ID) + } + + resp, err := client.Get(ctx, &cxsdk.GetViewRequest{ + Id: wrapperspb.Int32(int32(intID)), + }) + if err == nil && resp != nil && resp.View != nil { + return fmt.Errorf("view still exists: %v", rs.Primary.ID) + } + } + return nil +} + +func testAccCoralogixResourceView() string { + return `resource "coralogix_view" "test" { + name = "Example View" + time_selection = { + custom_selection = { + from_time = "2023-01-01T00:00:00Z" + to_time = "2023-01-02T00:00:00Z" + } + } + search_query = { + query = "error OR warning" + } +} + ` +} + +func testAccCoralogixResourceUpdatedView() string { + return `resource "coralogix_view" "test" { + name = "Example View Updated" + time_selection = { + quick_selection = { + seconds = 86400 # 24 hours in seconds + } + } + search_query = { + query = "error OR warning" + } +} + ` +} diff --git a/internal/provider/resource_coralogix_views_folder_test.go b/internal/provider/resource_coralogix_views_folder_test.go new file mode 100644 index 000000000..7fbbe820b --- /dev/null +++ b/internal/provider/resource_coralogix_views_folder_test.go @@ -0,0 +1,91 @@ +// Copyright 2025 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 +// +// https://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 provider + +import ( + "context" + "fmt" + "testing" + + "github.com/coralogix/terraform-provider-coralogix/coralogix/clientset" + + cxsdk "github.com/coralogix/coralogix-management-sdk/go" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func TestAccCoralogixResourceViewsFolder(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckViewsFolderDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCoralogixResourceViewsFolder(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("coralogix_views_folder.test", "id"), + resource.TestCheckResourceAttr("coralogix_views_folder.test", "name", "Example Views Folder"), + ), + }, + { + ResourceName: "coralogix_views_folder.test", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccCoralogixResourceUpdatedViewsFolder(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("coralogix_views_folder.test", "id"), + resource.TestCheckResourceAttr("coralogix_views_folder.test", "name", "Example Views Folder Updated"), + ), + }, + }, + }) +} + +func testAccCheckViewsFolderDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*clientset.ClientSet).ViewsFolders() + ctx := context.TODO() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "coralogix_views_folder" { + continue + } + + if rs.Primary.ID == "" { + return nil + } + + resp, err := client.Get(ctx, &cxsdk.GetViewFolderRequest{Id: wrapperspb.String(rs.Primary.ID)}) + if err == nil && resp != nil && resp.Folder != nil { + return fmt.Errorf("views-folder still exists: %v", rs.Primary.ID) + } + } + return nil +} + +func testAccCoralogixResourceViewsFolder() string { + return `resource "coralogix_views_folder" "test" { + name = "Example Views Folder" +} + ` +} + +func testAccCoralogixResourceUpdatedViewsFolder() string { + return `resource "coralogix_views_folder" "test" { + name = "Example Views Folder Updated" +} + ` +}