diff --git a/CHANGELOG.md b/CHANGELOG.md index ac7b31c29..eb5783a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ ENHANCEMENTS: FEATURES: * `r/tfe_team`: Add attributes `manage_teams`, `manage_organization_access`, and `access_secret_teams` to `organization_access` on `tfe_team` by @juliannatetreault [#1313](https://github.com/hashicorp/terraform-provider-tfe/pull/1313) +FEATURES: +* **New Data Source**: `d/tfe_projects` is a new data source to retrieve all projects in an organization, by @tdevelioglu + ## v0.54.0 ENHANCEMENTS: diff --git a/internal/provider/data_source_projects.go b/internal/provider/data_source_projects.go new file mode 100644 index 000000000..04dd01c23 --- /dev/null +++ b/internal/provider/data_source_projects.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &dataSourceTFEProjects{} + _ datasource.DataSourceWithConfigure = &dataSourceTFEProjects{} +) + +// NewProjectsDataSource is a helper function to simplify the provider implementation. +func NewProjectsDataSource() datasource.DataSource { + return &dataSourceTFEProjects{} +} + +// dataSourceTFEProjects is the data source implementation. +type dataSourceTFEProjects struct { + config ConfiguredClient +} + +// modelTFEProjects maps the data source schema data. +type modelTFEProjects struct { + ID types.String `tfsdk:"id"` + Organization types.String `tfsdk:"organization"` + Projects []modelTFEProject `tfsdk:"projects"` +} + +// Metadata returns the data source type name. +func (d *dataSourceTFEProjects) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_projects" +} + +// Schema defines the schema for the data source. +func (d *dataSourceTFEProjects) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "This data source can be used to retrieve all projects in an organization.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "organization": schema.StringAttribute{ + Description: "Name of the organization. If omitted, organization must be defined in the provider config.", + Optional: true, + Computed: true, + }, + "projects": schema.ListAttribute{ + Description: "List of Projects in the organization.", + Computed: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "name": types.StringType, + "description": types.StringType, + "organization": types.StringType, + }, + }, + }, + }, + } +} + +// Configure adds the provider configured client to the data source. +func (d *dataSourceTFEProjects) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(ConfiguredClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData), + ) + + return + } + d.config = client +} + +// Read refreshes the Terraform state with the latest data. +func (d *dataSourceTFEProjects) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var model modelTFEProjects // The model is what we save to the state + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + + if resp.Diagnostics.HasError() { + return + } + + var organization string + resp.Diagnostics.Append(d.config.dataOrDefaultOrganization(ctx, req.Config, &organization)...) + + if resp.Diagnostics.HasError() { + return + } + + options := tfe.ProjectListOptions{ + ListOptions: tfe.ListOptions{ + PageSize: 100, + }, + } + tflog.Debug(ctx, "Listing projects") + projectList, err := d.config.Client.Projects.List(ctx, organization, &options) + if err != nil { + resp.Diagnostics.AddError("Unable to list projects", err.Error()) + return + } + + model.ID = types.StringValue(organization) + model.Organization = types.StringValue(organization) + model.Projects = []modelTFEProject{} + + for { // paginate + for _, project := range projectList.Items { + model.Projects = append(model.Projects, modelFromTFEProject(project)) + } + + if projectList.CurrentPage >= projectList.TotalPages { + break + } + options.PageNumber = projectList.NextPage + + tflog.Debug(ctx, "Listing projects") + projectList, err = d.config.Client.Projects.List(ctx, organization, &options) + if err != nil { + resp.Diagnostics.AddError("Unable to list projects", err.Error()) + return + } + } + + // Save model into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} diff --git a/internal/provider/data_source_projects_test.go b/internal/provider/data_source_projects_test.go new file mode 100644 index 000000000..dc860ce71 --- /dev/null +++ b/internal/provider/data_source_projects_test.go @@ -0,0 +1,133 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccTFEProjectsDataSource_basic(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + orgName := fmt.Sprintf("tst-terraform-%d", rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEProjectsDataSourceConfig(orgName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "organization", orgName), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.#", "4"), + resource.TestCheckResourceAttrSet( + "data.tfe_projects.all", "projects.0.id"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.0.name", "Default Project"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.0.description", ""), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.0.organization", orgName), + resource.TestCheckResourceAttrSet( + "data.tfe_projects.all", "projects.1.id"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.1.name", "project1"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.1.description", "Project 1"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.1.organization", orgName), + resource.TestCheckResourceAttrSet( + "data.tfe_projects.all", "projects.2.id"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.2.name", "project2"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.2.description", "Project 2"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.2.organization", orgName), + resource.TestCheckResourceAttrSet( + "data.tfe_projects.all", "projects.3.id"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.3.name", "project3"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.3.description", "Project 3"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.3.organization", orgName), + ), + }, + }, + }) +} + +func TestAccTFEProjectsDataSource_basicNoProjects(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + orgName := fmt.Sprintf("tst-terraform-%d", rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEProjectsDataSourceConfig_noProjects(orgName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "organization", orgName), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.#", "1"), + resource.TestCheckResourceAttr( + "data.tfe_projects.all", "projects.0.name", "Default Project"), + ), + }, + }, + }) +} + +func testAccTFEProjectsDataSourceConfig(orgName string) string { + return fmt.Sprintf(` +resource "tfe_organization" "organization" { + name = "%s" + email = "admin@tfe.local" +} + +resource "tfe_project" "project1" { + name = "project1" + description = "Project 1" + organization = tfe_organization.organization.name +} + +resource "tfe_project" "project2" { + name = "project2" + description = "Project 2" + organization = tfe_organization.organization.name +} + +resource "tfe_project" "project3" { + name = "project3" + description = "Project 3" + organization = tfe_organization.organization.name +} + +data tfe_projects "all" { + organization = tfe_organization.organization.name +} +`, orgName) +} + +func testAccTFEProjectsDataSourceConfig_noProjects(orgName string) string { + return fmt.Sprintf(` +resource "tfe_organization" "organization" { + name = "%s" + email = "admin@tfe.local" +} + +data tfe_projects "all" { + organization = tfe_organization.organization.name +} +`, orgName) +} diff --git a/internal/provider/project.go b/internal/provider/project.go new file mode 100644 index 000000000..c29b01d51 --- /dev/null +++ b/internal/provider/project.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// modelTFEProject maps the resource or data source schema data to a +// struct. +type modelTFEProject struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Organization types.String `tfsdk:"organization"` +} + +// modelFromTFEProject builds a modelTFEProject struct from a +// tfe.Project value. +func modelFromTFEProject(v *tfe.Project) modelTFEProject { + return modelTFEProject{ + ID: types.StringValue(v.ID), + Name: types.StringValue(v.Name), + Description: types.StringValue(v.Description), + Organization: types.StringValue(v.Organization.Name), + } +} diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index 443e66823..5025d6b58 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -124,6 +124,7 @@ func (p *frameworkProvider) Configure(ctx context.Context, req provider.Configur func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ NewOrganizationRunTaskDataSource, + NewProjectsDataSource, NewRegistryGPGKeyDataSource, NewRegistryGPGKeysDataSource, NewRegistryProviderDataSource, diff --git a/website/docs/d/projects.html.markdown b/website/docs/d/projects.html.markdown new file mode 100644 index 000000000..687dc5e88 --- /dev/null +++ b/website/docs/d/projects.html.markdown @@ -0,0 +1,33 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_projects" +description: |- + Get information on projects in an organization. +--- + +# Data Source: tfe_projects + +Use this data source to get information about all projects in an organization. + +## Example Usage + +```hcl +data "tfe_projects" "all" { + organization = "my-org-name" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `organization` - (Optional) Name of the organization. If omitted, organization must be defined in the provider config. + +## Attributes Reference + +* `projects` - List of projects in the organization. Each element contains the following attributes: + * `id` - ID of the project. + * `name` - Name of the project. + * `description` - Description of the organization. + * `organization` - Name of the organization. +