diff --git a/README.md b/README.md index ce0b25e..0649c07 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,11 @@ scenario. The reason behind every resources and data sources are stated as below This resource is designed to attach a list of clusters' kubernetes role permissions (CS) with a (RAM) user, and to replace the official Alicloud Terraform Provider's resource [*alicloud_cs_kubernetes_permissions*](https://registry.terraform.io/providers/aliyun/alicloud/latest/docs/resources/cs_kubernetes_permissions). The official resource will overwrite all the permissions which is attached with the user, which means it will remove the permissions from other clusters. +- **st-alicloud_service_mesh_user_permission** + + This resource is designed to attach a list of ASM clusters' permissions with a (RAM) user, and to replace the official Alicloud Terraform Provider's resource [*alicloud_service_mesh_user_permission*](https://registry.terraform.io/providers/aliyun/alicloud/latest/docs/resources/service_mesh_user_permission). + The official resource will overwrite all the permissions which is attached with the user, which means it will remove the permissions from other ASM clusters. + ### Data Sources - **st-alicloud_ddoscoo_domain_resources** diff --git a/alicloud/provider.go b/alicloud/provider.go index a167995..c016dc9 100644 --- a/alicloud/provider.go +++ b/alicloud/provider.go @@ -24,23 +24,25 @@ import ( alicloudRamClient "github.com/alibabacloud-go/ram-20150501/v2/client" alicloudSlbClient "github.com/alibabacloud-go/slb-20140515/v4/client" alicloudEssClient "github.com/alibabacloud-go/ess-20220222/v2/client" + alicloudServicemeshClient "github.com/alibabacloud-go/servicemesh-20200111/v4/client" "github.com/alibabacloud-go/tea/tea" ) // Wrapper of AliCloud client type alicloudClients struct { - baseClient *alicloudBaseClient.Client - cdnClient *alicloudCdnClient.Client - antiddosClient *alicloudAntiddosClient.Client - slbClient *alicloudSlbClient.Client - dnsClient *alicloudDnsClient.Client - ramClient *alicloudRamClient.Client - cmsClient *alicloudCmsClient.Client - adbClient *alicloudAdbClient.Client - emrClient *alicloudEmrClient.Client - csClient *alicloudCsClient.Client - essClient *alicloudEssClient.Client + baseClient *alicloudBaseClient.Client + cdnClient *alicloudCdnClient.Client + antiddosClient *alicloudAntiddosClient.Client + slbClient *alicloudSlbClient.Client + dnsClient *alicloudDnsClient.Client + ramClient *alicloudRamClient.Client + cmsClient *alicloudCmsClient.Client + adbClient *alicloudAdbClient.Client + emrClient *alicloudEmrClient.Client + csClient *alicloudCsClient.Client + essClient *alicloudEssClient.Client + servicemeshClient *alicloudServicemeshClient.Client } // Ensure the implementation satisfies the expected interfaces @@ -358,19 +360,35 @@ func (p *alicloudProvider) Configure(ctx context.Context, req provider.Configure return } + // AliCloud Servicemesh Client + servicemeshClientConfig := clientCredentialsConfig + servicemeshClientConfig.Endpoint = tea.String("servicemesh.aliyuncs.com") + servicemeshClient, err := alicloudServicemeshClient.NewClient(servicemeshClientConfig) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Create AliCloud Servicemesh API Client", + "An unexpected error occurred when creating the AliCloud Servicemesh API client. "+ + "If the error is not clear, please contact the provider developers.\n\n"+ + "AliCloud Servicemesh Client Error: "+err.Error(), + ) + return + } + // AliCloud clients wrapper alicloudClients := alicloudClients{ - baseClient: baseClient, - cdnClient: cdnClient, - antiddosClient: antiddosClient, - slbClient: slbClient, - dnsClient: dnsClient, - ramClient: ramClient, - cmsClient: cmsClient, - adbClient: adbClient, - emrClient: emrClient, - csClient: csClient, - essClient: essClient, + baseClient: baseClient, + cdnClient: cdnClient, + antiddosClient: antiddosClient, + slbClient: slbClient, + dnsClient: dnsClient, + ramClient: ramClient, + cmsClient: cmsClient, + adbClient: adbClient, + emrClient: emrClient, + csClient: csClient, + essClient: essClient, + servicemeshClient: servicemeshClient, } resp.DataSourceData = alicloudClients @@ -403,5 +421,6 @@ func (p *alicloudProvider) Resources(_ context.Context) []func() resource.Resour NewDdosCooWebAIProtectConfigResource, NewEssClbDefaultServerGroupAttachmentResource, NewCsKubernetesPermissionsResource, + NewServicemeshUserPermissionResource, } } diff --git a/alicloud/resource_servicemesh_user_permission.go b/alicloud/resource_servicemesh_user_permission.go new file mode 100644 index 0000000..f761fce --- /dev/null +++ b/alicloud/resource_servicemesh_user_permission.go @@ -0,0 +1,436 @@ +package alicloud + +import ( + "context" + "reflect" + + // "strconv" + "encoding/json" + "time" + + util "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" + "github.com/cenkalti/backoff/v4" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + alicloudServicemeshClient "github.com/alibabacloud-go/servicemesh-20200111/v4/client" +) + +var ( + _ resource.Resource = &servicemeshUserPermissionResource{} + _ resource.ResourceWithConfigure = &servicemeshUserPermissionResource{} +) + +func NewServicemeshUserPermissionResource() resource.Resource { + return &servicemeshUserPermissionResource{} +} + +type servicemeshUserPermissionResource struct { + client *alicloudServicemeshClient.Client +} + +type servicemeshUserPermissionModel struct { + SubAccountUserId types.String `tfsdk:"sub_account_user_id"` + ServiceMeshUserPermissions []*serviceMeshUserPermissions `tfsdk:"permissions"` +} + +type serviceMeshUserPermissions struct { + ServiceMeshId types.String `tfsdk:"service_mesh_id"` + IsCustom types.Bool `tfsdk:"is_custom"` + RoleName types.String `tfsdk:"role_name"` + RoleType types.String `tfsdk:"role_type"` + IsRamRole types.Bool `tfsdk:"is_ram_role"` +} + +type userPermissions struct { + Cluster string + IsCustom bool + RoleName string + RoleType string + IsRamRole bool +} + +// Metadata returns the Service Mesh User Permissions resource name. +func (r *servicemeshUserPermissionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_mesh_user_permission" +} + +// Schema defines the schema for the Service Mesh User Permissions resource. +func (r *servicemeshUserPermissionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Attach service mesh' role permissions (ASM) with a RAM user.", + Attributes: map[string]schema.Attribute{ + "sub_account_user_id": schema.StringAttribute{ + Description: "The ID of the RAM user, and it can also be the id of the Ram Role. If you use Ram Role id, you need to set is_ram_role to true during authorization.", + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "permissions": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "service_mesh_id": schema.StringAttribute{ + Description: "The ID of the service mesh that you want to manage.", + Optional: true, + }, + "is_custom": schema.BoolAttribute{ + Description: "Specifies whether the grant object is a RAM role.", + Optional: true, + }, + "role_name": schema.StringAttribute{ + Description: "Specifies the predefined role that you want to assign. Valid values: [ istio-admin, istio-ops, istio-readonly ].", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf("istio-admin", "istio-ops", "istio-readonly"), + }, + }, + "role_type": schema.StringAttribute{ + Description: "The role type. Valid values: `custom`.", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf("custom"), + }, + }, + "is_ram_role": schema.BoolAttribute{ + Description: "Specifies whether the grant object is an entity.", + Optional: true, + }, + }, + }, + }, + }, + } +} + +// Configure adds the provider configured client to the resource. +func (r *servicemeshUserPermissionResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + r.client = req.ProviderData.(alicloudClients).servicemeshClient +} + +// Add Service Mesh user permissions with a RAM user. +func (r *servicemeshUserPermissionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan *servicemeshUserPermissionModel + getStateDiags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(getStateDiags...) + if resp.Diagnostics.HasError() { + return + } + + // Query the user's existing permissions + existingPerms, err := r.describeUserPermissions(plan.SubAccountUserId.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "[API ERROR] Failed to query user's existing permission.", + err.Error(), + ) + return + } + + // Append the existing permissions with the permission from plan result + // Convert the permissions list to a Json String + perms, err := json.Marshal(convertBaseTypeToPrimitiveDataType(append(existingPerms, plan.ServiceMeshUserPermissions...))) + if err != nil { + resp.Diagnostics.AddError( + "[API ERROR] Failed to convert the permissions list to a json string.", + err.Error(), + ) + return + } + + // Grant permissions for user + err = r.grantPermissions(plan.SubAccountUserId.ValueString(), string(perms)) + if err != nil { + resp.Diagnostics.AddError( + "[API ERROR] Failed to grant permissions for user.", + err.Error(), + ) + return + } + + // Set state items + state := &servicemeshUserPermissionModel{ + SubAccountUserId: plan.SubAccountUserId, + ServiceMeshUserPermissions: plan.ServiceMeshUserPermissions, + } + + // Set state to fully populated data + setStateDiags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(setStateDiags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read function (Do nothing). +func (r *servicemeshUserPermissionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Retrieve values from state + var state *servicemeshUserPermissionModel + getStateDiags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(getStateDiags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update the Service Mesh user permissions from a RAM user. +func (r *servicemeshUserPermissionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan + var plan *servicemeshUserPermissionModel + getPlanDiags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(getPlanDiags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve values from state + var state *servicemeshUserPermissionModel + getStateDiags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(getStateDiags...) + if resp.Diagnostics.HasError() { + return + } + + // Query the user's existing permissions + existingPerms, err := r.describeUserPermissions(plan.SubAccountUserId.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "[API ERROR] Failed to query user's existing permission.", + err.Error(), + ) + return + } + + // Only remove the permissions from terraform state. + var updatedPermission []*userPermissions + for _, extPerm := range convertBaseTypeToPrimitiveDataType(existingPerms) { + isExist := []bool{} + for _, perm := range convertBaseTypeToPrimitiveDataType(state.ServiceMeshUserPermissions) { + isExist = append(isExist, reflect.DeepEqual(extPerm, perm)) + } + if isAllFalse(isExist) { + updatedPermission = append(updatedPermission, extPerm) + } + } + + // Append the plan permissions with existing permissions + // Convert the permissions list to a Json String + perms, err := json.Marshal(append(updatedPermission, convertBaseTypeToPrimitiveDataType(plan.ServiceMeshUserPermissions)...)) + if err != nil { + resp.Diagnostics.AddError( + "[API ERROR] Failed to convert the permissions list to a json string.", + err.Error(), + ) + return + } + + // Grant permission for user + err = r.grantPermissions(plan.SubAccountUserId.ValueString(), string(perms)) + if err != nil { + resp.Diagnostics.AddError( + "[API ERROR] Failed to grant permissions for user.", + err.Error(), + ) + return + } + + // Set state items + state = &servicemeshUserPermissionModel{ + SubAccountUserId: plan.SubAccountUserId, + ServiceMeshUserPermissions: plan.ServiceMeshUserPermissions, + } + + // Set state to fully populated data + setStateDiags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(setStateDiags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Remove the CS kubernetes permissions from a RAM user. +func (r *servicemeshUserPermissionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state *servicemeshUserPermissionModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Query the user's existing permissions + existingPerms, err := r.describeUserPermissions(state.SubAccountUserId.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "[API ERROR] Failed to query user's existing permission.", + err.Error(), + ) + return + } + + // Only remove the permissions from terraform state. + var preservedPerms []*userPermissions + for _, extPerm := range convertBaseTypeToPrimitiveDataType(existingPerms) { + isExist := []bool{} + for _, perm := range convertBaseTypeToPrimitiveDataType(state.ServiceMeshUserPermissions) { + isExist = append(isExist, reflect.DeepEqual(extPerm, perm)) + } + if isAllFalse(isExist) { + preservedPerms = append(preservedPerms, extPerm) + } + } + + // Convert the permissions list to a Json String + perms, err := json.Marshal(preservedPerms) + if err != nil { + resp.Diagnostics.AddError( + "[API ERROR] Failed to convert the permissions list to a json string.", + err.Error(), + ) + return + } + + // Grant permission for user + err = r.grantPermissions(state.SubAccountUserId.ValueString(), string(perms)) + if err != nil { + resp.Diagnostics.AddError( + "[API ERROR] Failed to remove permissions for user.", + err.Error(), + ) + return + } +} + +func isAllFalse(list []bool) bool { + for _, value := range list { + if value == true { + return false + } + } + return true +} + +// Convert basetype to primitive data type +func convertBaseTypeToPrimitiveDataType(baseTypeList []*serviceMeshUserPermissions) []*userPermissions { + var primitiveDataTypeList []*userPermissions + + for _, value := range baseTypeList { + primitiveDataTypeList = append(primitiveDataTypeList, &userPermissions{ + Cluster: value.ServiceMeshId.ValueString(), + IsCustom: value.IsCustom.ValueBool(), + RoleName: value.RoleName.ValueString(), + RoleType: value.RoleType.ValueString(), + IsRamRole: value.IsRamRole.ValueBool(), + }) + } + + return primitiveDataTypeList +} + +// Query user's existing permission +func (r *servicemeshUserPermissionResource) describeUserPermissions(uid string) ([]*serviceMeshUserPermissions, error) { + var describeUserPermissionsResponse *alicloudServicemeshClient.DescribeUserPermissionsResponse + var permissions []*serviceMeshUserPermissions + var err error + + // Retry backoff function + describeUserPermissions := func() error { + runtime := &util.RuntimeOptions{} + describeUserPermissionsRequest := &alicloudServicemeshClient.DescribeUserPermissionsRequest{ + SubAccountUserId: tea.String(uid), + } + describeUserPermissionsResponse, err = r.client.DescribeUserPermissionsWithOptions(describeUserPermissionsRequest, runtime) + if err != nil { + if _t, ok := err.(*tea.SDKError); ok { + if isAbleToRetry(*_t.Code) { + return err + } else { + return backoff.Permanent(err) + } + } else { + return err + } + } + + return nil + } + + // Retry backoff + reconnectBackoff := backoff.NewExponentialBackOff() + reconnectBackoff.MaxElapsedTime = 30 * time.Second + err = backoff.Retry(describeUserPermissions, reconnectBackoff) + if err != nil { + return permissions, err + } + + for _, permission := range describeUserPermissionsResponse.Body.Permissions { + perm := &serviceMeshUserPermissions{ + ServiceMeshId: types.StringValue(*permission.ResourceId), + IsCustom: types.BoolValue(true), + RoleName: types.StringValue(*permission.RoleName), + RoleType: types.StringValue(*permission.RoleType), + IsRamRole: types.BoolValue(false), + } + + // check if the response returns the attribute IsRamRole + // hasRamRole := reflect.ValueOf(permission).FieldByName("IsRamRole") + // if hasRamRole.IsValid() { + // isRamRole, err := strconv.ParseBool(*permission.IsRamRole) + // if err != nil { + // return permissions, err + // } + // perm.IsRamRole = types.BoolValue(isRamRole) + // } + + permissions = append(permissions, perm) + } + + return permissions, nil +} + +// Grant Service Mesh permissions for user +func (r *servicemeshUserPermissionResource) grantPermissions(uid string, permString string) error { + var err error + + // Retry backoff function + grantPermissions := func() error { + runtime := &util.RuntimeOptions{} + + grantUserPermissionsRequest := &alicloudServicemeshClient.GrantUserPermissionsRequest{ + SubAccountUserId: tea.String(uid), + Permissions: tea.String(permString), + } + + _, err = r.client.GrantUserPermissionsWithOptions(grantUserPermissionsRequest, runtime) + if err != nil { + if _t, ok := err.(*tea.SDKError); ok { + if isAbleToRetry(*_t.Code) { + return err + } else { + return backoff.Permanent(err) + } + } else { + return err + } + } + + return nil + } + + // Retry backoff + reconnectBackoff := backoff.NewExponentialBackOff() + reconnectBackoff.MaxElapsedTime = 30 * time.Second + err = backoff.Retry(grantPermissions, reconnectBackoff) + if err != nil { + return err + } + + return nil +} diff --git a/docs/resources/servicemesh_user_permission.md b/docs/resources/servicemesh_user_permission.md new file mode 100644 index 0000000..c1d701c --- /dev/null +++ b/docs/resources/servicemesh_user_permission.md @@ -0,0 +1,45 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "st-alicloud_service_mesh_user_permission Resource - terraform-provider-st-alicloud" +subcategory: "" +description: |- + Attach Service Mesh (ASM) clusters' permissions with a RAM user. +--- + +# st-alicloud_service_mesh_user_permission (Resource) + +Attach Service Mesh (ASM) clusters' permissions with a RAM user. + +## Example Usage + +```terraform +resource "st-alicloud_service_mesh_user_permission" "default" { + sub_account_user_id = "201122334455667789" + + permissions { + role_name = "istio-admin" + service_mesh_id = "cxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + role_type = "custom" + is_custom = true + is_ram_role = false + } +} +``` + + +## Schema + +### Required + +- `sub_account_user_id` (String) The ID of the RAM user, and it can also be the id of the Ram Role. If you use Ram Role id, you need to set is_ram_role to true during authorization. +- `permissions` (Attributes List) A list of permissions. (see [below for nested schema](#nestedatt--permissions)) + + +### Nested Schema for `permissions` + +Optional: +- `service_mesh_id` (String) The ID of the ASM that you want to manage. +- `role_name` (String) Specifies the predefined role that you want to assign. Valid values: [ "istio-admin", "istio-ops", "istio-readonly" ]. +- `role_type` (String) The role type. Valid values: [ "custom" ]. +- `is_custom` (Bool) Specifies whether the grant object is a RAM role. +- `is_ram_role` (Bool) Specifies whether the permissions are granted to a RAM role. When `sub_account_user_id` is ram role id, the value of is_ram_role must be true. diff --git a/examples/resources/st-alicloud_servicemesh_user_permission/resource.tf b/examples/resources/st-alicloud_servicemesh_user_permission/resource.tf new file mode 100644 index 0000000..fc222ec --- /dev/null +++ b/examples/resources/st-alicloud_servicemesh_user_permission/resource.tf @@ -0,0 +1,11 @@ +resource "st-alicloud_service_mesh_user_permission" "default" { + sub_account_user_id = "201122334455667789" + + permissions { + role_name = "istio-admin" + service_mesh_id = "cxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + role_type = "custom" + is_custom = true + is_ram_role = false + } +} diff --git a/go.mod b/go.mod index 74b0937..e185c2f 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,7 @@ require ( github.com/alibabacloud-go/emr-20210320 v1.1.0 github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect github.com/alibabacloud-go/ram-20150501/v2 v2.0.0 + github.com/alibabacloud-go/servicemesh-20200111/v4 v4.3.1 github.com/alibabacloud-go/tea-utils v1.4.5 // indirect github.com/alibabacloud-go/tea-xml v1.1.2 // indirect github.com/aliyun/credentials-go v1.2.6 // indirect diff --git a/go.sum b/go.sum index 1b9948a..8ca2242 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= github.com/alibabacloud-go/ram-20150501/v2 v2.0.0 h1:7tKbdsJBn59lXekqzbi/t6FV0HmUdd4IkVHuYLUtR24= github.com/alibabacloud-go/ram-20150501/v2 v2.0.0/go.mod h1:DQFbLIWsFP16uwTnuIA7WoVdawxEXp8HygyeAKLUnSE= +github.com/alibabacloud-go/servicemesh-20200111/v4 v4.3.1 h1:qDglXllcA9lxVf0b2GyHuq5qA73RZVlR1m/pVW7vTlw= +github.com/alibabacloud-go/servicemesh-20200111/v4 v4.3.1/go.mod h1:sm2Jt/ujWlfkZQFAPcO7qyOjmIZzRUEkAhp590LyvFU= github.com/alibabacloud-go/slb-20140515/v4 v4.0.1 h1:iV30qBxECF4TP1guGf3T3QJiCqdAIuaYV5Ohz4rKqT8= github.com/alibabacloud-go/slb-20140515/v4 v4.0.1/go.mod h1:hv6EDZu9mSyySoYp6G/n6sg894syLggVssYwRw+qAR8= github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=