Skip to content

Commit

Permalink
fix: add configvalidator
Browse files Browse the repository at this point in the history
  • Loading branch information
amandahla committed Dec 13, 2024
1 parent 35e236a commit 138874f
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 51 deletions.
9 changes: 5 additions & 4 deletions docs/resources/access_offer.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
page_title: "juju_access_offer Resource - terraform-provider-juju"
subcategory: ""
description: |-
A resource that represent a Juju Access Offer.
A resource that represent a Juju Access Offer. Warning: Do not repeat users across different access levels.
---

# juju_access_offer (Resource)

A resource that represent a Juju Access Offer.
A resource that represent a Juju Access Offer. Warning: Do not repeat users across different access levels.



Expand All @@ -17,12 +17,13 @@ A resource that represent a Juju Access Offer.

### Required

- `access` (String) Level of access to grant. Changing this value will replace the Terraform resource. Valid access levels are described at https://juju.is/docs/juju/manage-offers#control-access-to-an-offer
- `offer_url` (String) The url of the offer for access management. If this is changed the resource will be deleted and a new resource will be created.

### Optional

- `users` (Set of String) List of users to grant access.
- `admin` (Set of String) List of users to grant admin access. "admin" user is not allowed.
- `consume` (Set of String) List of users to grant consume access. "admin" user is not allowed.
- `read` (Set of String) List of users to grant read access. "admin" user is not allowed.

### Read-Only

Expand Down
145 changes: 98 additions & 47 deletions internal/provider/resource_access_offer.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var _ resource.Resource = &accessOfferResource{}
var _ resource.ResourceWithConfigure = &accessOfferResource{}
var _ resource.ResourceWithImportState = &accessOfferResource{}
var _ resource.ResourceWithConfigValidators = &accessOfferResource{}
var _ resource.ResourceWithValidateConfig = &accessOfferResource{}

// NewAccessOfferResource returns a new instance of the Access Offer resource.
func NewAccessOfferResource() resource.Resource {
Expand Down Expand Up @@ -65,23 +66,23 @@ func (a *accessOfferResource) Schema(_ context.Context, req resource.SchemaReque
Description: "A resource that represent a Juju Access Offer. Warning: Do not repeat users across different access levels.",
Attributes: map[string]schema.Attribute{
string(permission.AdminAccess): schema.SetAttribute{
Description: "List of users to grant admin access.",
Description: "List of users to grant admin access. \"admin\" user is not allowed.",
Optional: true,
ElementType: types.StringType,
Validators: []validator.Set{
setvalidator.ValueStringsAre(ValidatorMatchString(names.IsValidUser, "user must be a valid Juju username")),
},
},
string(permission.ConsumeAccess): schema.SetAttribute{
Description: "List of users to grant consume access.",
Description: "List of users to grant consume access. \"admin\" user is not allowed.",
Optional: true,
ElementType: types.StringType,
Validators: []validator.Set{
setvalidator.ValueStringsAre(ValidatorMatchString(names.IsValidUser, "user must be a valid Juju username")),
},
},
string(permission.ReadAccess): schema.SetAttribute{
Description: "List of users to grant read access.",
Description: "List of users to grant read access. \"admin\" user is not allowed.",
Optional: true,
ElementType: types.StringType,
Validators: []validator.Set{
Expand Down Expand Up @@ -129,46 +130,30 @@ func (a *accessOfferResource) Create(ctx context.Context, req resource.CreateReq

// Get the users to grant admin
var adminUsers []string
resp.Diagnostics.Append(plan.AdminUsers.ElementsAs(ctx, &adminUsers, false)...)
if resp.Diagnostics.HasError() {
return
if !plan.AdminUsers.IsNull() {
resp.Diagnostics.Append(plan.AdminUsers.ElementsAs(ctx, &adminUsers, false)...)
if resp.Diagnostics.HasError() {
return
}
}

// Get the users to grant consume
var consumeUsers []string
resp.Diagnostics.Append(plan.ConsumeUsers.ElementsAs(ctx, &consumeUsers, false)...)
if resp.Diagnostics.HasError() {
return
if !plan.ConsumeUsers.IsNull() {
resp.Diagnostics.Append(plan.ConsumeUsers.ElementsAs(ctx, &consumeUsers, false)...)
if resp.Diagnostics.HasError() {
return
}
}

// Get the users to grant read
var readUsers []string
resp.Diagnostics.Append(plan.ReadUsers.ElementsAs(ctx, &readUsers, false)...)
if resp.Diagnostics.HasError() {
return
}

// Validate if there are repeated user
combinedUsers := append(append(adminUsers, consumeUsers...), readUsers...)
slices.Sort(combinedUsers)
originalCount := len(combinedUsers)
compactedUsers := slices.Compact(combinedUsers)
compactedCount := len(compactedUsers)
if originalCount != compactedCount {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create access offer resource, got same user in different access."))
return
}

// Get the offer
offerURLStr := plan.OfferURL.ValueString()
response, err := a.client.Offers.ReadOffer(&juju.ReadOfferInput{
OfferURL: offerURLStr,
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create access offer resource, got error: %s", err))
return
if !plan.ReadUsers.IsNull() {
resp.Diagnostics.Append(plan.ReadUsers.ElementsAs(ctx, &readUsers, false)...)
if resp.Diagnostics.HasError() {
return
}
}
a.trace(fmt.Sprintf("read offer %q at %q", response.Name, response.OfferURL))

// Call Offers.GrantOffer
users := make(map[permission.Access][]string)
Expand All @@ -180,14 +165,16 @@ func (a *accessOfferResource) Create(ctx context.Context, req resource.CreateReq
err := a.client.Offers.GrantOffer(&juju.GrantRevokeOfferInput{
Users: users,
Access: string(access),
OfferURL: offerURLStr,
OfferURL: plan.OfferURL.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create access offer resource, got error: %s", err))
return
}
}
plan.ID = types.StringValue(response.OfferURL)

// Set ID as the offer URL
plan.ID = plan.OfferURL

// Set the plan onto the Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
Expand Down Expand Up @@ -239,16 +226,27 @@ func (a *accessOfferResource) Read(ctx context.Context, req resource.ReadRequest
users[offerUserDetail.Access] = append(users[offerUserDetail.Access], offerUserDetail.UserName)
}

// Save found users to state
for access, user := range users {
stateUsers, errDiag := basetypes.NewSetValueFrom(ctx, types.StringType, users)
resp.Diagnostics.Append(errDiag...)
if resp.Diagnostics.HasError() {
return
}
state.Users = newStateUsers
// Save admin users to state
adminUsersSet, errDiag := basetypes.NewSetValueFrom(ctx, types.StringType, users[permission.AdminAccess])
resp.Diagnostics.Append(errDiag...)
if resp.Diagnostics.HasError() {
return
}

state.AdminUsers = adminUsersSet
// Save consume users to state
consumeUsersSet, errDiag := basetypes.NewSetValueFrom(ctx, types.StringType, users[permission.ConsumeAccess])
resp.Diagnostics.Append(errDiag...)
if resp.Diagnostics.HasError() {
return
}
state.ConsumeUsers = consumeUsersSet
// Save read users to state
readUsersSet, errDiag := basetypes.NewSetValueFrom(ctx, types.StringType, users[permission.ReadAccess])
resp.Diagnostics.Append(errDiag...)
if resp.Diagnostics.HasError() {
return
}
state.ReadUsers = readUsersSet
// Set the plan onto the Terraform state
state.OfferURL = types.StringValue(offerURL)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
Expand Down Expand Up @@ -285,10 +283,10 @@ func (a *accessOfferResource) Configure(ctx context.Context, req resource.Config
}

// ConfigValidators sets validators for the resource.
func (r *accessOfferResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator {
func (a *accessOfferResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator {
// JAAS users should use juju_jaas_access_offer instead.
return []resource.ConfigValidator{
NewAvoidJAASValidator(r.client, "juju_jaas_access_offer"),
NewAvoidJAASValidator(a.client, "juju_jaas_access_offer"),
resourcevalidator.AtLeastOneOf(
path.MatchRoot(string(permission.AdminAccess)),
path.MatchRoot(string(permission.ConsumeAccess)),
Expand All @@ -297,6 +295,59 @@ func (r *accessOfferResource) ConfigValidators(ctx context.Context) []resource.C
}
}

func (a *accessOfferResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
// TODO this validation does not work in case user name depends on the output of other resource
var configData accessOfferResourceOffer

// Read Terraform configuration from the request into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &configData)...)
if resp.Diagnostics.HasError() {
return
}

// Get the users to grant admin
var adminUsers []string
if !configData.AdminUsers.IsNull() {
resp.Diagnostics.Append(configData.AdminUsers.ElementsAs(ctx, &adminUsers, false)...)
if resp.Diagnostics.HasError() {
return
}
}

// Get the users to grant consume
var consumeUsers []string
if !configData.ConsumeUsers.IsNull() {
resp.Diagnostics.Append(configData.ConsumeUsers.ElementsAs(ctx, &consumeUsers, false)...)
if resp.Diagnostics.HasError() {
return
}
}

// Get the users to grant read
var readUsers []string
if !configData.ReadUsers.IsNull() {
resp.Diagnostics.Append(configData.ReadUsers.ElementsAs(ctx, &readUsers, false)...)
if resp.Diagnostics.HasError() {
return
}
}

combinedUsers := append(append(adminUsers, consumeUsers...), readUsers...)
// Validate if there are repeated user
if slices.Contains(combinedUsers, "admin") {
resp.Diagnostics.AddAttributeError(path.Root("offer_url"), "Attribute Error", "\"admin\" user is not allowed")
}
// Validate if there are repeated user
slices.Sort(combinedUsers)
originalCount := len(combinedUsers)
compactedUsers := slices.Compact(combinedUsers)
compactedCount := len(compactedUsers)
if originalCount != compactedCount {
resp.Diagnostics.AddAttributeError(path.Root("offer_url"), "Attribute Error", "do not repeat users across different access levels")
}

}

Check failure on line 349 in internal/provider/resource_access_offer.go

View workflow job for this annotation

GitHub Actions / golangci-lint

unnecessary trailing newline (whitespace)

// ImportState import existing resource to the state.
func (a *accessOfferResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
Expand Down

0 comments on commit 138874f

Please sign in to comment.