Skip to content

Commit

Permalink
Merge pull request #502 from hmlanigan/use-juju-3-5-1
Browse files Browse the repository at this point in the history
Use juju 3 5 1
  • Loading branch information
hmlanigan authored Jun 14, 2024
2 parents 386a877 + 1e480c7 commit d4909b8
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 87 deletions.
11 changes: 5 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ go 1.21
require (
github.com/bflad/tfproviderlint v0.30.0
github.com/hashicorp/terraform-plugin-docs v0.19.4
// v3.5-beta1
github.com/juju/juju v0.0.0-20240415234708-a7538882134d
// v3.5.1
github.com/juju/juju v0.0.0-20240524040137-95c267441801

)

Expand All @@ -16,8 +16,7 @@ require (
github.com/hashicorp/terraform-plugin-go v0.23.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.8.0
github.com/juju/charm/v11 v11.2.0
github.com/juju/charm/v12 v12.0.1
github.com/juju/charm/v12 v12.0.2
github.com/juju/clock v1.0.3
github.com/juju/cmd/v3 v3.0.14
github.com/juju/collections v1.0.4
Expand Down Expand Up @@ -129,8 +128,8 @@ require (
github.com/juju/lumberjack/v2 v2.0.2 // indirect
github.com/juju/mgo/v3 v3.0.4 // indirect
github.com/juju/mutex/v2 v2.0.0 // indirect
github.com/juju/os/v2 v2.2.3 // indirect
github.com/juju/packaging/v2 v2.0.1 // indirect
github.com/juju/os/v2 v2.2.5 // indirect
github.com/juju/packaging/v3 v3.0.0 // indirect
github.com/juju/persistent-cookiejar v1.0.0 // indirect
github.com/juju/proxy v1.0.0 // indirect
github.com/juju/pubsub/v2 v2.0.0 // indirect
Expand Down
18 changes: 8 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -324,10 +324,8 @@ github.com/juju/ansiterm v1.0.0 h1:gmMvnZRq7JZJx6jkfSq9/+2LMrVEwGwt7UR6G+lmDEg=
github.com/juju/ansiterm v1.0.0/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384=
github.com/juju/blobstore/v3 v3.0.2 h1:roZ4YBuZYmWId6y/6ZLQSAMmNlHOclHD8PQAMOQer6E=
github.com/juju/blobstore/v3 v3.0.2/go.mod h1:NXEgMhrVH5744/zLfSkzsySlDQUpCgzvcNxjJJhICko=
github.com/juju/charm/v11 v11.2.0 h1:SJ4WI+qvQmMpOE+X6vnZ4NL/BvUJBdjzPN7tscfe6Tk=
github.com/juju/charm/v11 v11.2.0/go.mod h1:Mge5Ko3pPgocmk4v1pQgmBhF8BuBLGTCFu3jq83JvHk=
github.com/juju/charm/v12 v12.0.1 h1:C40Vv7Llfj05y86CnxxbQbSdIXdGzP01rajJR63Ni24=
github.com/juju/charm/v12 v12.0.1/go.mod h1:QRuKxXC5zzfPlAa8ipmxX1tvpbIBliueLVHGbs3T7wU=
github.com/juju/charm/v12 v12.0.2 h1:0UqIAAb4csYF0bGasGcmmIhq/bJUWfGU5+o3XFziWlY=
github.com/juju/charm/v12 v12.0.2/go.mod h1:QRuKxXC5zzfPlAa8ipmxX1tvpbIBliueLVHGbs3T7wU=
github.com/juju/clock v1.0.3 h1:yJHIsWXeU8j3QcBdiess09SzfiXRRrsjKPn2whnMeds=
github.com/juju/clock v1.0.3/go.mod h1:HIBvJ8kiV/n7UHwKuCkdYL4l/MDECztHR2sAvWDxxf0=
github.com/juju/cmd/v3 v3.0.14 h1:KuuamArSH7vQ6SdQKEHYK2scEMkJTEZKLs8abrlW3XE=
Expand Down Expand Up @@ -356,8 +354,8 @@ github.com/juju/idmclient/v2 v2.0.0 h1:PsGa092JGy6iFNHZCcao+bigVsTyz1C+tHNRdYmKv
github.com/juju/idmclient/v2 v2.0.0/go.mod h1:EOiFbPmnkqKvCUS/DHpDRWhL7eKF0AJaTvMjIYlIUak=
github.com/juju/jsonschema v1.0.0 h1:2ScR9hhVdHxft+Te3fnclVx61MmlikHNEOirTGi+hV4=
github.com/juju/jsonschema v1.0.0/go.mod h1:SlFW+jFtpWX0P4Tb+zTTPR4ufttLrnJIdQPePxVEfkM=
github.com/juju/juju v0.0.0-20240415234708-a7538882134d h1:yK8L5+7fJvzsNkjUkkEps4dTGPa1RUr5rnumV4rKXIc=
github.com/juju/juju v0.0.0-20240415234708-a7538882134d/go.mod h1:8W1iXQ/tGftslRzNF/1U8mVZFjtqc0zZKygwuIcmbPI=
github.com/juju/juju v0.0.0-20240524040137-95c267441801 h1:5aNKt2VkNKsS2JI2QcCjNEvOVfMkNNSYaHX6z5odB30=
github.com/juju/juju v0.0.0-20240524040137-95c267441801/go.mod h1:dxhhMNnbWSXUB7EdQEsaxLhgwKQXo2Ctl4z4AZwWL88=
github.com/juju/loggo v1.0.0 h1:Y6ZMQOGR9Aj3BGkiWx7HBbIx6zNwNkxhVNOHU2i1bl0=
github.com/juju/loggo v1.0.0/go.mod h1:NIXFioti1SmKAlKNuUwbMenNdef59IF52+ZzuOmHYkg=
github.com/juju/lru v1.0.0 h1:FP8mBNF3jBnKwGO5PtsR+8iIegx8DREfhRhpcGpYcn4=
Expand All @@ -374,10 +372,10 @@ github.com/juju/names/v5 v5.0.0 h1:3IkRTUaniNXsgjy4lNqbJx7dVdsONlzuH6YMYT7uXss=
github.com/juju/names/v5 v5.0.0/go.mod h1:PkvHbErUTniKvLu1ejJ5m/AbXOW55MFn1jsGVEbVXk8=
github.com/juju/naturalsort v1.0.0 h1:kGmUUy3h8mJ5/SJYaqKOBR3f3owEd5R52Lh+Tjg/dNM=
github.com/juju/naturalsort v1.0.0/go.mod h1:Zqa/vGkXr78k47zM6tFmU9phhxKz/PIdqBzpLhJ86zc=
github.com/juju/os/v2 v2.2.3 h1:5SnGWfzFTXcFwu/sd9qEEf/No3UZxivOjJuWmsHI4N4=
github.com/juju/os/v2 v2.2.3/go.mod h1:xGfP9I+Xb/A03NcGBsoJgwr084hPckkQHecaHuV3wBQ=
github.com/juju/packaging/v2 v2.0.1 h1:KeTfqx3Z0c6RcM053GJH7mplroXoRSuh/dK5vqDQLn8=
github.com/juju/packaging/v2 v2.0.1/go.mod h1:JC+FIRTJXGLt9wA+iP3ltkzv+aWVMMojB/R47uIAK0Y=
github.com/juju/os/v2 v2.2.5 h1:Ayw9aC7axKtGgzy3dFRKx84FxasfISMege0iYDsH6io=
github.com/juju/os/v2 v2.2.5/go.mod h1:igGQLjgRSwUery5ZhV/1pZjZkMwnfkAwWCwh5ZfIg+c=
github.com/juju/packaging/v3 v3.0.0 h1:ZzuHhR8Ql9z2oeQ0m73x6k58PW65Cgk5wR9Yc1exoOI=
github.com/juju/packaging/v3 v3.0.0/go.mod h1:WXh/SXqh1du8SFzwb1KC+yZuV4Qc4alWP3MEPqFX9Lw=
github.com/juju/persistent-cookiejar v1.0.0 h1:Ag7+QLzqC2m+OYXy2QQnRjb3gTkEBSZagZ6QozwT3EQ=
github.com/juju/persistent-cookiejar v1.0.0/go.mod h1:zrbmo4nBKaiP/Ez3F67ewkMbzGYfXyMvRtbOfuAwG0w=
github.com/juju/proxy v1.0.0 h1:5XMp0opQJx8K/js3RFG/2EAk+cvKba/zwFJwd5f0AW0=
Expand Down
9 changes: 2 additions & 7 deletions internal/juju/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,11 @@ var singleQuery sync.Once

// GetLocalControllerConfig runs the locally installed juju command,
// if available, to get the current controller configuration.
func GetLocalControllerConfig() (map[string]string, error) {
func GetLocalControllerConfig() (map[string]string, bool) {
// populate the controller controllerConfig information only once
singleQuery.Do(populateControllerConfig)

// if empty something went wrong
if localProviderConfig == nil {
return nil, errors.New("the Juju CLI could not be accessed")
}

return localProviderConfig, nil
return localProviderConfig, localProviderConfig == nil
}

// populateControllerConfig executes the local juju CLI command
Expand Down
205 changes: 143 additions & 62 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource"
"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/terraform-provider-juju/internal/juju"
)
Expand All @@ -39,37 +40,50 @@ const (
JujuClientID = "client_id"
JujuClientSecret = "client_secret"
JujuCACert = "ca_certificate"

TwoSourcesAuthWarning = "Two sources of identity for controller login"
)

// populateJujuProviderModelLive gets the controller config,
// first from environment variables, then from a live juju
// controller as a fallback.
func populateJujuProviderModelLive() (jujuProviderModel, error) {
// jujuProviderModelEnvVar gets the controller config,
// from environment variables.
func jujuProviderModelEnvVar() jujuProviderModel {
return jujuProviderModel{
ControllerAddrs: getEnvVar(JujuControllerEnvKey),
CACert: getEnvVar(JujuCACertEnvKey),
ClientID: getEnvVar(JujuClientIDEnvKey),
ClientSecret: getEnvVar(JujuClientSecretEnvKey),
UserName: getEnvVar(JujuUsernameEnvKey),
Password: getEnvVar(JujuPasswordEnvKey),
}
}

func jujuProviderModelLiveDiscovery() (jujuProviderModel, bool) {
data := jujuProviderModel{}
controllerConfig, cliNotExist := juju.GetLocalControllerConfig()
data.ControllerAddrs = types.StringValue(getField(JujuControllerEnvKey, controllerConfig))
data.UserName = types.StringValue(getField(JujuUsernameEnvKey, controllerConfig))
data.Password = types.StringValue(getField(JujuPasswordEnvKey, controllerConfig))
data.ClientID = types.StringValue(getField(JujuClientIDEnvKey, controllerConfig))
data.ClientSecret = types.StringValue(getField(JujuClientSecretEnvKey, controllerConfig))
data.CACert = types.StringValue(getField(JujuCACertEnvKey, controllerConfig))
// Only error if a valid controller config could not be fetched
// from the environment variables.
if cliNotExist != nil && !data.valid() {
return data, errors.New("unable to acquire Juju controller config: no working Juju client, and environment variables are not fully set")
}

return data, nil

if ctrlAddrs, ok := controllerConfig[JujuControllerEnvKey]; ok && ctrlAddrs != "" {
data.ControllerAddrs = types.StringValue(ctrlAddrs)
}
if caCert, ok := controllerConfig[JujuCACertEnvKey]; ok && caCert != "" {
data.CACert = types.StringValue(caCert)
}
if user, ok := controllerConfig[JujuUsernameEnvKey]; ok && user != "" {
data.UserName = types.StringValue(user)
}
if password, ok := controllerConfig[JujuPasswordEnvKey]; ok && password != "" {
data.Password = types.StringValue(password)
}
return data, cliNotExist
}

func getField(field string, config map[string]string) string {
// get the value from the environment variable
controller := os.Getenv(field)
if controller == "" {
func getEnvVar(field string) types.String {
value := types.StringNull()
envVar := os.Getenv(field)
if envVar != "" {
// fall back to the live juju data
controller = config[field]
value = types.StringValue(envVar)
}
return controller
return value
}

// Ensure jujuProvider satisfies various provider interfaces.
Expand All @@ -93,13 +107,62 @@ type jujuProviderModel struct {
ClientSecret types.String `tfsdk:"client_secret"`
}

func (j jujuProviderModel) loginViaUsername() bool {
return j.UserName.ValueString() != "" && j.Password.ValueString() != ""
}

func (j jujuProviderModel) loginViaClientCredentials() bool {
return j.ClientID.ValueString() != "" && j.ClientSecret.ValueString() != ""
}

func (j jujuProviderModel) valid() bool {
validUserPass := j.UserName.ValueString() != "" && j.Password.ValueString() != ""
validClientCredentials := j.ClientID.ValueString() != "" && j.ClientSecret.ValueString() != ""
validUserPass := j.loginViaUsername()
validClientCredentials := j.loginViaClientCredentials()

return j.ControllerAddrs.ValueString() != "" &&
j.CACert.ValueString() != "" &&
(validUserPass || validClientCredentials)
(validUserPass || validClientCredentials) &&
!(validUserPass && validClientCredentials)
}

// merge 2 providerModels together. The receiver data takes
// precedence. If the model is valid after the client ID and
// client secret are set, return. They take precedence over
// username and password. The two combinations are also
// mutually exclusive. Diagnostic warning are returned if
// the new data contains a username but the current data has
// client ID.
func (j jujuProviderModel) merge(in jujuProviderModel, from string) (jujuProviderModel, diag.Diagnostics) {
diags := diag.Diagnostics{}
mergedModel := j
if mergedModel.ControllerAddrs.ValueString() == "" {
mergedModel.ControllerAddrs = in.ControllerAddrs
}
if mergedModel.CACert.ValueString() == "" {
mergedModel.CACert = in.CACert
}
if mergedModel.ClientID.ValueString() == "" {
mergedModel.ClientID = in.ClientID
}
if mergedModel.ClientSecret.ValueString() == "" {
mergedModel.ClientSecret = in.ClientSecret
}
if mergedModel.valid() {
if in.UserName.ValueString() != "" {
diags.AddWarning(TwoSourcesAuthWarning,
fmt.Sprintf("Ignoring Username value. Username provided via %s,"+
"however Client ID already available. Only one login type is possible.", from))
}

return mergedModel, diags
}
if mergedModel.UserName.ValueString() == "" {
mergedModel.UserName = in.UserName
}
if mergedModel.Password.ValueString() == "" {
mergedModel.Password = in.Password
}
return mergedModel, diags
}

// Metadata returns the metadata for the provider, such as
Expand Down Expand Up @@ -215,65 +278,83 @@ func (p *jujuProvider) Configure(ctx context.Context, req provider.ConfigureRequ
// the plan being used, then fall back to the JUJU_ environment variables,
// lastly check to see if an active juju can supply the data.
func getJujuProviderModel(ctx context.Context, req provider.ConfigureRequest) (jujuProviderModel, diag.Diagnostics) {
var data jujuProviderModel
var planData jujuProviderModel
var diags diag.Diagnostics

// Read Terraform configuration data into the data model
diags.Append(req.Config.Get(ctx, &data)...)
// Read Terraform configuration data into the juju provider model.
diags.Append(req.Config.Get(ctx, &planData)...)
if diags.HasError() {
return data, diags
return planData, diags
}
if data.valid() {
if planData.valid() {
// The plan contained full controller config,
// no need to continue
return data, diags
return planData, diags
}
// If validation failed because we have both username/password
// and client ID/secret combinations in the plan. Exit now.
if planData.UserName.ValueString() != "" && planData.ClientID.ValueString() != "" {
diags.AddError("Only username and password OR client id and "+
"client secret may be used.",
"Both username and client id are set in the plan. Please remove "+
"one of the login methods and try again.")
return planData, diags
}

// Not all controller config contained in the plan, attempt
// to find it.
liveData, err := populateJujuProviderModelLive()
if err != nil {
diags.AddError("Unable to get live controller data", err.Error())
return data, diags
}
if data.ControllerAddrs.ValueString() == "" {
data.ControllerAddrs = liveData.ControllerAddrs
}
if data.UserName.ValueString() == "" {
data.UserName = liveData.UserName
// to find it via the optional environment variables.
envVarData := jujuProviderModelEnvVar()
planEnvVarDataModel, planEnvVarDataDiags := planData.merge(envVarData, "environment variables")
diags.Append(planEnvVarDataDiags...)
if planEnvVarDataModel.valid() {
return planEnvVarDataModel, diags
}
if data.Password.ValueString() == "" {
data.Password = liveData.Password
}
if data.ClientID.ValueString() == "" {
data.ClientID = liveData.ClientID
if planEnvVarDataModel.loginViaClientCredentials() {
if planEnvVarDataModel.ControllerAddrs.ValueString() == "" {
diags.AddError("Controller address required", "The provider must know which juju controller to use. Please add to plan or use the JUJU_CONTROLLER_ADDRESSES environment variable.")
}
if planEnvVarDataModel.CACert.ValueString() == "" {
diags.AddError("Controller CACert required", "For the Juju certificate authority to be trusted by your system. Please add to plan or use the JUJU_CA_CERT environment variable.")
}
}
if data.ClientSecret.ValueString() == "" {
data.ClientSecret = liveData.ClientSecret
if diags.HasError() {
return planEnvVarDataModel, diags
}
if data.CACert.ValueString() == "" {
data.CACert = liveData.CACert

// Not all controller config contained in the plan, attempt
// to find it via live discovery.
liveData, cliAlive := jujuProviderModelLiveDiscovery()
errMsgDataModel := planEnvVarDataModel
if cliAlive {
livePlanEnvVarDataModel, livePlanEnvVarDataDiags := planEnvVarDataModel.merge(liveData, "live discovery")
diags.Append(livePlanEnvVarDataDiags...)
if livePlanEnvVarDataModel.valid() {
return livePlanEnvVarDataModel, diags
}
errMsgDataModel = livePlanEnvVarDataModel
} else {
tflog.Debug(ctx, "Live discovery of juju controller failed. The Juju CLI could not be accessed.")
}

// Validate controller config and return helpful error messages.
validUserPass := (data.UserName.ValueString() != "" && data.Password.ValueString() != "")
validClientCredentials := (data.ClientID.ValueString() != "" && data.ClientSecret.ValueString() != "")
if !validUserPass && !validClientCredentials {
if !errMsgDataModel.loginViaUsername() && !errMsgDataModel.loginViaClientCredentials() {
diags.AddError(
"Username and password or client id and client secret must be set",
"Currently the provider can authenticate using username and password or client id and client secret, otherwise it will panic",
"Currently the provider can authenticate using username and password or client id and client secret, otherwise it will panic.",
)
}

if data.ControllerAddrs.ValueString() == "" {
if errMsgDataModel.ControllerAddrs.ValueString() == "" {
diags.AddError("Controller address required", "The provider must know which juju controller to use.")
}

if data.CACert.ValueString() == "" {
diags.AddError("Controller CACert", "Required for the Juju certificate authority to be trusted by your system")
if errMsgDataModel.CACert.ValueString() == "" {
diags.AddError("Controller CACert required", "For the Juju certificate authority to be trusted by your system.")
}
if diags.HasError() {
tflog.Debug(ctx, "Current login values.",
map[string]interface{}{"jujuProviderModel": planData})
}

return data, diags
return errMsgDataModel, diags
}

// Resources returns a slice of functions to instantiate each Resource
Expand Down
7 changes: 6 additions & 1 deletion internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ func TestProviderConfigurex509InvalidFromEnv(t *testing.T) {
}

func testAccPreCheck(t *testing.T) {
if TestClient != nil {
return
}
if v := os.Getenv(JujuUsernameEnvKey); v == "" {
t.Fatalf("%s must be set for acceptance tests", JujuUsernameEnvKey)
}
Expand All @@ -170,7 +173,9 @@ func testAccPreCheck(t *testing.T) {
}
confResp := configureProvider(t, Provider)
assert.Equal(t, confResp.Diagnostics.HasError(), false)
TestClient = confResp.ResourceData.(*juju.Client)
testClient, ok := confResp.ResourceData.(*juju.Client)
assert.Truef(t, ok, "ResourceData, not of type juju client")
TestClient = testClient
}

func configureProvider(t *testing.T, p provider.Provider) provider.ConfigureResponse {
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/validator_channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"context"

"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/juju/charm/v11"
"github.com/juju/charm/v12"
)

type StringIsChannelValidator struct{}
Expand Down

0 comments on commit d4909b8

Please sign in to comment.