diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70da4a2af..e7322e612 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,13 +9,17 @@ nav_order: 1
+## [MAJOR.MINOR.PATCH] - YYYY-MM-DD
+
+- Add `aiven_organization_user_list` resource
+- Run client-side validation for `aiven_kafka_schema` AVRO type schema
+
## [4.27.0] - 2024-10-09
- Remove `aiven_thanos` from beta resources
- Removes `receiver_ingesting_remote_write_uri` and `store_uri` Thanos connection info fields
- Adds `stringtype` to `flink_external_postgresql_user_config` service integration
- Fix `terraform import` for services with additional disk space or read replica service integration
-- Run client-side validation for `aiven_kafka_schema` AVRO type schema
## [4.26.0] - 2024-09-25
diff --git a/docs/data-sources/organization_user_list.md b/docs/data-sources/organization_user_list.md
new file mode 100644
index 000000000..d573bb4dc
--- /dev/null
+++ b/docs/data-sources/organization_user_list.md
@@ -0,0 +1,53 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "aiven_organization_user_list Data Source - terraform-provider-aiven"
+subcategory: ""
+description: |-
+ List of users of the organization
+---
+
+# aiven_organization_user_list (Data Source)
+
+List of users of the organization
+
+
+
+
+## Schema
+
+### Optional
+
+- `id` (String) Organization id. Example: `org12345678`.
+- `name` (String) Organization name. Example: `aiven`.
+
+### Read-Only
+
+- `users` (List of Object) List of users of the organization (see [below for nested schema](#nestedatt--users))
+
+
+### Nested Schema for `users`
+
+Read-Only:
+
+- `is_super_admin` (Boolean)
+- `join_time` (String)
+- `last_activity_time` (String)
+- `user_id` (String)
+- `user_info` (List of Object) (see [below for nested schema](#nestedobjatt--users--user_info))
+
+
+### Nested Schema for `users.user_info`
+
+Read-Only:
+
+- `city` (String)
+- `country` (String)
+- `create_time` (String)
+- `department` (String)
+- `is_application_user` (Boolean)
+- `job_title` (String)
+- `managed_by_scim` (Boolean)
+- `managing_organization_id` (String)
+- `real_name` (String)
+- `state` (String)
+- `user_email` (String)
diff --git a/internal/acctest/acctest.go b/internal/acctest/acctest.go
index e66a10b78..35d233d71 100644
--- a/internal/acctest/acctest.go
+++ b/internal/acctest/acctest.go
@@ -55,12 +55,12 @@ func GetTestAivenClient() *aiven.Client {
return testAivenClient
}
-func GetTestGenAivenClient() avngen.Client {
+func GetTestGenAivenClient() (avngen.Client, error) {
client, err := common.NewAivenGenClient()
if err != nil {
- log.Panicf("test generated client error: %s", err)
+ return nil, fmt.Errorf("test generated client error: %w", err)
}
- return client
+ return client, nil
}
// commonTestDependencies is a struct that contains common dependencies that are used by acceptance tests.
diff --git a/internal/schemautil/schemautil.go b/internal/schemautil/schemautil.go
index 31aa869b3..e82e50725 100644
--- a/internal/schemautil/schemautil.go
+++ b/internal/schemautil/schemautil.go
@@ -386,24 +386,24 @@ func ResourceDataGet(d *schema.ResourceData, dto any, fns ...KVModifier) error {
k, value = f(k, value)
}
- m[k] = serializeValue(value)
+ m[k] = serializeGet(value)
}
return Remarshal(&m, dto)
}
-func serializeValue(value any) any {
+func serializeGet(value any) any {
switch t := value.(type) {
case *schema.Set:
- return serializeValue(t.List())
+ return serializeGet(t.List())
case []any:
for i, v := range t {
- t[i] = serializeValue(v)
+ t[i] = serializeGet(v)
}
return t
case map[string]any:
for k, v := range t {
- t[k] = serializeValue(v)
+ t[k] = serializeGet(v)
}
}
return value
@@ -442,6 +442,7 @@ func ResourceDataSet(s map[string]*schema.Schema, d *schema.ResourceData, dto an
}
}
+ m = serializeSet(s, m)
for k := range s {
if v, ok := m[k]; ok {
if err = d.Set(k, v); err != nil {
@@ -452,6 +453,36 @@ func ResourceDataSet(s map[string]*schema.Schema, d *schema.ResourceData, dto an
return nil
}
+func serializeSet(s map[string]*schema.Schema, m map[string]any) map[string]any {
+ for k, prop := range s {
+ value, ok := m[k]
+ if !ok {
+ continue
+ }
+
+ res, ok := prop.Elem.(*schema.Resource)
+ if !ok {
+ continue
+ }
+
+ // When we have an object, we need to convert it to a list.
+ // So there is no difference between a single object and a list of objects.
+ var items []any
+ switch element := value.(type) {
+ case map[string]any:
+ items = append(items, serializeSet(res.Schema, element))
+ case []any:
+ for _, v := range element {
+ items = append(items, serializeSet(res.Schema, v.(map[string]any)))
+ }
+ }
+
+ m[k] = items
+ }
+
+ return m
+}
+
// RenameAliases renames field names on object top level
func RenameAliases(aliases map[string]string) KVModifier {
return func(k string, v any) (string, any) {
diff --git a/internal/sdkprovider/provider/provider.go b/internal/sdkprovider/provider/provider.go
index 7b11a21a6..9794046d9 100644
--- a/internal/sdkprovider/provider/provider.go
+++ b/internal/sdkprovider/provider/provider.go
@@ -91,6 +91,7 @@ func Provider(version string) (*schema.Provider, error) {
// organization
"aiven_organizational_unit": organization.DatasourceOrganizationalUnit(),
"aiven_organization_user": organization.DatasourceOrganizationUser(),
+ "aiven_organization_user_list": organization.DatasourceOrganizationUserList(),
"aiven_organization_user_group": organization.DatasourceOrganizationUserGroup(),
"aiven_organization_application_user": organization.DatasourceOrganizationApplicationUser(),
diff --git a/internal/sdkprovider/service/organization/organization_permission_test.go b/internal/sdkprovider/service/organization/organization_permission_test.go
index dab234a04..706f8adda 100644
--- a/internal/sdkprovider/service/organization/organization_permission_test.go
+++ b/internal/sdkprovider/service/organization/organization_permission_test.go
@@ -9,6 +9,7 @@ import (
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
+ "github.com/stretchr/testify/require"
acc "github.com/aiven/terraform-provider-aiven/internal/acctest"
)
@@ -41,7 +42,8 @@ func TestAccAivenOrganizationPermission_basic(t *testing.T) {
Config: testAccOrganizationPermissionResource(rName, ""),
Check: func(s *terraform.State) error {
ctx := context.Background()
- client := acc.GetTestGenAivenClient()
+ client, err := acc.GetTestGenAivenClient()
+ require.NoError(t, err)
for _, r := range s.RootModule().Resources {
if r.Type != "aiven_project" {
diff --git a/internal/sdkprovider/service/organization/organization_user_list_data_source.go b/internal/sdkprovider/service/organization/organization_user_list_data_source.go
new file mode 100644
index 000000000..fce852517
--- /dev/null
+++ b/internal/sdkprovider/service/organization/organization_user_list_data_source.go
@@ -0,0 +1,178 @@
+package organization
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ avngen "github.com/aiven/go-client-codegen"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+
+ "github.com/aiven/terraform-provider-aiven/internal/common"
+ "github.com/aiven/terraform-provider-aiven/internal/schemautil"
+)
+
+func datasourceOrganizationUserListSchema() map[string]*schema.Schema {
+ return map[string]*schema.Schema{
+ "id": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "Organization id. Example: `org12345678`.",
+ ConflictsWith: []string{"name"},
+ },
+ "name": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "Organization name. Example: `aiven`.",
+ ConflictsWith: []string{"id"},
+ },
+ "users": {
+ Type: schema.TypeList,
+ Computed: true,
+ Description: "List of users of the organization",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "is_super_admin": {
+ Type: schema.TypeBool,
+ Computed: true,
+ Description: "Super admin state of the organization user",
+ },
+ "join_time": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "Join time",
+ },
+ "last_activity_time": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "Last activity time",
+ },
+ "user_id": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "User ID",
+ },
+ "user_info": {
+ Type: schema.TypeList,
+ Computed: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "city": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "City",
+ },
+ "country": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "Country",
+ },
+ "create_time": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "Creation time",
+ },
+ "department": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "Department",
+ },
+ "is_application_user": {
+ Type: schema.TypeBool,
+ Computed: true,
+ Description: "Is Application User",
+ },
+ "job_title": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "Job Title",
+ },
+ "managed_by_scim": {
+ Type: schema.TypeBool,
+ Computed: true,
+ Description: "Managed By Scim",
+ },
+ "managing_organization_id": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "Managing Organization ID",
+ },
+ "real_name": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "Real Name",
+ },
+ "state": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "State",
+ },
+ "user_email": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "User Email",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func DatasourceOrganizationUserList() *schema.Resource {
+ return &schema.Resource{
+ ReadContext: common.WithGenClient(datasourceOrganizationUserListRead),
+ Description: "List of users of the organization",
+ Schema: datasourceOrganizationUserListSchema(),
+ }
+}
+
+func datasourceOrganizationUserListRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) error {
+ organizationID := d.Get("id").(string)
+ if organizationID == "" {
+ name := d.Get("name").(string)
+ if name == "" {
+ return fmt.Errorf("either id or name must be specified")
+ }
+
+ id, err := GetOrganizationByName(ctx, client, name)
+ if err != nil {
+ return err
+ }
+ organizationID = id
+ }
+
+ list, err := client.OrganizationUserList(ctx, organizationID)
+ if err != nil {
+ return fmt.Errorf("cannot get organization %q user list: %w", organizationID, err)
+ }
+
+ d.SetId(organizationID)
+ users := map[string]any{"users": list}
+ return schemautil.ResourceDataSet(datasourceOrganizationUserListSchema(), d, users)
+}
+
+func GetOrganizationByName(ctx context.Context, client avngen.Client, name string) (string, error) {
+ ids := make([]string, 0)
+ list, err := client.UserOrganizationsList(ctx)
+ if err != nil {
+ return "", err
+ }
+
+ for _, o := range list {
+ // Organization name is not unique
+ if o.OrganizationName == name {
+ ids = append(ids, o.OrganizationId)
+ }
+ }
+
+ switch len(ids) {
+ case 0:
+ return "", fmt.Errorf("organization %q not found", name)
+ case 1:
+ return ids[0], nil
+ }
+ return "", fmt.Errorf("multiple organizations %q found, ids: %s", name, strings.Join(ids, ", "))
+}
diff --git a/internal/sdkprovider/service/organization/organization_user_list_data_source_test.go b/internal/sdkprovider/service/organization/organization_user_list_data_source_test.go
new file mode 100644
index 000000000..81f4bd085
--- /dev/null
+++ b/internal/sdkprovider/service/organization/organization_user_list_data_source_test.go
@@ -0,0 +1,79 @@
+package organization_test
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "regexp"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/stretchr/testify/require"
+
+ acc "github.com/aiven/terraform-provider-aiven/internal/acctest"
+ "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/organization"
+)
+
+func testAccAivenOrganizationUserListByName(name string) string {
+ return fmt.Sprintf(`
+data "aiven_organization_user_list" "org" {
+ name = "%s"
+}
+`, name)
+}
+
+func TestAccAivenOrganizationUserListByName(t *testing.T) {
+ resourceName := "data.aiven_organization_user_list.org"
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { acc.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccAivenOrganizationUserListByName(os.Getenv("AIVEN_ORGANIZATION_NAME")),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttrSet(resourceName, "users.#"),
+ resource.TestMatchResourceAttr(resourceName, "users.0.user_info.0.user_email", regexp.MustCompile(`.*@.*`)),
+ ),
+ },
+ },
+ })
+}
+
+func testAccAivenOrganizationUserListByID(id string) string {
+ return fmt.Sprintf(`
+data "aiven_organization_user_list" "org" {
+ id = "%s"
+}
+`, id)
+}
+
+func TestAccAivenOrganizationUserListByID(t *testing.T) {
+ // This test creates Aiven client before running PreCheck part
+ // Runs checks manually
+ _ = acc.CommonTestDependencies(t)
+
+ resourceName := "data.aiven_organization_user_list.org"
+ client, err := acc.GetTestGenAivenClient()
+ require.NoError(t, err)
+
+ id, err := organization.GetOrganizationByName(
+ context.Background(),
+ client,
+ os.Getenv("AIVEN_ORGANIZATION_NAME"),
+ )
+ require.NoError(t, err)
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { acc.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccAivenOrganizationUserListByID(id),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttrSet(resourceName, "users.#"),
+ resource.TestMatchResourceAttr(resourceName, "users.0.user_info.0.user_email", regexp.MustCompile(`.*@.*`)),
+ ),
+ },
+ },
+ })
+}