Skip to content

Commit

Permalink
Add tfe_projects data source
Browse files Browse the repository at this point in the history
Can be used to retrieve all projects in an organization.
  • Loading branch information
Taylan Develioglu committed Apr 26, 2024
1 parent c842c97 commit 28d86c7
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## UNRELEASED

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:
Expand Down
147 changes: 147 additions & 0 deletions internal/provider/data_source_projects.go
Original file line number Diff line number Diff line change
@@ -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)...)
}
133 changes: 133 additions & 0 deletions internal/provider/data_source_projects_test.go
Original file line number Diff line number Diff line change
@@ -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 = "[email protected]"
}
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 = "[email protected]"
}
data tfe_projects "all" {
organization = tfe_organization.organization.name
}
`, orgName)
}
29 changes: 29 additions & 0 deletions internal/provider/project.go
Original file line number Diff line number Diff line change
@@ -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),
}
}
1 change: 1 addition & 0 deletions internal/provider/provider_next.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions website/docs/d/projects.html.markdown
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 28d86c7

Please sign in to comment.