diff --git a/docs/data-sources/iaas_project.md b/docs/data-sources/iaas_project.md new file mode 100644 index 000000000..919318df8 --- /dev/null +++ b/docs/data-sources/iaas_project.md @@ -0,0 +1,35 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_iaas_project Data Source - stackit" +subcategory: "" +description: |- + Project details. Must have a region specified in the provider configuration. +--- + +# stackit_iaas_project (Data Source) + +Project details. Must have a `region` specified in the provider configuration. + +## Example Usage + +```terraform +data "stackit_iaas_project" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `project_id` (String) STACKIT project ID. + +### Read-Only + +- `area_id` (String) The area ID to which the project belongs to. +- `created_at` (String) Date-time when the project was created. +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`". +- `internet_access` (Boolean) Specifies if the project has internet_access +- `state` (String) Specifies the state of the project. +- `updated_at` (String) Date-time when the project was last updated. diff --git a/examples/data-sources/stackit_iaas_project/data-source.tf b/examples/data-sources/stackit_iaas_project/data-source.tf new file mode 100644 index 000000000..cb5e87f02 --- /dev/null +++ b/examples/data-sources/stackit_iaas_project/data-source.tf @@ -0,0 +1,3 @@ +data "stackit_iaas_project" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} \ No newline at end of file diff --git a/go.mod b/go.mod index f33be72d6..064c9c244 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/cdn v1.4.0 github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 github.com/stackitcloud/stackit-sdk-go/services/git v0.7.1 - github.com/stackitcloud/stackit-sdk-go/services/iaas v0.29.0 + github.com/stackitcloud/stackit-sdk-go/services/iaas v0.29.1 github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.5.1 github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.1 diff --git a/go.sum b/go.sum index 46c5cc799..c40e154a0 100644 --- a/go.sum +++ b/go.sum @@ -160,8 +160,8 @@ github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 h1:CnhAMLql0MNmAeq4r github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1/go.mod h1:7Bx85knfNSBxulPdJUFuBePXNee3cO+sOTYnUG6M+iQ= github.com/stackitcloud/stackit-sdk-go/services/git v0.7.1 h1:hkFixFnBcQzU4BSIZFITc8N0gK0pUYk7mk0wdUu5Ki8= github.com/stackitcloud/stackit-sdk-go/services/git v0.7.1/go.mod h1:Ng1EzrRndG3iGXGH90AZJz//wfK+2YOyDwTnTLwX3a4= -github.com/stackitcloud/stackit-sdk-go/services/iaas v0.29.0 h1:j4FKFOVkcTot8xNxpvDsPzIFyjADE4GxXF0rFE6/Uo4= -github.com/stackitcloud/stackit-sdk-go/services/iaas v0.29.0/go.mod h1:b/jgJf7QHdRzU2fmZeJJtu5j0TAevDRghzcn5MyRmOI= +github.com/stackitcloud/stackit-sdk-go/services/iaas v0.29.1 h1:GfE+FaeIKSVaKvgzh8Eacum+bQVyRS6ngltkh0qNGtM= +github.com/stackitcloud/stackit-sdk-go/services/iaas v0.29.1/go.mod h1:b/jgJf7QHdRzU2fmZeJJtu5j0TAevDRghzcn5MyRmOI= github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha h1:m1jq6a8dbUe+suFuUNdHmM/cSehpGLUtDbK1CqLqydg= github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha/go.mod h1:Nu1b5Phsv8plgZ51+fkxPVsU91ZJ5Ayz+cthilxdmQ8= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.5.1 h1:OdJEs8eOfrzn9tCBDLxIyP8hX50zPfcXNYnRoQX+chs= diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go index 38b04894f..3c1c4f649 100644 --- a/stackit/internal/services/iaas/iaas_acc_test.go +++ b/stackit/internal/services/iaas/iaas_acc_test.go @@ -4022,6 +4022,38 @@ func TestAccImageMax(t *testing.T) { }) } +func TestAccProject(t *testing.T) { + projectId := testutil.ProjectId + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Data source + { + ConfigVariables: testConfigKeyPairMin, + Config: fmt.Sprintf(` + %s + + data "stackit_iaas_project" "project" { + project_id = %q + } + `, + testutil.IaaSProviderConfig(), testutil.ProjectId, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Instance + resource.TestCheckResourceAttr("data.stackit_iaas_project.project", "project_id", projectId), + resource.TestCheckResourceAttr("data.stackit_iaas_project.project", "id", projectId), + resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "area_id"), + resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "internet_access"), + resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "state"), + resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "created_at"), + resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "updated_at"), + ), + }, + }, + }) +} + func testAccCheckDestroy(s *terraform.State) error { checkFunctions := []func(s *terraform.State) error{ testAccCheckNetworkV1Destroy, diff --git a/stackit/internal/services/iaas/project/datasource.go b/stackit/internal/services/iaas/project/datasource.go new file mode 100644 index 000000000..c5be5e8a0 --- /dev/null +++ b/stackit/internal/services/iaas/project/datasource.go @@ -0,0 +1,204 @@ +package project + +import ( + "context" + "fmt" + "time" + + "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-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSourceWithConfigure = &projectDataSource{} +) + +type DatasourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + AreaId types.String `tfsdk:"area_id"` + InternetAccess types.Bool `tfsdk:"internet_access"` + State types.String `tfsdk:"state"` + CreatedAt types.String `tfsdk:"created_at"` + UpdatedAt types.String `tfsdk:"updated_at"` +} + +// NewProjectDataSource is a helper function to simplify the provider implementation. +func NewProjectDataSource() datasource.DataSource { + return &projectDataSource{} +} + +// projectDatasource is the data source implementation. +type projectDataSource struct { + client *iaas.APIClient +} + +func (d *projectDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") +} + +// Metadata returns the data source type name. +func (d *projectDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_iaas_project" +} + +// Schema defines the schema for the datasource. +func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Project details. Must have a `region` specified in the provider configuration.", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`\".", + "project_id": "STACKIT project ID.", + "area_id": "The area ID to which the project belongs to.", + "internet_access": "Specifies if the project has internet_access", + "state": "Specifies the state of the project.", + "created_at": "Date-time when the project was created.", + "updated_at": "Date-time when the project was last updated.", + } + resp.Schema = schema.Schema{ + MarkdownDescription: descriptions["main"], + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "area_id": schema.StringAttribute{ + Description: descriptions["area_id"], + Computed: true, + }, + "internet_access": schema.BoolAttribute{ + Description: descriptions["internet_access"], + Computed: true, + }, + "state": schema.StringAttribute{ + Description: descriptions["state"], + Computed: true, + }, + "created_at": schema.StringAttribute{ + Description: descriptions["created_at"], + Computed: true, + }, + "updated_at": schema.StringAttribute{ + Description: descriptions["updated_at"], + Computed: true, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model DatasourceModel + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + + projectResp, err := d.client.GetProjectDetailsExecute(ctx, projectId) + if err != nil { + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading project", + fmt.Sprintf("Project with ID %q does not exists.", projectId), + nil, + ) + resp.State.RemoveResource(ctx) + return + } + + // Map response body to schema + err = mapDataSourceFields(projectResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Process API payload: %v", err)) + return + } + // Set refreshed state + diags = resp.State.Set(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "project read") +} + +func mapDataSourceFields(projectResp *iaas.Project, model *DatasourceModel) error { + if projectResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var projectId string + if model.ProjectId.ValueString() != "" { + projectId = model.ProjectId.ValueString() + } else if projectResp.ProjectId != nil { + projectId = *projectResp.ProjectId + } else { + return fmt.Errorf("project id is not present") + } + + model.Id = utils.BuildInternalTerraformId(projectId) + model.ProjectId = types.StringValue(projectId) + + var areaId basetypes.StringValue + if projectResp.AreaId != nil { + if projectResp.AreaId.String != nil { + areaId = types.StringPointerValue(projectResp.AreaId.String) + } else if projectResp.AreaId.StaticAreaID != nil { + areaId = types.StringValue(string(*projectResp.AreaId.StaticAreaID)) + } + } + + var createdAt basetypes.StringValue + if projectResp.CreatedAt != nil { + createdAtValue := *projectResp.CreatedAt + createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) + } + + var updatedAt basetypes.StringValue + if projectResp.UpdatedAt != nil { + updatedAtValue := *projectResp.UpdatedAt + updatedAt = types.StringValue(updatedAtValue.Format(time.RFC3339)) + } + + model.AreaId = areaId + model.InternetAccess = types.BoolPointerValue(projectResp.InternetAccess) + model.State = types.StringPointerValue(projectResp.State) + model.CreatedAt = createdAt + model.UpdatedAt = updatedAt + return nil +} diff --git a/stackit/internal/services/iaas/project/datasource_test.go b/stackit/internal/services/iaas/project/datasource_test.go new file mode 100644 index 000000000..adbd5ec26 --- /dev/null +++ b/stackit/internal/services/iaas/project/datasource_test.go @@ -0,0 +1,120 @@ +package project + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + testTimestampValue = "2006-01-02T15:04:05Z" +) + +func testTimestamp() time.Time { + timestamp, _ := time.Parse(time.RFC3339, testTimestampValue) + return timestamp +} + +func TestMapDataSourceFields(t *testing.T) { + const projectId = "pid" + tests := []struct { + description string + state *DatasourceModel + input *iaas.Project + expected *DatasourceModel + isValid bool + }{ + { + description: "default_values", + state: &DatasourceModel{ + ProjectId: types.StringValue(projectId), + }, + input: &iaas.Project{ + ProjectId: utils.Ptr(projectId), + }, + expected: &DatasourceModel{ + Id: types.StringValue(projectId), + ProjectId: types.StringValue(projectId), + }, + isValid: true, + }, + { + description: "simple_values", + state: &DatasourceModel{ + ProjectId: types.StringValue(projectId), + }, + input: &iaas.Project{ + AreaId: utils.Ptr(iaas.AreaId{String: utils.Ptr("aid")}), + CreatedAt: utils.Ptr(testTimestamp()), + InternetAccess: utils.Ptr(true), + OpenstackProjectId: utils.Ptr("oid"), + ProjectId: utils.Ptr(projectId), + State: utils.Ptr("CREATED"), + UpdatedAt: utils.Ptr(testTimestamp()), + }, + expected: &DatasourceModel{ + Id: types.StringValue(projectId), + ProjectId: types.StringValue(projectId), + AreaId: types.StringValue("aid"), + InternetAccess: types.BoolValue(true), + State: types.StringValue("CREATED"), + CreatedAt: types.StringValue(testTimestampValue), + UpdatedAt: types.StringValue(testTimestampValue), + }, + isValid: true, + }, + { + description: "static_area_id", + state: &DatasourceModel{ + ProjectId: types.StringValue(projectId), + }, + input: &iaas.Project{ + AreaId: utils.Ptr(iaas.AreaId{ + StaticAreaID: iaas.STATICAREAID_PUBLIC.Ptr(), + }), + ProjectId: utils.Ptr(projectId), + }, + expected: &DatasourceModel{ + Id: types.StringValue(projectId), + ProjectId: types.StringValue(projectId), + AreaId: types.StringValue("PUBLIC"), + }, + isValid: true, + }, + { + description: "response_nil_fail", + state: &DatasourceModel{}, + input: nil, + expected: &DatasourceModel{}, + isValid: false, + }, + { + description: "no_project_id_fail", + state: &DatasourceModel{}, + input: &iaas.Project{}, + expected: &DatasourceModel{}, + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapDataSourceFields(tt.input, tt.state) + if !tt.isValid && err == nil { + t.Fatal("should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.expected, tt.state) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/provider.go b/stackit/provider.go index c7d0953bc..f1079bec3 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -32,6 +32,7 @@ import ( iaasNetworkAreaRoute "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearoute" iaasNetworkInterface "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterface" iaasNetworkInterfaceAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterfaceattach" + iaasProject "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/project" iaasPublicIp "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/publicip" iaasPublicIpAssociate "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/publicipassociate" iaasPublicIpRanges "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/publicipranges" @@ -460,6 +461,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource iaasNetworkAreaRoute.NewNetworkAreaRouteDataSource, iaasNetworkInterface.NewNetworkInterfaceDataSource, iaasVolume.NewVolumeDataSource, + iaasProject.NewProjectDataSource, iaasPublicIp.NewPublicIpDataSource, iaasPublicIpRanges.NewPublicIpRangesDataSource, iaasKeyPair.NewKeyPairDataSource,