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

Add Data Source: Organizations & Organization #320

Merged
merged 18 commits into from
May 27, 2021
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.25.4 (May 27, 2021)

FEATURES:
* **New Data Sources:** d/tfe_organizations, d/tfe_organization [#320](https://github.com/hashicorp/terraform-provider-tfe/pull/320)

## 0.25.3 (May 18, 2021)

BUG FIXES:
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ Please use the template below when updating the changelog:
* r/tfe_resource: description of change or bug fix ([#124](link-to-PR))
```

### Updating the documentation

For pull requests that update provider documentation, please help us verify that the
markdown will display correctly on the Registry:

- Copy the new markdown and paste it here to preview: https://registry.terraform.io/tools/doc-preview
- Paste a screenshot of that preview in your pull request.

### Change categories

- BREAKING CHANGES: Use this for any changes that aren't backwards compatible. Include details on how to handle these changes.
Expand Down
78 changes: 78 additions & 0 deletions tfe/data_source_organization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package tfe

import (
"fmt"
"log"

tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func dataSourceTFEOrganization() *schema.Resource {
return &schema.Resource{
Read: dataSourceTFEOrganizationRead,

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},

"external_id": {
Type: schema.TypeString,
Computed: true,
},

"collaborator_auth_policy": {
Type: schema.TypeString,
Computed: true,
},

"cost_estimation_enabled": {
Type: schema.TypeBool,
Computed: true,
},

"email": {
Type: schema.TypeString,
Computed: true,
},

"owners_team_saml_role_id": {
Type: schema.TypeString,
Computed: true,
},

"two_factor_conformant": {
Type: schema.TypeBool,
Computed: true,
},
},
}
}

func dataSourceTFEOrganizationRead(d *schema.ResourceData, meta interface{}) error {
tfeClient := meta.(*tfe.Client)

name := d.Get("name").(string)
log.Printf("[DEBUG] Read configuration for Organization: %s", name)
org, err := tfeClient.Organizations.Read(ctx, name)
if err != nil {
if err == tfe.ErrResourceNotFound {
return fmt.Errorf("Could not read organization '%s'", name)
}
return fmt.Errorf("Error retrieving organization: %v", err)
}

log.Printf("[DEBUG] Setting Organization Attributes")
d.SetId(org.ExternalID)
d.Set("name", org.Name)
d.Set("external_id", org.ExternalID)
d.Set("collaborator_auth_policy", org.CollaboratorAuthPolicy)
d.Set("cost_estimation_enabled", org.CostEstimationEnabled)
d.Set("email", org.Email)
d.Set("owners_team_saml_role_id", org.OwnersTeamSAMLRoleID)
d.Set("two_factor_conformant", org.TwoFactorConformant)

return nil
}
49 changes: 49 additions & 0 deletions tfe/data_source_organization_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package tfe

import (
"fmt"
"math/rand"
"testing"
"time"

tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccTFEOrganizationDataSource_basic(t *testing.T) {
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
org := &tfe.Organization{}
orgName := fmt.Sprintf("tst-terraform-foo-%d", rInt)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccTFEOrganizationDataSourceConfig_basic(rInt),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckTFEOrganizationExists("tfe_organization.foo", org),
// check resource attrs
resource.TestCheckResourceAttr("tfe_organization.foo", "name", orgName),
resource.TestCheckResourceAttr("tfe_organization.foo", "email", "admin@company.com"),

// check data attrs
resource.TestCheckResourceAttr("data.tfe_organization.foo", "name", orgName),
resource.TestCheckResourceAttr("data.tfe_organization.foo", "email", "admin@company.com"),
),
},
},
})
}

func testAccTFEOrganizationDataSourceConfig_basic(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "foo" {
name = "tst-terraform-foo-%d"
email = "admin@company.com"
}

data "tfe_organization" "foo" {
name = tfe_organization.foo.name
}`, rInt)
}
55 changes: 55 additions & 0 deletions tfe/data_source_organizations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package tfe

import (
"fmt"
"log"

tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func dataSourceTFEOrganizations() *schema.Resource {
return &schema.Resource{
Read: dataSourceTFEOrganizationList,

Schema: map[string]*schema.Schema{
"names": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if this is necessary? For workspaces, names is a required argument you must provide; here in organizations, you're just fetching all organizations accessible by the authenticated token.

Since ids (which matches the equivalent workspaces one) includes both names and IDs, this just feels like duplication, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I put names here as well because we use the organization's name as a primary identifier throughout the API, and I figured a common usage pattern is that users would query this data source and iterate through the names. That being said, one could also just iterate through ids (IDs/Names map) and do an action using the name.

Copy link
Member

Choose a reason for hiding this comment

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

Hmmm, I was actually wondering the opposite! We use id elsewhere in the provider to mean the organization's name, so it seems like it could be confusing to use id to mean the organization's external id 😬 plus you can't really use an organization's external id for anything right now, though of course that may change in the future.

Possibly terrible idea: what if we left ids out of this data source completely and had names as the only attribute?

Copy link
Member

Choose a reason for hiding this comment

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

That being said, one could also just iterate through ids (IDs/Names map) and do an action using the name.

Yeah that was my thought

The fact that we don't ever use external IDs for orgs is problematic, and I hope we make some enhancements to allow for either in the future and actually change things like the reference CJ linked, but I can also appreciate that it might be overoptimizing for now. I think names as the only attribute is just fine, actually! Maybe we could do that and leave the ID map for when (because we probably will) we migrate r/tfe_organization.id over 🤷‍♂️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The fact that we don't ever use external IDs for orgs is problematic

I'm in favor keeping names. The user is used to using the organization name to execute operations, so now seeing "ids" only (and not name) will be confusing. But like CJ pointed, we set id elsewhere as name where its not actually the ID (external_id) but in fact is a name...😢.

I'm actually in favor of keeping both. names because the user is already used to using that as the primary "id". And keeping the ids map, even though it may not be immediately used, is good because we can put the pieces in place for a future migration to start using external_id as the primary identifier instead of names.

Copy link
Member

Choose a reason for hiding this comment

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

Good discussion, sounds good to me!


"ids": {
Type: schema.TypeMap,
Computed: true,
},
},
}
}

func dataSourceTFEOrganizationList(d *schema.ResourceData, meta interface{}) error {
tfeClient := meta.(*tfe.Client)

log.Printf("[DEBUG] Listing all organizations")
orgs, err := tfeClient.Organizations.List(ctx, tfe.OrganizationListOptions{})
if err != nil {
if err == tfe.ErrResourceNotFound {
return fmt.Errorf("Could not list organizations.")
}
return fmt.Errorf("Error retrieving organizations: %v.", err)
}

names := []string{}
ids := map[string]string{}
for _, org := range orgs.Items {
ids[org.Name] = org.ExternalID
names = append(names, org.Name)
}

log.Printf("[DEBUG] Setting Organizations Attributes")
d.SetId("organizations")
d.Set("names", names)
d.Set("ids", ids)

return nil
}
112 changes: 112 additions & 0 deletions tfe/data_source_organizations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package tfe

import (
"fmt"
"math/rand"
"strconv"
"testing"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

func TestAccTFEOrganizationsDataSource_basic(t *testing.T) {
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccTFEOrganizationsDataSourceConfig_basic_resource(rInt),
},
{
Config: testAccTFEOrganizationsDataSourceConfig_basic_data(),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckTFEOrganizationHasNames("data.tfe_organizations.foobarbaz", []string{
fmt.Sprintf("tst-terraform-foo-%d", rInt),
fmt.Sprintf("tst-terraform-bar-%d", rInt),
fmt.Sprintf("tst-terraform-baz-%d", rInt),
}),
testAccCheckTFEOrganizationHasIDs("data.tfe_organizations.foobarbaz", []string{
fmt.Sprintf("tst-terraform-foo-%d", rInt),
fmt.Sprintf("tst-terraform-bar-%d", rInt),
fmt.Sprintf("tst-terraform-baz-%d", rInt),
}),
),
},
},
})
}

func testAccCheckTFEOrganizationHasNames(dataOrg string, orgNames []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
org, ok := s.RootModule().Resources[dataOrg]
if !ok {
return fmt.Errorf("Data organization '%s' not found.", dataOrg)
}
numOrgsStr := org.Primary.Attributes["names.#"]
numOrgs, _ := strconv.Atoi(numOrgsStr)

if numOrgs < len(orgNames) {
return fmt.Errorf("Expected at least %d organizations, but found %d.", len(orgNames), numOrgs)
}

allOrgsMap := map[string]struct{}{}
for i := 0; i < numOrgs; i++ {
orgName := org.Primary.Attributes[fmt.Sprintf("names.%d", i)]
allOrgsMap[orgName] = struct{}{}
}

for _, orgName := range orgNames {
_, ok := allOrgsMap[orgName]
if !ok {
return fmt.Errorf("Expected to find organization name %s, but did not.", orgName)
}
}

return nil
}
}

func testAccCheckTFEOrganizationHasIDs(dataOrg string, orgNames []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
org, ok := s.RootModule().Resources[dataOrg]
if !ok {
return fmt.Errorf("Organization not found: %s.", dataOrg)
}

for _, orgName := range orgNames {
id := fmt.Sprintf("ids.%s", orgName)
_, ok := org.Primary.Attributes[id]
if !ok {
return fmt.Errorf("Expected to find organization id %s, but did not.", id)
}
}

return nil
}
}

func testAccTFEOrganizationsDataSourceConfig_basic_resource(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "foo" {
name = "tst-terraform-foo-%d"
email = "admin@company.com"
}

resource "tfe_organization" "bar" {
name = "tst-terraform-bar-%d"
email = "admin@company.com"
}

resource "tfe_organization" "baz" {
name = "tst-terraform-baz-%d"
email = "admin@company.com"
}`, rInt, rInt, rInt)
}

func testAccTFEOrganizationsDataSourceConfig_basic_data() string {
return `data "tfe_organizations" "foobarbaz" {}`
}
2 changes: 2 additions & 0 deletions tfe/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ func Provider() *schema.Provider {
},

DataSourcesMap: map[string]*schema.Resource{
"tfe_organizations": dataSourceTFEOrganizations(),
"tfe_organization": dataSourceTFEOrganization(),
"tfe_agent_pool": dataSourceTFEAgentPool(),
"tfe_ip_ranges": dataSourceTFEIPRanges(),
"tfe_oauth_client": dataSourceTFEOAuthClient(),
Expand Down
36 changes: 36 additions & 0 deletions website/docs/d/organization.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
layout: "tfe"
page_title: "Terraform Enterprise: tfe_organization"
sidebar_current: "docs-datasource-tfe-organization"
description: |-
Get information on an Organization.
---

# Data Source: tfe_organization

Use this data source to get information about an organization.

## Example Usage

```hcl
data "tfe_organization" "foo" {
name = "organization-name"
}
```

## Argument Reference

The following arguments are supported:
* `name` - (Required) Name of the organization.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:

* `name` - Name of the organization.
* `email` - Admin email address.
* `external_id` - An identifier for the organization.
* `collaborator_auth_policy` - Authentication policy (`password`
or `two_factor_mandatory`). Defaults to `password`.
* `cost_estimation_enabled` - Whether or not the cost estimation feature is enabled for all workspaces in the organization. Defaults to true. In a Terraform Cloud organization which does not have Teams & Governance features, this value is always false and cannot be changed. In Terraform Enterprise, Cost Estimation must also be enabled in Site Administration.
* `owners_team_saml_role_id` - The name of the "owners" team.
29 changes: 29 additions & 0 deletions website/docs/d/organizations.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
layout: "tfe"
page_title: "Terraform Enterprise: tfe_organizations"
sidebar_current: "docs-datasource-tfe-organizations"
description: |-
Get information on Organizations.
---

# Data Source: tfe_organizations

Use this data source to get a list of Organizations and a map of their IDs.

## Example Usage

```hcl
data "tfe_organizations" "foo" {
}
```

## Argument Reference

No arguments are required. This retrieves the names and IDs of all the organizations readable by the provided token.

## Attributes Reference

The following attributes are exported:

* `names` - A list of names of every organization.
* `ids` - A map of organization names and their IDs.