Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add juju_access_offer resource #633

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

amandahla
Copy link
Contributor

@amandahla amandahla commented Nov 19, 2024

Description

This is a PR for adding the juju_access_offer resource based on schema described in #627

Type of change

<What type of a change is this? Please keep one or more relevant lines from below and delete the others.>

  • Add new resource

Environment

  • Juju controller version: 3.5

  • Terraform version: 1.7.4

QA steps

Manual QA steps should be done to test this PR.

provider juju {}
...

Additional notes

<Please add relevant notes & information, links to mattermost chats, other related issues/PRs, anything to help understand and QA the PR.>

Since Update changes the ID, I had to set Access as a RequiresReplace. Otherwise, tests fail with:

=== RUN   TestAcc_ResourceAccessOffer
=== PAUSE TestAcc_ResourceAccessOffer
=== CONT  TestAcc_ResourceAccessOffer
    resource_access_offer_test.go:27: Step 3/4 error: Error running apply: exit status 1
        
        Error: Provider produced inconsistent result after apply
        
        When applying changes to juju_access_offer.access_prometheus_endpoint,
        provider "provider[\"registry.terraform.io/hashicorp/juju\"]" produced an
        unexpected new value: .id: was
        cty.StringVal("admin/tf-access-model-7830324085062068677.prometheus-k8s:consume:tfuser-4861434009413829244"),
        but now
        cty.StringVal("admin/tf-access-model-7830324085062068677.prometheus-k8s:admin:tfuser-4861434009413829244").
        
        This is a bug in the provider, which should be reported in the provider's own
        issue tracker.
--- FAIL: TestAcc_ResourceAccessOffer (40.70s)
FAIL
FAIL	github.com/juju/terraform-provider-juju/internal/provider	40.719s
FAIL

@amandahla amandahla marked this pull request as draft November 19, 2024 21:23
@amandahla amandahla marked this pull request as ready for review November 28, 2024 21:21
@amandahla amandahla changed the title WIP feat: add juju_access_offer resource feat: add juju_access_offer resource Nov 28, 2024
@amandahla amandahla force-pushed the feat/offer-access branch 2 times, most recently from 29c1c47 to df0e20b Compare December 4, 2024 13:40
Copy link
Member

@alesstimec alesstimec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some initial comments. happy to talk about this, if needed.

internal/provider/resource_access_offer.go Show resolved Hide resolved
internal/provider/resource_access_offer.go Show resolved Hide resolved
},
"users": schema.SetAttribute{
Description: "List of users to grant access.",
Optional: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this optional? without users, does an access offer resource make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All attributes regarding users are optional but there is one validator for checking if at least one is set.

}

// Get information from ID
offerURL, _, _ := retrieveAccessOfferDataFromID(ctx, state.ID, state.Users, &resp.Diagnostics)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why ignore the access level from offer ID. i think you should use it to filter out users that don't have the matching access level.

continue
}
users = append(users, offerUserDetail.UserName)
if access != "" && access != string(offerUserDetail.Access) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, i don't think this is quite right. imagine an application offer where alice has admin access, bob has consume access and eve has read access, which is quite a valid state for an application offer. the way this condition is written, this would produce an error here, because we see three different access levels for the same offer.

internal/provider/resource_access_offer.go Show resolved Hide resolved
internal/provider/resource_access_offer.go Show resolved Hide resolved
internal/provider/resource_access_offer.go Show resolved Hide resolved
internal/provider/resource_access_offer.go Show resolved Hide resolved
return nil
}

// This function revokes access to an offer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function revokes a specific level of access to an offer.

I think (correct me if i'm wrong), but access levels for offers work a bit differently in juju.
e.g. if a user has admin access to an offer and we revoke admin access for that user, the user is left with consume access to the offer.. if we revoke consume access from an admin user, that user is left with read access. and if we revoke read access, user is left with no access to the offer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alesstimec you are correct in how revoke on offer works.

The provider should decide which level to grant or revoke, not the internal juju code making the API call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hmlanigan yes, but with the current schema, where juju_access_offer only lists users for a single access level, this is nearly impossible, since you can have a juju_access_offer resource for the consume level and another one for read level and the two don't "know" about eachother.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the godoc is not formatted correctly.

.github/workflows/test_integration.yml Outdated Show resolved Hide resolved

A resource that represent a Juju Access Offer.


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: add examples/resource/juju_access_offer directory with resource.tf and import.sh files, content will be added to this markdown file during make install.

"github.com/juju/juju/core/crossmodel"
"github.com/juju/names/v5"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: move the github.com/juju imports to the second stanza, in sorted order.

resp.Diagnostics.AddError(
"ImportState Failure",
fmt.Sprintf("Malformed AccessOffer ID %q, "+
"please use format '<offer URL>:<access>:<user1,user1>'", IDstr),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: we're having issues with cross controller CMR today in this provider, however an offer URL with a controller uses a colon, e.g. foo:admin/another-model.postgresql. A different delineator would be a good idea.

Comment on lines 240 to 253
// Get user/access info from Offer
response, err := a.client.Offers.ReadOffer(&juju.ReadOfferInput{
OfferURL: offerURL,
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read offer, got error: %s", err))
return
}
a.trace(fmt.Sprintf("read juju offer %q", offerURL))
offerUsers := response.Users
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This data is available in state:

        var state accessOfferResourceOffer

 	// Get the current state
 	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)

Doing it this way has the potential to change access levels for the offer which were not set via this resource.

return nil
}

// This function revokes access to an offer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alesstimec you are correct in how revoke on offer works.

The provider should decide which level to grant or revoke, not the internal juju code making the API call.

offerUserNames[offerUser.UserName] = struct{}{}
if !slices.Contains(planUsers, offerUser.UserName) {
a.trace(fmt.Sprintf("revoke user %q for offer %q", offerUser.UserName, offerURL))
err := a.client.Offers.RevokeOffer(&juju.GrantRevokeOfferInput{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a user has been removed from the resource, then to remove their privileges, you'd Revoke read. Afaik, it's not necessary to step through the levels.

I've also suggested not allowing the admin user as a valid user to avoid problems there.

Comment on lines +433 to +441
_, err = client.ApplicationOffer(input.OfferURL)
if err != nil {
return err
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to do this. RevokeOffer will validate that the offer exists.

Comment on lines +411 to +417
_, err = client.ApplicationOffer(input.OfferURL)
if err != nil {
return err
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to do this. GrantOffer will validate that the offer exists.

users := []string{}
var access string
for _, offerUserDetail := range response.Users {
if offerUserDetail.UserName == "everyone@external" || offerUserDetail.UserName == "admin" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: please document in the schema description the admin user will not be handled via this resource. Suggest failing the validation of the user set as well if admin is specified.

type GrantRevokeOfferInput struct {
User string
Access string
OfferURL string
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be URL's, you can revoke many at once

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we discussed changing the schema in the last terraform office hours, I think this no longer apply, WDYT?

return nil
}

// This function revokes access to an offer.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the godoc is not formatted correctly.

return err
}

err = client.RevokeOffer(input.User, input.Access, input.OfferURL)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be (if we can make the schema look nice) input.OfferURLs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants