ec/ecresource/organizationresource/update.go (214 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package organizationresource
import (
"context"
"fmt"
"github.com/elastic/cloud-sdk-go/pkg/api/organizationapi"
"github.com/elastic/cloud-sdk-go/pkg/models"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"sort"
"strings"
)
func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
diagnostics := &response.Diagnostics
var plan Organization
var state Organization
diagnostics.Append(request.Plan.Get(ctx, &plan)...)
diagnostics.Append(request.State.Get(ctx, &state)...)
if diagnostics.HasError() {
return
}
organizationID := plan.ID.ValueString()
planMembers := make(map[string]types.Object)
diags := plan.Members.ElementsAs(ctx, &planMembers, false)
if diags.HasError() {
diagnostics.Append(diags...)
return
}
stateMembers := make(map[string]types.Object)
diags = state.Members.ElementsAs(ctx, &stateMembers, false)
if diags.HasError() {
diagnostics.Append(diags...)
return
}
// Create new members, update changed members
for email, planMember := range planMembers {
planMemberModel := toModel(ctx, planMember, diagnostics)
if diagnostics.HasError() {
continue
}
// create new invitation if member is in plan but not in state
stateMember, ok := stateMembers[email]
if !ok {
r.createInvitation(ctx, email, planMemberModel, organizationID, diagnostics)
} else {
// member is in plan and state, update if there is a diff
if !stateMember.Equal(planMember) {
stateMemberModel := toModel(ctx, stateMember, diagnostics)
if diagnostics.HasError() {
continue
}
r.updateMember(ctx, email, stateMemberModel, planMemberModel, organizationID, diagnostics)
}
}
}
// Delete removed members
for email, stateMember := range stateMembers {
_, ok := planMembers[email]
if !ok {
// member is in state, but not in plan
stateMemberModel := toModel(ctx, stateMember, diagnostics)
if diagnostics.HasError() {
continue
}
r.deleteMember(email, stateMemberModel, organizationID, diagnostics)
}
}
// Re-read the whole org from the API to get the current state
updatedOrganization := r.readFromApi(ctx, organizationID, diagnostics)
if diagnostics.HasError() {
return
}
diagnostics.Append(response.State.Set(ctx, *updatedOrganization)...)
}
func (r *Resource) updateMember(
ctx context.Context,
email string,
stateMember OrganizationMember,
planMember OrganizationMember,
organizationID string,
diagnostics *diag.Diagnostics,
) {
if planMember.InvitationPending.ValueBool() {
// Invitations can't be updated, so while the invitation is pending the role assignments can't be changed
// The only way to update them is by creating a new invitation with the right role-assignments.
r.deleteInvitation(email, organizationID, diagnostics)
r.createInvitation(ctx, email, planMember, organizationID, diagnostics)
} else {
// Add new role assignments
planApiMember := modelToApi(ctx, planMember, organizationID, diagnostics)
if diagnostics.HasError() {
return
}
stateApiMember := modelToApi(ctx, stateMember, organizationID, diagnostics)
if diagnostics.HasError() {
return
}
add, remove := diffRoleAssignments(stateApiMember.RoleAssignments, planApiMember.RoleAssignments)
if hasChanges(add) {
_, err := organizationapi.AddRoleAssignments(organizationapi.AddRoleAssignmentsParams{
API: r.client,
UserID: planMember.UserID.ValueString(),
RoleAssignments: add,
})
if err != nil {
diagnostics.Append(diag.NewErrorDiagnostic("Updating member roles failed.", err.Error()))
return
}
}
if hasChanges(remove) {
_, err := organizationapi.RemoveRoleAssignments(organizationapi.RemoveRoleAssignmentsParams{
API: r.client,
UserID: planMember.UserID.ValueString(),
RoleAssignments: remove,
})
if err != nil {
diagnostics.Append(diag.NewErrorDiagnostic("Updating member roles failed.", err.Error()))
return
}
}
}
}
func hasChanges(ra models.RoleAssignments) bool {
if len(ra.Organization) > 0 {
return true
}
if len(ra.Deployment) > 0 {
return true
}
if ra.Project != nil {
return len(ra.Project.Elasticsearch) > 0 ||
len(ra.Project.Security) > 0 ||
len(ra.Project.Observability) > 0
}
return false
}
func toModel(ctx context.Context, member types.Object, diags *diag.Diagnostics) OrganizationMember {
var modelValue OrganizationMember
var objectAsOptions = basetypes.ObjectAsOptions{UnhandledNullAsEmpty: false, UnhandledUnknownAsEmpty: false}
diags.Append(member.As(ctx, &modelValue, objectAsOptions)...)
return modelValue
}
func diffRoleAssignments(old, new *models.RoleAssignments) (models.RoleAssignments, models.RoleAssignments) {
addOrganization, removeOrganization := diffOrganizationRoleAssignments(
old.Organization,
new.Organization,
)
addDeployment, removeDeployment := diffDeploymentRoleAssignments(
old.Deployment,
new.Deployment,
)
var addProject, removeProject *models.ProjectRoleAssignments
if old.Project != nil && new.Project != nil {
addProject, removeProject = diffProjectRoleAssignments(
*old.Project,
*new.Project,
)
} else if old.Project == nil && new.Project != nil {
addProject, removeProject = new.Project, nil
} else if old.Project != nil && new.Project == nil {
addProject, removeProject = nil, old.Project
}
add := models.RoleAssignments{
Organization: addOrganization,
Deployment: addDeployment,
Project: addProject,
}
remove := models.RoleAssignments{
Organization: removeOrganization,
Deployment: removeDeployment,
Project: removeProject,
}
return add, remove
}
func diffOrganizationRoleAssignments(old, new []*models.OrganizationRoleAssignment) ([]*models.OrganizationRoleAssignment, []*models.OrganizationRoleAssignment) {
getKey := func(ra models.OrganizationRoleAssignment) string {
return *ra.RoleID
}
add := difference(new, old, getKey)
remove := difference(old, new, getKey)
return add, remove
}
func diffDeploymentRoleAssignments(old, new []*models.DeploymentRoleAssignment) ([]*models.DeploymentRoleAssignment, []*models.DeploymentRoleAssignment) {
getKey := func(ra models.DeploymentRoleAssignment) string {
var all bool
if ra.All != nil {
all = *ra.All
}
sort.Strings(ra.DeploymentIds)
return fmt.Sprintf("%s-%t-%s", *ra.RoleID, all, strings.Join(ra.DeploymentIds, ","))
}
add := difference(new, old, getKey)
remove := difference(old, new, getKey)
return add, remove
}
func diffProjectRoleAssignments(old, new models.ProjectRoleAssignments) (*models.ProjectRoleAssignments, *models.ProjectRoleAssignments) {
getKey := func(ra models.ProjectRoleAssignment) string {
var all bool
if ra.All != nil {
all = *ra.All
}
sort.Strings(ra.ProjectIds)
return fmt.Sprintf("%s-%t-%s", *ra.RoleID, all, strings.Join(ra.ProjectIds, ","))
}
addElasticsearch := difference(new.Elasticsearch, old.Elasticsearch, getKey)
removeElasticsearch := difference(old.Elasticsearch, new.Elasticsearch, getKey)
addObservability := difference(new.Observability, old.Observability, getKey)
removeObservability := difference(old.Observability, new.Observability, getKey)
addSecurity := difference(new.Security, old.Security, getKey)
removeSecurity := difference(old.Security, new.Security, getKey)
add := models.ProjectRoleAssignments{
Elasticsearch: addElasticsearch,
Observability: addObservability,
Security: addSecurity,
}
remove := models.ProjectRoleAssignments{
Elasticsearch: removeElasticsearch,
Observability: removeObservability,
Security: removeSecurity,
}
return &add, &remove
}