From 7d20341949ff71ffc4f2dc5fa26c3f5cad3a9f60 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Fri, 21 Jun 2024 16:10:36 +0200 Subject: [PATCH] feat: project group bindings --- client/client.go | 23 +- client/project.go | 2 + client/project_binding.go | 134 +++++++++++ client/project_group_binding.go | 26 +++ client/project_user_binding.go | 102 +-------- client/tenant.go | 2 + docs/data-sources/project_group_binding.md | 68 ++++++ docs/data-sources/project_user_binding.md | 4 +- docs/resources/project_group_binding.md | 90 ++++++++ .../data-source.tf | 5 + .../data-source.tf | 2 +- .../meshstack_project_group_binding/import.sh | 2 + .../resource.tf | 18 ++ .../project_group_binding_data_source.go | 133 +++++++++++ .../project_group_binding_resource.go | 211 ++++++++++++++++++ .../project_user_binding_data_source.go | 23 +- .../provider/project_user_binding_resource.go | 2 +- internal/provider/provider.go | 4 +- 18 files changed, 723 insertions(+), 128 deletions(-) create mode 100644 client/project_binding.go create mode 100644 client/project_group_binding.go create mode 100644 docs/data-sources/project_group_binding.md create mode 100644 docs/resources/project_group_binding.md create mode 100644 examples/data-sources/meshstack_project_group_binding/data-source.tf create mode 100644 examples/resources/meshstack_project_group_binding/import.sh create mode 100644 examples/resources/meshstack_project_group_binding/resource.tf create mode 100644 internal/provider/project_group_binding_data_source.go create mode 100644 internal/provider/project_group_binding_resource.go diff --git a/client/client.go b/client/client.go index 9158f69..281d040 100644 --- a/client/client.go +++ b/client/client.go @@ -20,11 +20,6 @@ const ( ERROR_GENERIC_API_ERROR = "api error" ERROR_AUTHENTICATION_FAILURE = "Not authorized. Check api key and secret." ERROR_ENDPOINT_LOOKUP = "Could not fetch endpoints for meshStack." - - CONTENT_TYPE_PROJECT = "application/vnd.meshcloud.api.meshproject.v2.hal+json" - CONTENT_TYPE_TENANT = "application/vnd.meshcloud.api.meshtenant.v3.hal+json" - CONTENT_TYPE_PROJECT_USER_BINDINGS = "application/vnd.meshcloud.api.meshprojectuserbinding.v1.hal+json" - CONTENT_TYPE_PROJECT_USER_BINDING = "application/vnd.meshcloud.api.meshprojectuserbinding.v3.hal+json" ) type MeshStackProviderClient struct { @@ -38,10 +33,11 @@ type MeshStackProviderClient struct { } type endpoints struct { - BuildingBlocks *url.URL `json:"meshbuildingblocks"` - Projects *url.URL `json:"meshprojects"` - ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` - Tenants *url.URL `json:"meshtenants"` + BuildingBlocks *url.URL `json:"meshbuildingblocks"` + Projects *url.URL `json:"meshprojects"` + ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` + ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` + Tenants *url.URL `json:"meshtenants"` } type loginResponse struct { @@ -62,10 +58,11 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro // TODO: lookup endpoints client.endpoints = endpoints{ - BuildingBlocks: rootUrl.JoinPath(apiMeshObjectsRoot, "meshbuildingblocks"), - Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), - ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), - Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), + BuildingBlocks: rootUrl.JoinPath(apiMeshObjectsRoot, "meshbuildingblocks"), + Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), + ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), + ProjectGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "groupbindings"), + Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), } return client, nil diff --git a/client/project.go b/client/project.go index f9d97f8..eca75ca 100644 --- a/client/project.go +++ b/client/project.go @@ -9,6 +9,8 @@ import ( "net/url" ) +const CONTENT_TYPE_PROJECT = "application/vnd.meshcloud.api.meshproject.v2.hal+json" + type MeshProject struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` diff --git a/client/project_binding.go b/client/project_binding.go new file mode 100644 index 0000000..f3b4d41 --- /dev/null +++ b/client/project_binding.go @@ -0,0 +1,134 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +type MeshProjectBinding struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshProjectBindingMetadata `json:"metadata" tfsdk:"metadata"` + RoleRef MeshProjectRoleRef `json:"roleRef" tfsdk:"role_ref"` + TargetRef MeshProjectTargetRef `json:"targetRef" tfsdk:"target_ref"` + Subject MeshSubject `json:"subject" tfsdk:"subject"` +} + +type MeshProjectBindingMetadata struct { + Name string `json:"name" tfsdk:"name"` +} + +type MeshProjectRoleRef struct { + Name string `json:"name" tfsdk:"name"` +} + +type MeshProjectTargetRef struct { + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +} + +type MeshSubject struct { + Name string `json:"name" tfsdk:"name"` +} + +func (c *MeshStackProviderClient) readProjectBinding(name string, contentType string) (*MeshProjectBinding, error) { + var targetUrl *url.URL + switch contentType { + case CONTENT_TYPE_PROJECT_USER_BINDING: + targetUrl = c.urlForPojectUserBinding(name) + + case CONTENT_TYPE_PROJECT_GROUP_BINDING: + targetUrl = c.urlForPojectGroupBinding(name) + + default: + return nil, fmt.Errorf("Unexpected content type: %s", contentType) + } + + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", contentType) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if res.StatusCode == 404 { + return nil, nil + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var binding MeshProjectBinding + err = json.Unmarshal(data, &binding) + if err != nil { + return nil, err + } + + return &binding, nil +} + +func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBinding, contentType string) (*MeshProjectBinding, error) { + var targetUrl *url.URL + switch contentType { + case CONTENT_TYPE_PROJECT_USER_BINDING: + targetUrl = c.endpoints.ProjectUserBindings + + case CONTENT_TYPE_PROJECT_GROUP_BINDING: + targetUrl = c.endpoints.ProjectGroupBindings + + default: + return nil, fmt.Errorf("Unexpected content type: %s", contentType) + } + + payload, err := json.Marshal(binding) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", targetUrl.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT_GROUP_BINDING) + req.Header.Set("Accept", CONTENT_TYPE_PROJECT_GROUP_BINDING) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdBinding MeshProjectBinding + err = json.Unmarshal(data, &createdBinding) + if err != nil { + return nil, err + } + + return &createdBinding, nil +} diff --git a/client/project_group_binding.go b/client/project_group_binding.go new file mode 100644 index 0000000..8b66818 --- /dev/null +++ b/client/project_group_binding.go @@ -0,0 +1,26 @@ +package client + +import ( + "net/url" +) + +const CONTENT_TYPE_PROJECT_GROUP_BINDING = "application/vnd.meshcloud.api.meshprojectgroupbinding.v3.hal+json" + +type MeshProjectGroupBinding = MeshProjectBinding + +func (c *MeshStackProviderClient) urlForPojectGroupBinding(name string) *url.URL { + return c.endpoints.ProjectGroupBindings.JoinPath(name) +} + +func (c *MeshStackProviderClient) ReadProjectGroupBinding(name string) (*MeshProjectGroupBinding, error) { + return c.readProjectBinding(name, CONTENT_TYPE_PROJECT_GROUP_BINDING) +} + +func (c *MeshStackProviderClient) CreateProjectGroupBinding(binding *MeshProjectGroupBinding) (*MeshProjectGroupBinding, error) { + return c.createProjectBinding(binding, CONTENT_TYPE_PROJECT_GROUP_BINDING) +} + +func (c *MeshStackProviderClient) DeleteProjecGroupBinding(name string) error { + targetUrl := c.urlForPojectGroupBinding(name) + return c.deleteMeshObject(*targetUrl, 204) +} diff --git a/client/project_user_binding.go b/client/project_user_binding.go index 0c6f601..3de8e22 100644 --- a/client/project_user_binding.go +++ b/client/project_user_binding.go @@ -1,117 +1,23 @@ package client import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" "net/url" ) -type MeshProjectUserBinding struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshProjectUserBindingMetadata `json:"metadata" tfsdk:"metadata"` - RoleRef MeshProjectRoleRef `json:"roleRef" tfsdk:"role_ref"` - TargetRef MeshProjectTargetRef `json:"targetRef" tfsdk:"target_ref"` - Subject MeshSubject `json:"subject" tfsdk:"subject"` -} - -type MeshProjectUserBindingMetadata struct { - Name string `json:"name" tfsdk:"name"` -} - -type MeshProjectRoleRef struct { - Name string `json:"name" tfsdk:"name"` -} - -type MeshProjectTargetRef struct { - Name string `json:"name" tfsdk:"name"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` -} +const CONTENT_TYPE_PROJECT_USER_BINDING = "application/vnd.meshcloud.api.meshprojectuserbinding.v3.hal+json" -type MeshSubject struct { - Name string `json:"name" tfsdk:"name"` -} +type MeshProjectUserBinding = MeshProjectBinding func (c *MeshStackProviderClient) urlForPojectUserBinding(name string) *url.URL { return c.endpoints.ProjectUserBindings.JoinPath(name) } func (c *MeshStackProviderClient) ReadProjectUserBinding(name string) (*MeshProjectUserBinding, error) { - targetUrl := c.urlForPojectUserBinding(name) - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_PROJECT_USER_BINDING) - - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - defer res.Body.Close() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if res.StatusCode == 404 { - return nil, nil - } - - if res.StatusCode != 200 { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - - var binding MeshProjectUserBinding - err = json.Unmarshal(data, &binding) - if err != nil { - return nil, err - } - - return &binding, nil + return c.readProjectBinding(name, CONTENT_TYPE_PROJECT_USER_BINDING) } func (c *MeshStackProviderClient) CreateProjectUserBinding(binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { - payload, err := json.Marshal(binding) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.ProjectUserBindings.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT_USER_BINDING) - req.Header.Set("Accept", CONTENT_TYPE_PROJECT_USER_BINDING) - - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - defer res.Body.Close() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if res.StatusCode != 200 { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - - var createdBinding MeshProjectUserBinding - err = json.Unmarshal(data, &createdBinding) - if err != nil { - return nil, err - } - - return &createdBinding, nil + return c.createProjectBinding(binding, CONTENT_TYPE_PROJECT_USER_BINDING) } func (c *MeshStackProviderClient) DeleteProjecUserBinding(name string) error { diff --git a/client/tenant.go b/client/tenant.go index b8026aa..b83c5e6 100644 --- a/client/tenant.go +++ b/client/tenant.go @@ -9,6 +9,8 @@ import ( "net/url" ) +const CONTENT_TYPE_TENANT = "application/vnd.meshcloud.api.meshtenant.v3.hal+json" + type MeshTenant struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` diff --git a/docs/data-sources/project_group_binding.md b/docs/data-sources/project_group_binding.md new file mode 100644 index 0000000..ceee7c9 --- /dev/null +++ b/docs/data-sources/project_group_binding.md @@ -0,0 +1,68 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "meshstack_project_group_binding Data Source - terraform-provider-meshstack" +subcategory: "" +description: |- + Single project group binding by name. +--- + +# meshstack_project_group_binding (Data Source) + +Single project group binding by name. + +## Example Usage + +```terraform +data "meshstack_project_group_binding" "example" { + metadata = { + name = "my-project-group-binding" + } +} +``` + + +## Schema + +### Required + +- `metadata` (Attributes) Project role assigned by this binding. (see [below for nested schema](#nestedatt--metadata)) + +### Read-Only + +- `api_version` (String) Project group binding datatype version +- `kind` (String) meshObject type, always `meshProjectGroupBinding`. +- `role_ref` (Attributes) Project role assigned by this binding. (see [below for nested schema](#nestedatt--role_ref)) +- `subject` (Attributes) Group assigned by this binding. (see [below for nested schema](#nestedatt--subject)) +- `target_ref` (Attributes) Project, identified by workspace and project identifier. (see [below for nested schema](#nestedatt--target_ref)) + + +### Nested Schema for `metadata` + +Required: + +- `name` (String) + + + +### Nested Schema for `role_ref` + +Read-Only: + +- `name` (String) + + + +### Nested Schema for `subject` + +Read-Only: + +- `name` (String) Groupname. + + + +### Nested Schema for `target_ref` + +Read-Only: + +- `name` (String) +- `owned_by_workspace` (String) diff --git a/docs/data-sources/project_user_binding.md b/docs/data-sources/project_user_binding.md index bdfc64e..03c6f2f 100644 --- a/docs/data-sources/project_user_binding.md +++ b/docs/data-sources/project_user_binding.md @@ -15,7 +15,7 @@ Single project user binding by name. ```terraform data "meshstack_project_user_binding" "example" { metadata = { - name = "214cb14d-2e21-11ef-8e80-0242ac130003" + name = "my-project-user-binding" } } ``` @@ -32,7 +32,7 @@ data "meshstack_project_user_binding" "example" { - `api_version` (String) Project user binding datatype version - `kind` (String) meshObject type, always `meshProjectUserBinding`. - `role_ref` (Attributes) Project role assigned by this binding. (see [below for nested schema](#nestedatt--role_ref)) -- `subject` (Attributes) Users assigned by this binding. (see [below for nested schema](#nestedatt--subject)) +- `subject` (Attributes) User assigned by this binding. (see [below for nested schema](#nestedatt--subject)) - `target_ref` (Attributes) Project, identified by workspace and project identifier. (see [below for nested schema](#nestedatt--target_ref)) diff --git a/docs/resources/project_group_binding.md b/docs/resources/project_group_binding.md new file mode 100644 index 0000000..0dde0ba --- /dev/null +++ b/docs/resources/project_group_binding.md @@ -0,0 +1,90 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "meshstack_project_group_binding Resource - terraform-provider-meshstack" +subcategory: "" +description: |- + Project group binding assigns a group with a specific role to a project. +--- + +# meshstack_project_group_binding (Resource) + +Project group binding assigns a group with a specific role to a project. + +## Example Usage + +```terraform +resource "meshstack_project_group_binding" "example" { + metadata = { + name = "this-is-an-example" + } + + role_ref = { + name = "Project Reader" + } + + target_ref = { + owned_by_workspace = "my-customer" + name = "my-project" + } + + subject = { + name = "my-user-group" + } +} +``` + + +## Schema + +### Required + +- `metadata` (Attributes) Project group binding metadata. (see [below for nested schema](#nestedatt--metadata)) +- `role_ref` (Attributes) Selects the role to use for this project binding. (see [below for nested schema](#nestedatt--role_ref)) +- `subject` (Attributes) Selects the group for this binding. (see [below for nested schema](#nestedatt--subject)) +- `target_ref` (Attributes) Selects the project to which this binding applies. (see [below for nested schema](#nestedatt--target_ref)) + +### Read-Only + +- `api_version` (String) Project group binding datatype version +- `kind` (String) meshObject type, always `meshProjectGroupBinding`. + + +### Nested Schema for `metadata` + +Required: + +- `name` (String) The name identifies the binding and must be unique across the meshStack. + + + +### Nested Schema for `role_ref` + +Required: + +- `name` (String) + + + +### Nested Schema for `subject` + +Required: + +- `name` (String) Groupname. + + + +### Nested Schema for `target_ref` + +Required: + +- `name` (String) Project identifier. +- `owned_by_workspace` (String) Identifier of workspace containing the target project. + +## Import + +Import is supported using the following syntax: + +```shell +# import via project binding name +terraform import 'meshstack_project_user_binding.example' 'my-binding-name' +``` diff --git a/examples/data-sources/meshstack_project_group_binding/data-source.tf b/examples/data-sources/meshstack_project_group_binding/data-source.tf new file mode 100644 index 0000000..3c36fed --- /dev/null +++ b/examples/data-sources/meshstack_project_group_binding/data-source.tf @@ -0,0 +1,5 @@ +data "meshstack_project_group_binding" "example" { + metadata = { + name = "my-project-group-binding" + } +} diff --git a/examples/data-sources/meshstack_project_user_binding/data-source.tf b/examples/data-sources/meshstack_project_user_binding/data-source.tf index c002e76..b048066 100644 --- a/examples/data-sources/meshstack_project_user_binding/data-source.tf +++ b/examples/data-sources/meshstack_project_user_binding/data-source.tf @@ -1,5 +1,5 @@ data "meshstack_project_user_binding" "example" { metadata = { - name = "214cb14d-2e21-11ef-8e80-0242ac130003" + name = "my-project-user-binding" } } diff --git a/examples/resources/meshstack_project_group_binding/import.sh b/examples/resources/meshstack_project_group_binding/import.sh new file mode 100644 index 0000000..98debba --- /dev/null +++ b/examples/resources/meshstack_project_group_binding/import.sh @@ -0,0 +1,2 @@ +# import via project binding name +terraform import 'meshstack_project_user_binding.example' 'my-binding-name' diff --git a/examples/resources/meshstack_project_group_binding/resource.tf b/examples/resources/meshstack_project_group_binding/resource.tf new file mode 100644 index 0000000..16f1d5d --- /dev/null +++ b/examples/resources/meshstack_project_group_binding/resource.tf @@ -0,0 +1,18 @@ +resource "meshstack_project_group_binding" "example" { + metadata = { + name = "this-is-an-example" + } + + role_ref = { + name = "Project Reader" + } + + target_ref = { + owned_by_workspace = "my-customer" + name = "my-project" + } + + subject = { + name = "my-user-group" + } +} diff --git a/internal/provider/project_group_binding_data_source.go b/internal/provider/project_group_binding_data_source.go new file mode 100644 index 0000000..10b534a --- /dev/null +++ b/internal/provider/project_group_binding_data_source.go @@ -0,0 +1,133 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/meshcloud/terraform-provider-meshstack/client" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/schema/validator" +) + +var ( + _ datasource.DataSource = &projectGroupBindingDataSource{} + _ datasource.DataSourceWithConfigure = &projectGroupBindingDataSource{} +) + +func NewProjectGroupBindingDataSource() datasource.DataSource { + return &projectGroupBindingDataSource{} +} + +type projectGroupBindingDataSource struct { + client *client.MeshStackProviderClient +} + +func (d *projectGroupBindingDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_group_binding" + +} + +func (d *projectGroupBindingDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Single project group binding by name.", + + Attributes: map[string]schema.Attribute{ + "api_version": schema.StringAttribute{ + MarkdownDescription: "Project group binding datatype version", + Computed: true, + }, + + "kind": schema.StringAttribute{ + MarkdownDescription: "meshObject type, always `meshProjectGroupBinding`.", + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf([]string{"meshProjectGroupBinding"}...), + }, + }, + + "metadata": schema.SingleNestedAttribute{ + MarkdownDescription: "Project role assigned by this binding.", + Required: true, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 45), + }, + }, + }, + }, + + "role_ref": schema.SingleNestedAttribute{ + MarkdownDescription: "Project role assigned by this binding.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{Computed: true}, + }, + }, + + "target_ref": schema.SingleNestedAttribute{ + MarkdownDescription: "Project, identified by workspace and project identifier.", + Computed: true, + Attributes: map[string]schema.Attribute{ + + "name": schema.StringAttribute{Computed: true}, + "owned_by_workspace": schema.StringAttribute{Computed: true}, + }, + }, + "subject": schema.SingleNestedAttribute{ + MarkdownDescription: "Group assigned by this binding.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: "Groupname.", + Computed: true, + }, + }, + }, + }, + } +} + +func (d *projectGroupBindingDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.MeshStackProviderClient) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *MeshStackProviderClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = client +} + +func (d *projectGroupBindingDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var name string + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("metadata").AtName("name"), &name)...) + if resp.Diagnostics.HasError() { + return + } + + binding, err := d.client.ReadProjectGroupBinding(name) + if err != nil { + resp.Diagnostics.AddError("Unable to read project group binding", err.Error()) + } + + if binding == nil { + resp.Diagnostics.AddError("Project group binding not found", fmt.Sprintf("Can't find project group binding with name '%s'.", name)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, binding)...) +} diff --git a/internal/provider/project_group_binding_resource.go b/internal/provider/project_group_binding_resource.go new file mode 100644 index 0000000..aa683a3 --- /dev/null +++ b/internal/provider/project_group_binding_resource.go @@ -0,0 +1,211 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/meshcloud/terraform-provider-meshstack/client" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &projectGroupBindingResource{} + _ resource.ResourceWithConfigure = &projectGroupBindingResource{} + _ resource.ResourceWithImportState = &projectGroupBindingResource{} +) + +// NewProjectGroupBindingResource is a helper function to simplify the provider implementation. +func NewProjectGroupBindingResource() resource.Resource { + return &projectGroupBindingResource{} +} + +// projectGroupBindingResource is the resource implementation. +type projectGroupBindingResource struct { + client *client.MeshStackProviderClient +} + +// Metadata returns the resource type name. +func (r *projectGroupBindingResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_group_binding" +} + +// Configure adds the provider configured client to the resource. +func (r *projectGroupBindingResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.MeshStackProviderClient) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *MeshStackProviderClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +// Schema defines the schema for the resource. +func (r *projectGroupBindingResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Project group binding assigns a group with a specific role to a project.", + + Attributes: map[string]schema.Attribute{ + "api_version": schema.StringAttribute{ + MarkdownDescription: "Project group binding datatype version", + Computed: true, + Default: stringdefault.StaticString("v3"), + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + + "kind": schema.StringAttribute{ + MarkdownDescription: "meshObject type, always `meshProjectGroupBinding`.", + Computed: true, + Default: stringdefault.StaticString("meshProjectGroupBinding"), + Validators: []validator.String{ + stringvalidator.OneOf([]string{"meshProjectGroupBinding"}...), + }, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + + "metadata": schema.SingleNestedAttribute{ + Required: true, + MarkdownDescription: "Project group binding metadata.", + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: "The name identifies the binding and must be unique across the meshStack.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 45), + }, + }, + }, + }, + + "role_ref": schema.SingleNestedAttribute{ + MarkdownDescription: "Selects the role to use for this project binding.", + Required: true, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + }, + }, + + "target_ref": schema.SingleNestedAttribute{ + MarkdownDescription: "Selects the project to which this binding applies.", + Required: true, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: "Project identifier.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "owned_by_workspace": schema.StringAttribute{ + MarkdownDescription: "Identifier of workspace containing the target project.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + }, + }, + + "subject": schema.SingleNestedAttribute{ + MarkdownDescription: "Selects the group for this binding.", + Required: true, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: "Groupname.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *projectGroupBindingResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan client.MeshProjectGroupBinding + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + binding, err := r.client.CreateProjectGroupBinding(&plan) + if err != nil { + resp.Diagnostics.AddError( + "Error creating project group binding", + "Could not create project group binding, unexpected error: "+err.Error(), + ) + return + } + + diags = resp.State.Set(ctx, binding) + resp.Diagnostics.Append(diags...) +} + +// Read refreshes the Terraform state with the latest data. +func (r *projectGroupBindingResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var name string + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("metadata").AtName("name"), &name)...) + if resp.Diagnostics.HasError() { + return + } + + binding, err := r.client.ReadProjectGroupBinding(name) + if err != nil { + resp.Diagnostics.AddError("Unable to read project group binding", err.Error()) + } + + if binding == nil { + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, binding)...) +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *projectGroupBindingResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Project group bindings can't be updated", "Unsupported operation: project group bindings can't be updated.") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *projectGroupBindingResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var name string + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("metadata").AtName("name"), &name)...) + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteProjecGroupBinding(name) + if err != nil { + resp.Diagnostics.AddError( + "Error deleting project group binding", + "Could not delete project group binding, unexpected error: "+err.Error(), + ) + return + } +} + +func (r *projectGroupBindingResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("metadata").AtName("name"), req.ID)...) +} diff --git a/internal/provider/project_user_binding_data_source.go b/internal/provider/project_user_binding_data_source.go index 2afa059..321603b 100644 --- a/internal/provider/project_user_binding_data_source.go +++ b/internal/provider/project_user_binding_data_source.go @@ -14,24 +14,24 @@ import ( ) var ( - _ datasource.DataSource = &projectUserBindingsDataSource{} - _ datasource.DataSourceWithConfigure = &projectUserBindingsDataSource{} + _ datasource.DataSource = &projectUserBindingDataSource{} + _ datasource.DataSourceWithConfigure = &projectUserBindingDataSource{} ) -func NewProjectUserBindingsDataSource() datasource.DataSource { - return &projectUserBindingsDataSource{} +func NewProjectUserBindingDataSource() datasource.DataSource { + return &projectUserBindingDataSource{} } -type projectUserBindingsDataSource struct { +type projectUserBindingDataSource struct { client *client.MeshStackProviderClient } -func (d *projectUserBindingsDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *projectUserBindingDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_project_user_binding" } -func (d *projectUserBindingsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *projectUserBindingDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Single project user binding by name.", @@ -80,7 +80,7 @@ func (d *projectUserBindingsDataSource) Schema(ctx context.Context, req datasour }, }, "subject": schema.SingleNestedAttribute{ - MarkdownDescription: "Users assigned by this binding.", + MarkdownDescription: "User assigned by this binding.", Computed: true, Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ @@ -93,7 +93,7 @@ func (d *projectUserBindingsDataSource) Schema(ctx context.Context, req datasour } } -func (d *projectUserBindingsDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +func (d *projectUserBindingDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { if req.ProviderData == nil { return } @@ -112,8 +112,7 @@ func (d *projectUserBindingsDataSource) Configure(ctx context.Context, req datas d.client = client } -func (d *projectUserBindingsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - // get workspace and project to query for bindings +func (d *projectUserBindingDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var name string resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("metadata").AtName("name"), &name)...) if resp.Diagnostics.HasError() { @@ -122,7 +121,7 @@ func (d *projectUserBindingsDataSource) Read(ctx context.Context, req datasource binding, err := d.client.ReadProjectUserBinding(name) if err != nil { - resp.Diagnostics.AddError("Unable to read project", err.Error()) + resp.Diagnostics.AddError("Unable to read project user binding", err.Error()) } if binding == nil { diff --git a/internal/provider/project_user_binding_resource.go b/internal/provider/project_user_binding_resource.go index a79d37e..be4aa9e 100644 --- a/internal/provider/project_user_binding_resource.go +++ b/internal/provider/project_user_binding_resource.go @@ -199,7 +199,7 @@ func (r *projectUserBindingResource) Delete(ctx context.Context, req resource.De err := r.client.DeleteProjecUserBinding(name) if err != nil { resp.Diagnostics.AddError( - "Error deleting project", + "Error deleting project user binding", "Could not delete project, unexpected error: "+err.Error(), ) return diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 4d48261..00715f4 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -79,6 +79,7 @@ func (p *MeshStackProvider) Resources(ctx context.Context) []func() resource.Res NewProjectResource, NewTenantResource, NewProjectUserBindingResource, + NewProjectGroupBindingResource, } } @@ -87,7 +88,8 @@ func (p *MeshStackProvider) DataSources(ctx context.Context) []func() datasource NewBuildingBlockDataSource, NewProjectDataSource, NewProjectsDataSource, - NewProjectUserBindingsDataSource, + NewProjectUserBindingDataSource, + NewProjectGroupBindingDataSource, NewTenantDataSource, } }