diff --git a/docs/data-sources/sfs_snapshot_policies.md b/docs/data-sources/sfs_snapshot_policies.md new file mode 100644 index 000000000..545b8970c --- /dev/null +++ b/docs/data-sources/sfs_snapshot_policies.md @@ -0,0 +1,65 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_sfs_snapshot_policies Data Source - stackit" +subcategory: "" +description: |- + SFS snapshot policies datasource schema +--- + +# stackit_sfs_snapshot_policies (Data Source) + +SFS snapshot policies datasource schema + +## Example Usage + +```terraform +data "stackit_sfs_snapshot_policies" "all" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +data "stackit_sfs_snapshot_policies" "immutable_only" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + immutable = true +} +``` + + +## Schema + +### Required + +- `project_id` (String) STACKIT project ID to which the snapshot policy is associated. + +### Optional + +- `immutable` (Boolean) List only immutable snapshot policies. + +### Read-Only + +- `id` (String) Terraform's internal data source ID. It is structured as "`project_id`". +- `items` (Attributes List) (see [below for nested schema](#nestedatt--items)) + + +### Nested Schema for `items` + +Read-Only: + +- `comment` (String) Comment of the Snapshot Policy. +- `created_at` (String) Created At timestamp. +- `enabled` (Boolean) Wether the Snapshot Policy is enabled. +- `id` (String) ID of the Snapshot Policy. +- `name` (String) Name of the Snapshot Policy. +- `snapshot_schedules` (Attributes List) (see [below for nested schema](#nestedatt--items--snapshot_schedules)) + + +### Nested Schema for `items.snapshot_schedules` + +Read-Only: + +- `created_at` (String) Created At timestamp. +- `id` (String) ID of the Snapshot Schedule. +- `interval` (String) Interval of the Snapshot Schedule (follows the cron schedule xpression in Unix-like systems). +- `name` (String) Name of the Snapshot Schedule. +- `prefix` (String) Prefix used for snapshots created by this policy. +- `retention_count` (Number) Retention Count. +- `retention_period` (String) Retention Period (ISO 8601 format or 'infinite'). diff --git a/examples/data-sources/stackit_sfs_snapshot_policies/data-source.tf b/examples/data-sources/stackit_sfs_snapshot_policies/data-source.tf new file mode 100644 index 000000000..0e9bdee84 --- /dev/null +++ b/examples/data-sources/stackit_sfs_snapshot_policies/data-source.tf @@ -0,0 +1,8 @@ +data "stackit_sfs_snapshot_policies" "all" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +data "stackit_sfs_snapshot_policies" "immutable_only" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + immutable = true +} \ No newline at end of file diff --git a/stackit/internal/services/sfs/sfs_acc_test.go b/stackit/internal/services/sfs/sfs_acc_test.go index 546df69ea..88688f36c 100644 --- a/stackit/internal/services/sfs/sfs_acc_test.go +++ b/stackit/internal/services/sfs/sfs_acc_test.go @@ -44,6 +44,9 @@ var ( //go:embed testdata/project-lock-min.tf resourceProjectLockConfig string + + //go:embed testdata/snapshot-policies-datasource.tf + snapshotPoliciesDataSourceConfig string ) // EXPORT POLICY - MIN @@ -904,6 +907,51 @@ func TestAccProjectLockMin(t *testing.T) { }) } +func TestAccSnapshotPolicies(t *testing.T) { + projectId := config.StringVariable(testutil.ProjectId) + cfg := fmt.Sprintf(` +%s + +%s`, testutil.NewConfigBuilder().BuildProviderConfig(), snapshotPoliciesDataSourceConfig) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // all + { + Config: cfg, + ConfigVariables: config.Variables{ + "project_id": projectId, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_sfs_snapshot_policies.snapshot_policies", "project_id", testutil.ProjectId), + ), + }, + // immutable + { + Config: cfg, + ConfigVariables: config.Variables{ + "project_id": projectId, + "immutable": config.BoolVariable(true), + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_sfs_snapshot_policies.snapshot_policies", "project_id", testutil.ProjectId), + ), + }, + // mutable + { + Config: cfg, + ConfigVariables: config.Variables{ + "project_id": projectId, + "immutable": config.BoolVariable(false), + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_sfs_snapshot_policies.snapshot_policies", "project_id", testutil.ProjectId), + ), + }, + }, + }) +} + func createClient() (*sfs.APIClient, error) { client, err := sfs.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.SFSCustomEndpoint, false)...) if err != nil { diff --git a/stackit/internal/services/sfs/snapshot-policy/list_resource.go b/stackit/internal/services/sfs/snapshot-policy/list_resource.go new file mode 100644 index 000000000..9cd6a28dc --- /dev/null +++ b/stackit/internal/services/sfs/snapshot-policy/list_resource.go @@ -0,0 +1 @@ +package snapshot_policy diff --git a/stackit/internal/services/sfs/snapshot-policy/policies_datasource.go b/stackit/internal/services/sfs/snapshot-policy/policies_datasource.go new file mode 100644 index 000000000..a695fc149 --- /dev/null +++ b/stackit/internal/services/sfs/snapshot-policy/policies_datasource.go @@ -0,0 +1,257 @@ +package snapshot_policy + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + sfsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sfs/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSource = &policiesDataSource{} +) + +func NewSnapshotPoliciesDataSource() datasource.DataSource { + return &policiesDataSource{} +} + +type policiesDataSource struct { + client *sfs.APIClient + providerData core.ProviderData +} + +func (r *policiesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_sfs_snapshot_policies" +} + +func (r *policiesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := sfsUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "SFS client configured.") +} + +func (r *policiesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "SFS snapshot policies datasource schema", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal data source ID. It is structured as \"`project_id`\".", + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "STACKIT project ID to which the snapshot policy is associated.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "immutable": schema.BoolAttribute{ + Description: "List only immutable snapshot policies.", + Optional: true, + }, + "items": schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "ID of the Snapshot Policy.", + }, + "name": schema.StringAttribute{ + Computed: true, + Description: "Name of the Snapshot Policy.", + }, + "comment": schema.StringAttribute{ + Computed: true, + Description: "Comment of the Snapshot Policy.", + }, + "enabled": schema.BoolAttribute{ + Computed: true, + Description: "Wether the Snapshot Policy is enabled.", + }, + "created_at": schema.StringAttribute{ + Computed: true, + Description: "Created At timestamp.", + }, + "snapshot_schedules": schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "ID of the Snapshot Schedule.", + }, + "name": schema.StringAttribute{ + Computed: true, + Description: "Name of the Snapshot Schedule.", + }, + "created_at": schema.StringAttribute{ + Computed: true, + Description: "Created At timestamp.", + }, + "interval": schema.StringAttribute{ + Computed: true, + Description: "Interval of the Snapshot Schedule (follows the cron schedule xpression in Unix-like systems).", + }, + "prefix": schema.StringAttribute{ + Computed: true, + Description: "Prefix used for snapshots created by this policy.", + }, + "retention_count": schema.Int64Attribute{ + Computed: true, + Description: "Retention Count.", + }, + "retention_period": schema.StringAttribute{ + Computed: true, + Description: "Retention Period (ISO 8601 format or 'infinite').", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +type model struct { + ID types.String `tfsdk:"id"` + ProjectId types.String `tfsdk:"project_id"` + Immutable types.Bool `tfsdk:"immutable"` + Items []policy `tfsdk:"items"` +} + +type policy struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Comment types.String `tfsdk:"comment"` + Enabled types.Bool `tfsdk:"enabled"` + CreatedAt types.String `tfsdk:"created_at"` + SnapshotSchedules []schedule `tfsdk:"snapshot_schedules"` +} + +type schedule struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + CreatedAt types.String `tfsdk:"created_at"` + Interval types.String `tfsdk:"interval"` + Prefix types.String `tfsdk:"prefix"` + RetentionCount types.Int64 `tfsdk:"retention_count"` + RetentionPeriod types.String `tfsdk:"retention_period"` +} + +func (r *policiesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model model + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + + ctx = tflog.SetField(ctx, "project_id", projectId) + + listRequest := r.client.DefaultAPI.ListSnapshotPolicies(ctx, projectId) + if !utils.IsUndefined(model.Immutable) { + listRequest = listRequest.Immutable(model.Immutable.ValueBool()) + + title := `The "immutable" attribute of the "stackit_sfs_snapshot_policies" data source is in beta` + content := `This attribute may be subject to breaking changes in the future. Use with caution.` + tflog.Warn(ctx, fmt.Sprintf(`%s | %s`, title, content)) + diags.AddWarning(title, content) + } + policies, err := listRequest.Execute() + if err != nil { + core.LogAndAddError(ctx, &diags, "Error listing snapshot policies", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, policies, &model) + if err != nil { + core.LogAndAddError(ctx, &diags, "Error reading snapshot policies", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "SFS snapshot policies read") +} + +func mapFields(_ context.Context, resp *sfs.ListSnapshotPoliciesResponse, model *model) error { + if resp == nil || resp.SnapshotPolicies == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + projectID := model.ProjectId.ValueString() + + model.ID = utils.BuildInternalTerraformId(projectID) + + for _, respPolicy := range resp.SnapshotPolicies { + var createdAt types.String + if respPolicy.CreatedAt != nil { + createdAt = types.StringValue(respPolicy.CreatedAt.String()) + } + modelPolicy := policy{ + ID: types.StringPointerValue(respPolicy.Id), + Name: types.StringPointerValue(respPolicy.Name), + Comment: types.StringPointerValue(respPolicy.Comment), + Enabled: types.BoolPointerValue(respPolicy.Enabled), + CreatedAt: createdAt, + } + for _, respSchedule := range respPolicy.SnapshotSchedules { + var scheduleCreatedAt types.String + if respPolicy.CreatedAt != nil { + scheduleCreatedAt = types.StringValue(respSchedule.CreatedAt.String()) + } + var retentionCount *int64 + if respSchedule.RetentionCount != nil { + retentionCount = new(int64(*respSchedule.RetentionCount)) + } + modelSchedule := schedule{ + ID: types.StringPointerValue(respSchedule.Id), + Name: types.StringPointerValue(respSchedule.Name), + CreatedAt: scheduleCreatedAt, + Interval: types.StringPointerValue(respSchedule.Interval), + Prefix: types.StringPointerValue(respSchedule.Prefix), + RetentionCount: types.Int64PointerValue(retentionCount), + RetentionPeriod: types.StringPointerValue(respSchedule.RetentionPeriod), + } + modelPolicy.SnapshotSchedules = append(modelPolicy.SnapshotSchedules, modelSchedule) + } + model.Items = append(model.Items, modelPolicy) + } + return nil +} diff --git a/stackit/internal/services/sfs/snapshot-policy/policies_datasource_test.go b/stackit/internal/services/sfs/snapshot-policy/policies_datasource_test.go new file mode 100644 index 000000000..79bc44782 --- /dev/null +++ b/stackit/internal/services/sfs/snapshot-policy/policies_datasource_test.go @@ -0,0 +1,137 @@ +package snapshot_policy + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" +) + +func TestMapFields(t *testing.T) { + now := time.Now() + tests := []struct { + name string + resp *sfs.ListSnapshotPoliciesResponse + want *model + wantErr bool + }{ + { + name: "nil response", + resp: nil, + want: &model{}, + wantErr: true, + }, + { + name: "some values", + resp: &sfs.ListSnapshotPoliciesResponse{ + SnapshotPolicies: []sfs.SnapshotPolicy{ + { + Comment: new("comment"), + CreatedAt: new(now), + Enabled: new(true), + Id: new("id"), + Name: new("name"), + SnapshotSchedules: []sfs.SnapshotPolicySnapshotPolicySchedule{ + { + CreatedAt: new(now), + Id: new("id"), + Interval: new("interval"), + Name: new("name"), + Prefix: new("prefix"), + RetentionCount: new(int32(123)), + RetentionPeriod: new("period"), + }, + }, + }, + }, + }, + want: &model{ + ID: types.StringValue(""), + Items: []policy{ + { + ID: types.StringValue("id"), + Name: types.StringValue("name"), + Comment: types.StringValue("comment"), + Enabled: types.BoolValue(true), + CreatedAt: types.StringValue(now.String()), + SnapshotSchedules: []schedule{ + { + ID: types.StringValue("id"), + Name: types.StringValue("name"), + CreatedAt: types.StringValue(now.String()), + Interval: types.StringValue("interval"), + Prefix: types.StringValue("prefix"), + RetentionCount: types.Int64Value(123), + RetentionPeriod: types.StringValue("period"), + }, + }, + }, + }, + }, + }, + { + name: "nil values policy", + resp: &sfs.ListSnapshotPoliciesResponse{ + SnapshotPolicies: []sfs.SnapshotPolicy{ + {}, + }, + }, + want: &model{ + ID: types.StringValue(""), + Items: []policy{ + { + ID: types.String{}, + Name: types.String{}, + Comment: types.String{}, + Enabled: types.Bool{}, + CreatedAt: types.String{}, + SnapshotSchedules: nil, + }, + }, + }, + }, + { + name: "nil values schedule", + resp: &sfs.ListSnapshotPoliciesResponse{ + SnapshotPolicies: []sfs.SnapshotPolicy{ + { + SnapshotSchedules: []sfs.SnapshotPolicySnapshotPolicySchedule{ + {}, + }, + }, + }, + }, + want: &model{ + ID: types.StringValue(""), + Items: []policy{ + { + SnapshotSchedules: []schedule{ + {}, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &model{} + err := mapFields(t.Context(), tt.resp, m) + if err != nil && !tt.wantErr { + t.Errorf("unexpected error: %v", err) + } + if err == nil && tt.wantErr { + t.Errorf("unexpected error: %v", err) + } + if err != nil { + return + } + if diff := cmp.Diff(tt.want, m); diff != "" { + t.Errorf("Data does not match: %s", diff) + } + }) + } +} diff --git a/stackit/internal/services/sfs/testdata/snapshot-policies-datasource.tf b/stackit/internal/services/sfs/testdata/snapshot-policies-datasource.tf new file mode 100644 index 000000000..3c7b63362 --- /dev/null +++ b/stackit/internal/services/sfs/testdata/snapshot-policies-datasource.tf @@ -0,0 +1,9 @@ +variable "project_id" {} +variable "immutable" { + default = null +} + +data "stackit_sfs_snapshot_policies" "snapshot_policies" { + project_id = var.project_id + immutable = var.immutable +} \ No newline at end of file diff --git a/stackit/provider.go b/stackit/provider.go index 01b56db9c..216cbce00 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -112,6 +112,7 @@ import ( projectLock "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sfs/project-lock" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sfs/resourcepool" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sfs/share" + snapshotPolicy "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sfs/snapshot-policy" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sfs/snapshots" skeCluster "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/cluster" skeKubeconfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/kubeconfig" @@ -697,6 +698,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource share.NewShareDataSource, exportpolicy.NewExportPolicyDataSource, snapshots.NewResourcePoolSnapshotDataSource, + snapshotPolicy.NewSnapshotPoliciesDataSource, projectLock.NewProjectLockDatasource, compliancelock.NewComplianceLockDataSource, serverBackupEnable.NewServerBackupEnableDataSource,