Skip to content

Commit

Permalink
feat: add juju_access_offer resource
Browse files Browse the repository at this point in the history
  • Loading branch information
amandahla committed Nov 19, 2024
1 parent b0a8fe4 commit 0417e68
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 0 deletions.
28 changes: 28 additions & 0 deletions internal/juju/offers.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ type RemoveRemoteOfferInput struct {
OfferURL string
}

type GrantOfferInput struct {
User string
Access string
OfferURL string
}

func newOffersClient(sc SharedClient) *offersClient {
return &offersClient{
SharedClient: sc,
Expand Down Expand Up @@ -390,3 +396,25 @@ func (c offersClient) RemoveRemoteOffer(input *RemoveRemoteOfferInput) []error {

return nil
}

// This function adds access to an offer
func (c offersClient) GrantOffer(input GrantOfferInput) error {
conn, err := c.GetConnection(nil)
if err != nil {
return err
}
defer func() { _ = conn.Close() }()

client := applicationoffers.NewClient(conn)
_, err = client.ApplicationOffer(input.OfferURL)
if err != nil {
return err
}

err = client.GrantOffer(input.User, input.Access, input.OfferURL)
if err != nil {
return err
}

return nil
}
1 change: 1 addition & 0 deletions internal/provider/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (

LogResourceApplication = "resource-application"
LogResourceAccessModel = "resource-access-model"
LogResourceAccessOffer = "resource-access-offer"
LogResourceCredential = "resource-credential"
LogResourceKubernetesCloud = "resource-kubernetes-cloud"
LogResourceMachine = "resource-machine"
Expand Down
201 changes: 201 additions & 0 deletions internal/provider/resource_access_offer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the Apache License, Version 2.0, see LICENCE file for details.

package provider

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"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/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"

"github.com/juju/juju/core/crossmodel"
"github.com/juju/names/v5"
"github.com/juju/terraform-provider-juju/internal/juju"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &accessOfferResource{}
var _ resource.ResourceWithConfigure = &accessOfferResource{}
var _ resource.ResourceWithImportState = &accessOfferResource{}
var _ resource.ResourceWithConfigValidators = &accessOfferResource{}

func NewAccessOfferResource() resource.Resource {
return &accessOfferResource{}
}

type accessOfferResource struct {
client *juju.Client

// subCtx is the context created with the new tflog subsystem for applications.
subCtx context.Context
}

type accessOfferResourceOffer struct {
OfferURL types.String `tfsdk:"offer_url"`
Users types.List `tfsdk:"users"`
Access types.String `tfsdk:"access"`

// ID required by the testing framework
ID types.String `tfsdk:"id"`
}

// Metadata returns metadata about the access offer resource.
func (a *accessOfferResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_access_offer"
}

// Schema defines the schema for the access offer resource.
func (a *accessOfferResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "A resource that represent a Juju Access Offer.",
Attributes: map[string]schema.Attribute{
"access": schema.StringAttribute{
Description: "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",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.OneOf("admin", "read", "consume"),
},
},
"users": schema.SetAttribute{
Description: "List of users to grant access.",
Optional: true,
ElementType: types.StringType,
Validators: []validator.Set{
setvalidator.ValueStringsAre(ValidatorMatchString(names.IsValidUser, "user must be a valid Juju username")),
},
},
// ID required for imports
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"offer_url": schema.StringAttribute{
Description: "The url of the offer for access management. If this is changed the resource will be deleted and a new resource will be created.",
Required: true,
Validators: []validator.String{
ValidatorMatchString(func(s string) bool {
_, err := crossmodel.ParseOfferURL(s)
return err == nil
}, "offer_url must be a valid offer string."),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
}
}

func (a *accessOfferResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Check first if the client is configured
if a.client == nil {
addClientNotConfiguredError(&resp.Diagnostics, "access offer", "create")
return
}
var plan accessOfferResourceOffer

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

// Get the users
var users []string
resp.Diagnostics.Append(plan.Users.ElementsAs(ctx, &users, false)...)
if resp.Diagnostics.HasError() {
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
}
a.trace(fmt.Sprintf("read offer %q at %q", response.Name, response.OfferURL))

accessStr := plan.Access.ValueString()
// Call Offers.GrantOffer
for _, user := range users {
err := a.client.Offers.GrantOffer(juju.GrantOfferInput{
User: user,
Access: accessStr,
OfferURL: offerURLStr,
})
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(newAccessOfferIDFrom(offerURLStr, accessStr, users))

// Set the plan onto the Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (a *accessOfferResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Read
}

func (a *accessOfferResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// Update
}

func (a *accessOfferResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// Delete
}

func (a *accessOfferResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Configure
}

// ConfigValidators sets validators for the resource.
func (r *accessOfferResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator {
// ConfigValidators
return nil
}

func (a *accessOfferResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
IDstr := req.ID
if len(strings.Split(IDstr, ":")) != 3 {
resp.Diagnostics.AddError(
"ImportState Failure",
fmt.Sprintf("Malformed AccessOffer ID %q, "+
"please use format '<offer URL>:<access>:<user1,user1>'", IDstr),
)
return
}
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

func (a *accessOfferResource) trace(msg string, additionalFields ...map[string]interface{}) {
if a.subCtx == nil {
return
}

tflog.SubsystemTrace(a.subCtx, LogResourceAccessOffer, msg, additionalFields...)
}

func newAccessOfferIDFrom(offerURLStr string, accessStr string, users []string) string {
return fmt.Sprintf("%s:%s:%s", offerURLStr, accessStr, strings.Join(users, ","))
}

0 comments on commit 0417e68

Please sign in to comment.