Skip to content

Commit

Permalink
feat: add coderd_organization data source
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanndickson committed Jul 17, 2024
1 parent 393c5a9 commit 7dc20eb
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 3 deletions.
28 changes: 28 additions & 0 deletions docs/data-sources/organization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "coderd_organization Data Source - coderd"
subcategory: ""
description: |-
An existing organization on the coder deployment.
---

# coderd_organization (Data Source)

An existing organization on the coder deployment.



<!-- schema generated by tfplugindocs -->
## Schema

### Optional

- `id` (String) The ID of the organization to retrieve. This field will be populated if the organization is found by name, or if the default organization is requested.
- `is_default` (Boolean) Whether the organization is the default organization of the deployment. This field will be populated if the organization is found by ID or name.
- `name` (String) The name of the organization to retrieve. This field will be populated if the organization is found by ID, or if the default organization is requested.

### Read-Only

- `created_at` (Number) Unix timestamp when the organization was created.
- `members` (Set of String) Members of the organization, by ID
- `updated_at` (Number) Unix timestamp when the organization was last updated.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ toolchain go1.22.5

require (
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6
github.com/coder/coder/v2 v2.12.3
github.com/coder/coder/v2 v2.13.0
github.com/docker/docker v27.0.3+incompatible
github.com/docker/go-connections v0.4.0
github.com/google/uuid v1.6.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/coder/coder/v2 v2.12.3 h1:tA+0lWIO7xXJ4guu+tqcram/6kKKX1pWd1WlipdhIpc=
github.com/coder/coder/v2 v2.12.3/go.mod h1:io26dngPVP3a7zD1lL/bzEOGDSincJGomBKlqmRRVNA=
github.com/coder/coder/v2 v2.13.0 h1:MlkRGqQcCAdwIkLc9iV8sQfT4jB3EThHopG0jF3BuFE=
github.com/coder/coder/v2 v2.13.0/go.mod h1:Gxc79InMB6b+sncuDUORtFLWi7aKshvis3QrMUhpq5Q=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
github.com/coder/serpent v0.7.0 h1:zGpD2GlF3lKIVkMjNGKbkip88qzd5r/TRcc30X/SrT0=
Expand Down
183 changes: 183 additions & 0 deletions internal/provider/organization_data_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package provider

import (
"context"
"fmt"

"github.com/coder/coder/v2/codersdk"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
"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/path"
"github.com/hashicorp/terraform-plugin-framework/types"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ datasource.DataSource = &OrganizationDataSource{}
var _ datasource.DataSourceWithConfigValidators = &OrganizationDataSource{}

func NewOrganizationDataSource() datasource.DataSource {
return &OrganizationDataSource{}
}

// OrganizationDataSource defines the data source implementation.
type OrganizationDataSource struct {
data *CoderdProviderData
}

// OrganizationDataSourceModel describes the data source data model.
type OrganizationDataSourceModel struct {
// Exactly one of ID, IsDefault, or Name must be set.
ID types.String `tfsdk:"id"`
IsDefault types.Bool `tfsdk:"is_default"`
Name types.String `tfsdk:"name"`

CreatedAt types.Int64 `tfsdk:"created_at"`
UpdatedAt types.Int64 `tfsdk:"updated_at"`
Members types.Set `tfsdk:"members"`
}

func (d *OrganizationDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_organization"
}

func (d *OrganizationDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "An existing organization on the coder deployment.",

Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The ID of the organization to retrieve. This field will be populated if the organization is found by name, or if the default organization is requested.",
Optional: true,
Computed: true,
},
"is_default": schema.BoolAttribute{
MarkdownDescription: "Whether the organization is the default organization of the deployment. This field will be populated if the organization is found by ID or name.",
Optional: true,
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The name of the organization to retrieve. This field will be populated if the organization is found by ID, or if the default organization is requested.",
Optional: true,
Computed: true,
},
"created_at": schema.Int64Attribute{
MarkdownDescription: "Unix timestamp when the organization was created.",
Computed: true,
},
"updated_at": schema.Int64Attribute{
MarkdownDescription: "Unix timestamp when the organization was last updated.",
Computed: true,
},

"members": schema.SetAttribute{
MarkdownDescription: "Members of the organization, by ID",
Computed: true,
ElementType: types.StringType,
},
},
}
}

func (d *OrganizationDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

data, ok := req.ProviderData.(*CoderdProviderData)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

d.data = data
}

func (d *OrganizationDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data OrganizationDataSourceModel

// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

client := d.data.Client

var org codersdk.Organization
if !data.ID.IsNull() { // By ID
orgID, err := uuid.Parse(data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied ID as UUID, got error: %s", err))
return
}
org, err = client.Organization(ctx, orgID)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by ID, got error: %s", err))
return
}
if org.ID.String() != data.ID.ValueString() {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Organization ID %s does not match requested ID %s", org.ID, data.ID))
return
}
} else if data.IsDefault.ValueBool() { // Get Default
var err error
org, err = client.OrganizationByName(ctx, "default")
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get default organization, got error: %s", err))
return
}
if !org.IsDefault {
resp.Diagnostics.AddError("Client Error", "Found organization was not the default organization")
return
}
} else { // By Name
var err error
org, err = client.OrganizationByName(ctx, data.Name.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by name, got error: %s", err))
return
}
if org.Name != data.Name.ValueString() {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Organization name %s does not match requested name %s", org.Name, data.Name))
return
}
}
data.ID = types.StringValue(org.ID.String())
data.Name = types.StringValue(org.Name)
data.IsDefault = types.BoolValue(org.IsDefault)
data.CreatedAt = types.Int64Value(org.CreatedAt.Unix())
data.UpdatedAt = types.Int64Value(org.UpdatedAt.Unix())
members, err := client.OrganizationMembers(ctx, org.ID)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization members, got error: %s", err))
return
}
memberIDs := make([]attr.Value, 0, len(members))
for _, member := range members {
memberIDs = append(memberIDs, types.StringValue(member.UserID.String()))
}
data.Members = types.SetValueMust(types.StringType, memberIDs)

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (d *OrganizationDataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator {
return []datasource.ConfigValidator{
datasourcevalidator.ExactlyOneOf(
path.MatchRoot("id"),
path.MatchRoot("is_default"),
path.MatchRoot("name"),
),
}
}
146 changes: 146 additions & 0 deletions internal/provider/organization_data_source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package provider

import (
"context"
"os"
"regexp"
"strings"
"testing"
"text/template"

"github.com/coder/coder/v2/codersdk"
"github.com/coder/terraform-provider-coderd/integration"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/stretchr/testify/require"
)

func TestAccOrganizationDataSource(t *testing.T) {
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
ctx := context.Background()
client := integration.StartCoder(ctx, t, "group_acc")
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)

defaultCheckFn := resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.coderd_organization.test", "id", firstUser.OrganizationIDs[0].String()),
resource.TestCheckResourceAttr("data.coderd_organization.test", "is_default", "true"),
resource.TestCheckResourceAttr("data.coderd_organization.test", "name", "first-organization"),
resource.TestCheckResourceAttr("data.coderd_organization.test", "members.#", "1"),
resource.TestCheckTypeSetElemAttr("data.coderd_organization.test", "members.*", firstUser.ID.String()),
resource.TestCheckResourceAttrSet("data.coderd_organization.test", "created_at"),
resource.TestCheckResourceAttrSet("data.coderd_organization.test", "updated_at"),
)

t.Run("DefaultOrgByIDOk", func(t *testing.T) {
cfg := testAccOrganizationDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
ID: PtrTo(firstUser.OrganizationIDs[0].String()),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: cfg.String(t),
Check: defaultCheckFn,
},
},
})
})

t.Run("DefaultOrgByNameOk", func(t *testing.T) {
cfg := testAccOrganizationDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
Name: PtrTo("first-organization"),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: cfg.String(t),
Check: defaultCheckFn,
},
},
})
})

t.Run("DefaultOrgByIsDefaultOk", func(t *testing.T) {
cfg := testAccOrganizationDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
IsDefault: PtrTo(true),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: cfg.String(t),
Check: defaultCheckFn,
},
},
})
})

t.Run("InvalidAttributesError", func(t *testing.T) {
cfg := testAccOrganizationDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
IsDefault: PtrTo(true),
Name: PtrTo("first-organization"),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: cfg.String(t),
ExpectError: regexp.MustCompile(`Exactly one of these attributes must be configured: \[id,is\_default,name\]`),
},
},
})
})

// TODO: Non-default org tests
}

type testAccOrganizationDataSourceConfig struct {
URL string
Token string

ID *string
Name *string
IsDefault *bool
}

func (c testAccOrganizationDataSourceConfig) String(t *testing.T) string {
tpl := `
provider coderd {
url = "{{.URL}}"
token = "{{.Token}}"
}
data "coderd_organization" "test" {
id = {{orNull .ID}}
name = {{orNull .Name}}
is_default = {{orNull .IsDefault}}
}
`

funcMap := template.FuncMap{
"orNull": PrintOrNull(t),
}

buf := strings.Builder{}
tmpl, err := template.New("groupDataSource").Funcs(funcMap).Parse(tpl)
require.NoError(t, err)

err = tmpl.Execute(&buf, c)
require.NoError(t, err)
return buf.String()
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func (p *CoderdProvider) DataSources(ctx context.Context) []func() datasource.Da
return []func() datasource.DataSource{
NewGroupDataSource,
NewUserDataSource,
NewOrganizationDataSource,
}
}

Expand Down

0 comments on commit 7dc20eb

Please sign in to comment.