Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement Monitoring Service #729

Merged
merged 9 commits into from
Jan 13, 2025
4 changes: 2 additions & 2 deletions docs/data-sources/monitoring_pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ If a single match is found, it will be returned. If your search results in multi
```hcl
data "ionoscloud_monitoring_pipeline" "example" {
location = "de/txl"
id = <pipeline_id>
id = "pipeline_id"
}
```

### By name
```hcl
data "ionoscloud_monitoring_pipeline" "example" {
location = "de/txl"
name = <pipeline_name>
name = "pipeline_name"
}
```

Expand Down
15 changes: 9 additions & 6 deletions docs/resources/monitoring_pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,30 @@ resource "ionoscloud_monitoring_pipeline" "example" {
}
```

**NOTE:** The default timeout for all operations is 20 minutes. If you want to change the default value, you can use `timeouts` attribute inside the resource:
**NOTE:** The default timeout for all operations is 60 minutes. If you want to change the default value, you can use `timeouts` attribute inside the resource:

```hcl
resource "ionoscloud_monitoring_pipeline" "example" {
location = "es/vit"
name = "pipelineExample"
timeouts {
create = "10m"
create = "20m"
read = "30s"
update = "5m"
delete = "1m"
update = "10m"
delete = "10m"
}
}
```

## Argument reference

* `name` - (Required)[string] The name of the Monitoring pipeline.
* `location` - (Optional)[string] The location of the Monitoring pipeline. Default is `de/fra`. It can be one of `de/fra`, `de/txl`, `gb/lhr`, `es/vit`, `fr/par`. If this is not set and if no value is provided for the `IONOS_API_URL` env var, the default `location` will be: `de/fra`.
* `location` - (Optional)[string] The location of the Monitoring pipeline. Default is `de/fra`. It can be one of `de/fra`, `de/txl`, `gb/lhr`, `es/vit`, `fr/par`. If this is not set and if no value is provided for the `IONOS_API_URL_MONITORING` env var, the default `location` will be: `de/fra`.
* `grafana_endpoint` - (Computed)[string] The endpoint of the Grafana instance.
* `http_endpoint` - (Computed)[string] The HTTP endpoint of the monitoring instance.
* `key` - (Computed)(Sensitive)[string] The key used to connect to the monitoring pipeline.

> **⚠ NOTE:** `IONOS_API_URL_MONITORING` can be used to set a custom API URL for the resource. `location` field needs to be empty, otherwise it will override the custom API URL.

## Import

Expand All @@ -56,5 +59,5 @@ resource "ionoscloud_monitoring_pipeline" "example" {
The resource can be imported using the `location` and `pipeline_id`, for example:

```shell
terraform import ionoscloud_monitoring_pipeline.example {location}:{pipeline_id}
terraform import ionoscloud_monitoring_pipeline.example location:pipeline_id
```
1 change: 1 addition & 0 deletions gitbook_docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ See the [IonosCloud Provider documentation](https://registry.terraform.io/provid
| `IONOS_API_URL_KAFKA` | Sets a custom API URL for the Event Streams product. `location` field needs to be empty, otherwise it will override the custom API URL. Setting `token` or `IONOS_API_URL` does not have any effect. |
| `IONOS_API_URL_VPN` | Sets a custom API URL for the VPN product. `location` field needs to be empty, otherwise it will override the custom API URL. Setting `token` or `IONOS_API_URL` does not have any effect. |
| `IONOS_API_URL_OBJECT_STORAGE` | Sets a custom API URL for the Object Storage product. `region` field needs to be empty, otherwise it will override the custom API URL. Setting `token` or `IONOS_API_URL` does not have any effect. |
| `IONOS_API_URL_MONITORING` | Sets a custom API URL for the Monitoring product. `region` field needs to be empty, otherwise it will override the custom API URL. Setting `token` or `IONOS_API_URL` does not have any effect. |

### Certificate pinning:

Expand Down
6 changes: 0 additions & 6 deletions internal/acctest/acctest.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,6 @@ func PreCheck(t *testing.T) {
t.Fatalf("%s/%s or %s must be set for acceptance tests", envar.IonosUsername, envar.IonosPassword, envar.IonosToken)
}
}

accessKey := os.Getenv(envar.IonosS3AccessKey)
Copy link
Contributor Author

@adeatcu-ionos adeatcu-ionos Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cristiGuranIonos It's good that you removed this since the API will complain about not setting the accessKey and the secretKey, so there is no need for us to do this check.

secretKey := os.Getenv(envar.IonosS3SecretKey)
if accessKey == "" || secretKey == "" {
t.Fatalf("%s and %s must be set for acceptance tests", envar.IonosS3AccessKey, envar.IonosS3SecretKey)
}
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func (d *pipelineDataSource) Read(ctx context.Context, req datasource.ReadReques
// Retrieve ALL pipelines.
retrievedPipelines, _, err := d.client.GetPipelines(ctx, location)
if err != nil {
resp.Diagnostics.AddError("failed to get Monitoring pipelines", err.Error())
resp.Diagnostics.AddError(fmt.Sprintf("failed to get Monitoring pipelines location %s", location), err.Error())
return
}

Expand All @@ -151,7 +151,7 @@ func (d *pipelineDataSource) Read(ctx context.Context, req datasource.ReadReques
}

if len(pipelines) == 0 {
resp.Diagnostics.AddError("no Monitoring pipeline found with the specified name", "Please make sure that the name is correct or search using the pipeline ID instead")
resp.Diagnostics.AddError(fmt.Sprintf("no Monitoring pipeline found with the specified name %s in location %s ", pipelineName, location), "Please make sure that the name and location are correct, or search using the pipeline ID instead")
return
}

Expand Down
2 changes: 1 addition & 1 deletion internal/framework/services/monitoring/data_sources.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/datasource"
)

// DataSources returns the list of data sources for the objectstorage package.
// DataSources returns the list of data sources for the package.
func DataSources() []func() datasource.DataSource {
return []func() datasource.DataSource{
NewPipelineDataSource,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package monitoring
import (
"context"
"fmt"
"github.com/cenkalti/backoff/v4"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
Expand All @@ -15,7 +16,6 @@ import (
monitoringService "github.com/ionos-cloud/terraform-provider-ionoscloud/v6/services/monitoring"
"github.com/ionos-cloud/terraform-provider-ionoscloud/v6/utils"
"strings"
"time"
)

var (
Expand Down Expand Up @@ -69,6 +69,7 @@ func (r *pipelineResource) Schema(ctx context.Context, req resource.SchemaReques
},
"key": schema.StringAttribute{
Computed: true,
Sensitive: true,
Description: "The authentication key of the monitoring instance",
},
"location": schema.StringAttribute{
Expand Down Expand Up @@ -123,8 +124,7 @@ func (r *pipelineResource) Create(ctx context.Context, req resource.CreateReques
},
}
location := data.Location.ValueString()

createTimeout, diags := data.Timeouts.Create(ctx, 20*time.Minute)
createTimeout, diags := data.Timeouts.Create(ctx, utils.DefaultTimeout)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
Expand All @@ -135,19 +135,18 @@ func (r *pipelineResource) Create(ctx context.Context, req resource.CreateReques
resp.Diagnostics.AddError("failed to create Monitoring pipeline", err.Error())
return
}

pipelineID := pipelineResponse.Id
key := pipelineResponse.Metadata.Key

err = utils.WaitForResourceToBeReadyV2(ctx, createTimeout, pipelineID, location, r.client.IsPipelineReady)
err = backoff.Retry(func() error {
return r.client.IsPipelineReady(ctx, pipelineID, location)
}, backoff.NewExponentialBackOff(backoff.WithMaxElapsedTime(createTimeout)))
if err != nil {
resp.Diagnostics.AddError("error occurred while waiting for the Monitoring pipeline to become available", err.Error())
return
}

// Make another `GET` request after the pipeline becomes 'AVAILABLE' in order to retrieve some
// attributes that are not set in the `POST` response.

retrievedPipeline, _, err := r.client.GetPipelineByID(ctx, pipelineID, location)
if err != nil {
resp.Diagnostics.AddError("error while fetching Monitoring pipeline after creation", (fmt.Errorf("pipeline ID: %v, error: %w", pipelineID, err)).Error())
Expand Down Expand Up @@ -205,7 +204,7 @@ func (r *pipelineResource) Delete(ctx context.Context, req resource.DeleteReques
pipelineID := data.ID.ValueString()
location := data.Location.ValueString()

deleteTimeout, diags := data.Timeouts.Delete(ctx, 20*time.Minute)
deleteTimeout, diags := data.Timeouts.Delete(ctx, utils.DefaultTimeout)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
Expand All @@ -217,7 +216,9 @@ func (r *pipelineResource) Delete(ctx context.Context, req resource.DeleteReques
return
}

err = utils.WaitForResourceToBeDeletedV2(ctx, deleteTimeout, pipelineID, location, r.client.IsPipelineDeleted)
err = backoff.Retry(func() error {
return r.client.IsPipelineDeleted(ctx, pipelineID, location)
}, backoff.NewExponentialBackOff(backoff.WithMaxElapsedTime(deleteTimeout)))
if err != nil {
resp.Diagnostics.AddError("error occurred while waiting for the Monitoring pipeline to be deleted", (fmt.Errorf("pipeline ID: %v, error: %w", pipelineID, err)).Error())
return
Expand All @@ -241,7 +242,7 @@ func (r *pipelineResource) Update(ctx context.Context, req resource.UpdateReques
},
}

updateTimeout, diags := plan.Timeouts.Update(ctx, 20*time.Minute)
updateTimeout, diags := plan.Timeouts.Update(ctx, utils.DefaultTimeout)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
Expand All @@ -252,10 +253,11 @@ func (r *pipelineResource) Update(ctx context.Context, req resource.UpdateReques
resp.Diagnostics.AddError("error while updating Monitoring pipeline", (fmt.Errorf("pipeline ID: %v, error: %w", pipelineID, err)).Error())
return
}

err = utils.WaitForResourceToBeReadyV2(ctx, updateTimeout, pipelineID, location, r.client.IsPipelineReady)
err = backoff.Retry(func() error {
return r.client.IsPipelineReady(ctx, pipelineID, location)
}, backoff.NewExponentialBackOff(backoff.WithMaxElapsedTime(updateTimeout)))
if err != nil {
resp.Diagnostics.AddError("error while waiting for the Monitoring pipeline to become available after update", (fmt.Errorf("pipeline ID: %v, error: %w", pipelineID, err)).Error())
resp.Diagnostics.AddError("error while waiting for the Monitoring pipeline to become AVAILABLE after update", (fmt.Errorf("pipeline ID: %v, error: %w", pipelineID, err)).Error())
return
}

Expand Down
2 changes: 1 addition & 1 deletion internal/framework/services/monitoring/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package monitoring

import "github.com/hashicorp/terraform-plugin-framework/resource"

// Resources returns the list of resources for the objectstorage package.
// Resources returns the list of resources for the package.
func Resources() []func() resource.Resource {
return []func() resource.Resource{
NewPipelineResource,
Expand Down
20 changes: 13 additions & 7 deletions services/monitoring/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package monitoring
import (
"context"
"fmt"
"github.com/cenkalti/backoff/v4"
"github.com/ionos-cloud/sdk-go-bundle/shared"
"github.com/ionos-cloud/terraform-provider-ionoscloud/v6/utils"
"github.com/ionos-cloud/terraform-provider-ionoscloud/v6/utils/constant"
Expand Down Expand Up @@ -83,24 +84,29 @@ func (c *MonitoringClient) GetPipelines(ctx context.Context, location string) ([
}

// IsPipelineReady checks if the pipeline is ready.
func (c *MonitoringClient) IsPipelineReady(ctx context.Context, pipelineID, location string) (bool, error) {
// backoff.Permanent is used to stop the retry.
func (c *MonitoringClient) IsPipelineReady(ctx context.Context, pipelineID, location string) error {
pipeline, _, err := c.GetPipelineByID(ctx, pipelineID, location)
if err != nil {
return false, err
return backoff.Permanent(err)
}
log.Printf("[DEBUG] Monitoring pipeline state: %s", pipeline.Metadata.Status)

return strings.EqualFold(pipeline.Metadata.Status, constant.Available), nil
if strings.EqualFold(pipeline.Metadata.Status, constant.Available) {
return nil
}
return fmt.Errorf("pipeline is not ready, current state: %s", pipeline.Metadata.Status)
}

// IsPipelineDeleted checks if the pipeline is deleted.
func (c *MonitoringClient) IsPipelineDeleted(ctx context.Context, pipelineID, location string) (bool, error) {
// backoff.Permanent is used to stop the retry.
func (c *MonitoringClient) IsPipelineDeleted(ctx context.Context, pipelineID, location string) error {
_, apiResponse, err := c.GetPipelineByID(ctx, pipelineID, location)
if err != nil {
if apiResponse.HttpNotFound() {
return true, nil
return nil
}
return false, fmt.Errorf("check failed for Monitoring pipeline with ID: %v, error: %w", pipelineID, err)
return backoff.Permanent(fmt.Errorf("check failed for Monitoring pipeline with ID: %v, error: %w", pipelineID, err))
}
return false, nil
return fmt.Errorf("resource not yet deleted %s, location %s", pipelineID, location)
}
39 changes: 0 additions & 39 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,6 @@ type ApiResponseInfo interface {
// ResourceReadyFunc polls api to see if resource exists based on id
type ResourceReadyFunc func(ctx context.Context, d *schema.ResourceData) (bool, error)

// ResourceReadyFuncV2 polls api to see if resource exists based on id
// V2 signature is different because it is written for the terraform-plugin-framework
type ResourceReadyFuncV2 func(ctx context.Context, resourceID, location string) (bool, error)

// WaitForResourceToBeReady - keeps retrying until resource is ready(true is returned), or until err is thrown, or ctx is cancelled
func WaitForResourceToBeReady(ctx context.Context, d *schema.ResourceData, fn ResourceReadyFunc) error {
if d.Id() == "" {
Expand All @@ -263,44 +259,9 @@ func WaitForResourceToBeReady(ctx context.Context, d *schema.ResourceData, fn Re
})
}

// WaitForResourceToBeReadyV2 - keeps retrying until resource is ready (true is returned), or until err is thrown, or ctx is cancelled
// V2 signature is different because it is written for the terraform-plugin-framework
func WaitForResourceToBeReadyV2(ctx context.Context, timeout time.Duration, resourceID, location string, fn ResourceReadyFuncV2) error {
return retry.RetryContext(ctx, timeout, func() *retry.RetryError {
isReady, err := fn(ctx, resourceID, location)
if isReady {
return nil
}
if err != nil {
return retry.NonRetryableError(err)
}
log.Printf("[DEBUG] resource with id %s not ready, still trying ", resourceID)
return retry.RetryableError(fmt.Errorf("resource with id %s not ready, still trying ", resourceID))
})
}

// IsResourceDeletedFunc polls api to see if resource exists based on id
type IsResourceDeletedFunc func(ctx context.Context, d *schema.ResourceData) (bool, error)

// IsResourceDeletedFuncV2 polls api to see if resource exists based on id
// V2 signature is different because it is written for the terraform-plugin-framework
type IsResourceDeletedFuncV2 func(ctx context.Context, resourceID, location string) (bool, error)

// WaitForResourceToBeDeletedV2 - keeps retrying until the resource is not found, or until err is thrown, or until ctx is cancelled
// V2 signature is different because it is written for the terraform-plugin-framework
func WaitForResourceToBeDeletedV2(ctx context.Context, timeout time.Duration, resourceID, location string, fn IsResourceDeletedFuncV2) error {
return retry.RetryContext(ctx, timeout, func() *retry.RetryError {
isDeleted, err := fn(ctx, resourceID, location)
if isDeleted {
return nil
}
if err != nil {
return retry.NonRetryableError(err)
}
return retry.RetryableError(fmt.Errorf("resource with ID %s not deleted yet, still trying ", resourceID))
})
}

// WaitForResourceToBeDeleted - keeps retrying until resource is not found(404), or until ctx is cancelled
func WaitForResourceToBeDeleted(ctx context.Context, d *schema.ResourceData, fn IsResourceDeletedFunc) error {

Expand Down