diff --git a/internal/juju/client.go b/internal/juju/client.go index 807559eb..41d2d19a 100644 --- a/internal/juju/client.go +++ b/internal/juju/client.go @@ -49,6 +49,14 @@ type Client struct { SSHKeys sshKeysClient Users usersClient Secrets secretsClient + + isJAAS func() bool +} + +// IsJAAS returns a boolean to indicate whether the controller configured is a JAAS controller. +// JAAS controllers offer additional functionality for permission management. +func (c Client) IsJAAS() bool { + return c.isJAAS() } type jujuModel struct { @@ -82,6 +90,12 @@ func NewClient(ctx context.Context, config ControllerConfiguration) (*Client, er modelUUIDcache: make(map[string]jujuModel), subCtx: tflog.NewSubsystem(ctx, LogJujuClient), } + // Client ID and secret are only set when connecting to JAAS. Use this as a fallback + // value if connecting to the controller fails. + defaultJAASCheck := false + if config.ClientID != "" && config.ClientSecret != "" { + defaultJAASCheck = true + } return &Client{ Applications: *newApplicationClient(sc), @@ -93,9 +107,33 @@ func NewClient(ctx context.Context, config ControllerConfiguration) (*Client, er SSHKeys: *newSSHKeysClient(sc), Users: *newUsersClient(sc), Secrets: *newSecretsClient(sc), + isJAAS: func() bool { return sc.IsJAAS(defaultJAASCheck) }, }, nil } +var checkJAASOnce sync.Once +var isJAAS bool + +// IsJAAS checks if the controller is a JAAS controller. +// It does this by checking whether it offers the "JIMM" facade which +// will only ever be offered by JAAS. The method accepts a default value +// and doesn't return an error because callers are not expected to fail if +// they can't determine whether they are connecting to JAAS. +// +// IsJAAS uses a synchronisation object to only perform the check once and return the same result. +func (sc *sharedClient) IsJAAS(defaultVal bool) bool { + checkJAASOnce.Do(func() { + conn, err := sc.GetConnection(nil) + if err != nil { + isJAAS = defaultVal + return + } + defer conn.Close() + isJAAS = conn.BestFacadeVersion("JIMM") != 0 + }) + return isJAAS +} + // GetConnection returns a juju connection for use creating juju // api clients given the provided model name. func (sc *sharedClient) GetConnection(modelName *string) (api.Connection, error) { diff --git a/internal/provider/validator_avoid_jaas.go b/internal/provider/validator_avoid_jaas.go new file mode 100644 index 00000000..2e18e167 --- /dev/null +++ b/internal/provider/validator_avoid_jaas.go @@ -0,0 +1,64 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the Apache License, Version 2.0, see LICENCE file for details. + +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/juju/terraform-provider-juju/internal/juju" +) + +var _ datasource.ConfigValidator = &AvoidJAASValidator{} +var _ resource.ConfigValidator = &AvoidJAASValidator{} + +// AvoidJAASValidator enforces that the resource is not used with JAAS. +// Useful to direct users to more capable resources. +type AvoidJAASValidator struct { + Client *juju.Client + PreferredObject string +} + +// Description returns a plain text description of the validator's behavior, suitable for a practitioner to understand its impact. +func (v AvoidJAASValidator) Description(ctx context.Context) string { + return v.MarkdownDescription(ctx) +} + +// MarkdownDescription returns a markdown formatted description of the validator's behavior, suitable for a practitioner to understand its impact. +func (v AvoidJAASValidator) MarkdownDescription(_ context.Context) string { + return "Enforces that this resource should not be used with JAAS" +} + +// ValidateResource performs the validation on the data source. +func (v AvoidJAASValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) { + resp.Diagnostics = v.validate() +} + +// ValidateResource performs the validation on the resource. +func (v AvoidJAASValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + resp.Diagnostics = v.validate() +} + +// validate runs the main validation logic of the validator, reading configuration data out of `config` and returning with diagnostics. +func (v AvoidJAASValidator) validate() diag.Diagnostics { + var diags diag.Diagnostics + + // Return without error if a nil client is detected. + // This is possible since validation is called at various points throughout resource creation. + if v.Client != nil && v.Client.IsJAAS() { + hint := "" + if v.PreferredObject != "" { + hint = "Try the " + v.PreferredObject + " resource instead." + } + diags.AddError("Invalid use of resource with JAAS.", + "This resource is not supported with JAAS. "+ + hint+ + "JAAS offers additional enterprise features through the use of dedicated resources. "+ + "See https://jaas.ai/ for more details.") + } + return diags +} diff --git a/internal/provider/validator_require_jaas.go b/internal/provider/validator_require_jaas.go new file mode 100644 index 00000000..7497dbbe --- /dev/null +++ b/internal/provider/validator_require_jaas.go @@ -0,0 +1,56 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the Apache License, Version 2.0, see LICENCE file for details. + +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/juju/terraform-provider-juju/internal/juju" +) + +var _ datasource.ConfigValidator = &RequiresJAASValidator{} +var _ resource.ConfigValidator = &RequiresJAASValidator{} + +// RequiresJAASValidator enforces that the resource can only be used with JAAS. +type RequiresJAASValidator struct { + Client *juju.Client +} + +// Description returns a plain text description of the validator's behavior, suitable for a practitioner to understand its impact. +func (v RequiresJAASValidator) Description(ctx context.Context) string { + return v.MarkdownDescription(ctx) +} + +// MarkdownDescription returns a markdown formatted description of the validator's behavior, suitable for a practitioner to understand its impact. +func (v RequiresJAASValidator) MarkdownDescription(_ context.Context) string { + return "Enforces that this resource can only be used with JAAS" +} + +// ValidateResource performs the validation on the data source. +func (v RequiresJAASValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) { + resp.Diagnostics = v.validate() +} + +// ValidateResource performs the validation on the resource. +func (v RequiresJAASValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + resp.Diagnostics = v.validate() +} + +// validate runs the main validation logic of the validator, reading configuration data out of `config` and returning with diagnostics. +func (v RequiresJAASValidator) validate() diag.Diagnostics { + var diags diag.Diagnostics + + // Return without error if a nil client is detected. + // This is possible since validation is called at various points throughout resource creation. + if v.Client != nil && !v.Client.IsJAAS() { + diags.AddError("Attempted use of resource without JAAS.", + "This resource can only be used with JAAS, which offers additional enterprise features - see https://jaas.ai/ for more details.") + } + + return diags +}