Skip to content

chore: use the juju client for model related methods#1633

Merged
kian99 merged 13 commits intocanonical:v3from
kian99:replace-model-manager-client
Feb 16, 2026
Merged

chore: use the juju client for model related methods#1633
kian99 merged 13 commits intocanonical:v3from
kian99:replace-model-manager-client

Conversation

@kian99
Copy link
Contributor

@kian99 kian99 commented Jun 26, 2025

Description

This change started off as a simple one, a continuation of #1611 where we slowly move our jujuclient package to use Juju's Go client so that we don't need to call facades directly and we get nicer types out of the client.
The downside of this change is that I had to replace all usages of jujuparams.* with base.* where were were using the RPC object in many places (i.e. an object with JSON tags used for marshalling/unmarshalling over the wire). We are now using a different but similar type that simplifies a few rough edges.

There was also a somewhat unrelated but necessary change in the internal/rpc package where I had previously introduced a dependency on internal/jimm/ that was incorrect and caused cyclic dependencies during this PR. That's now been addressed.

Finally, you'll see new functions in internal/jujuapi/types.go that convert from the base.* types to the API type. I haven't written any tests for these as our integration tests cover this behavior, but as discussed before, if we have any objections to this approach we can always revisit it. Also, confusingly, the types in Juju's base package are not the same types used by Juju's apiserver, this might be worth looking at further.

Fixes JUJU-9132

Engineering checklist

  • Documentation updated
  • Covered by unit tests
  • Covered by integration tests

@kian99 kian99 force-pushed the replace-model-manager-client branch from 643ceb3 to fca0e39 Compare June 27, 2025 07:39
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This file was renamed and modified, see types.go for what existed previously.

@kian99 kian99 force-pushed the replace-model-manager-client branch 2 times, most recently from b3b7330 to f7aa97d Compare July 8, 2025 07:51
@kian99 kian99 marked this pull request as ready for review July 8, 2025 09:13
@kian99 kian99 requested a review from a team as a code owner July 8, 2025 09:13
Copy link
Collaborator

@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.

1st pass review with a few comments

// API Error types.
if ec, ok := e.Err.(interface{ ErrorCode() string }); ok {
e.Code = Code(ec.ErrorCode())
var errCode interface{ ErrorCode() string }
Copy link
Collaborator

Choose a reason for hiding this comment

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

why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The error that comes out of the Juju client is wrapped internally due to a errors.Trace(err) call where that original err has an error code. Then, when do errors.E(err) and try to extract an error code, we get nothing because the error with the code is 2 levels deep. So we can try to unwrap the error until we find an error that implement ErrorCode() or we reach the end of the list of wrapped errors.


// ChangeModelCredential replaces cloud credential for a given model with the provided one.
ChangeModelCredential(context.Context, names.ModelTag, names.CloudCredentialTag) error
ChangeModelCredential(model names.ModelTag, credential names.CloudCredentialTag) error
Copy link
Collaborator

Choose a reason for hiding this comment

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

i'm not fond of named arguments in interface definitions.. feels like the godoc above does not fully explain what the method is expected to do

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will remove the named arguments and update the godoc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the named arguments where unneccessary (some were useful and I've left them).
Left the docstring since I didn't touch that in this PR.

"gopkg.in/yaml.v3"
)

func cloudFromParams(cloudName string, p jujuparams.Cloud) cloud.Cloud {
Copy link
Collaborator

Choose a reason for hiding this comment

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

it would be nice to have some consistency in naming.. XYToZW where X is a package, Y a type, Z a package and W a type.. e.g. paramsCloudToCloudCloud wdyt?

Copy link
Collaborator

Choose a reason for hiding this comment

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

or have a type that embeds cloud.Cloud and has a FromParamsCloud method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I hear you, I'll take a look at this

Copy link
Contributor Author

@kian99 kian99 Jul 11, 2025

Choose a reason for hiding this comment

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

Here are some thoughts, we either need to convert from an RPC struct to some type that we use internally or we need to convert to whatever type we use internally to an RPC type - speaking specifically about the jujuapi package.

For converting InternalType -> RPC, we can look at internal/jujuapi/cloud.go where we've got the API handler CloudInfo() which receives a dbmodel.Cloud and runs cloud.ToJujuCloudInfo() on the dbmodel object converting from the dbmodel type to the RPC type. I don't think that is great because it imports the RPC types into the dbmodel package for the purposes of putting the conversion logic in there.

Then for RPC -> InternalType we can look at AddCloud in the same file, which calls cloudFromParams to convert a jujuparams.Cloud to github.com/juju/juju/cloud.Cloud.

So ultimately, my idea is that it would be nice if the naming convention for the mapping functions easily indicated we are pushing data down vs surfacing it.
I.e. if we are converting to an internal type we could name the function toX where X is some type. And if we are converting to an RPC type we could name the function fromX.
The actual words don't matter to me but at least names that reflect which way the data is going. Another options might be rpcToX and xFromRPC.

Copy link
Collaborator

Choose a reason for hiding this comment

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

exactly.. some naming convention where i can tell which way we're converting just by looking at the method name

return d.createLoginRequest1(ctx, ctl.ResourceTag(), user.ResourceTag(), p)
}

// toModelAccessString maps relation to a model access string.
Copy link
Collaborator

Choose a reason for hiding this comment

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

hhmmm.. i vaguely remember we have a place for these conversion methods between openfga relations to juju access strings, don't we?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah inside internal/jimm/permissions , and you'll see that's where it was used from initially, but I don't think the jujuclient should be reaching in there for things so I've copied it here. Especially because internal/jimm/juju imports this package, so you can get into a dependency cycle very easily (can't remember if this import did cause a cycle but better to avoid importing anything internal/jimm from internal/jujuclient).

The issue makes me think the conversion of OpenFGA types to Juju strings should happen in some kind of adapter package that like internal/openfgaToJuju or at least something along those lines is worth thinking about.

Copy link
Contributor

Choose a reason for hiding this comment

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

what do you think about a test to make sure these checks can't drift.

Copy link
Contributor Author

@kian99 kian99 Feb 13, 2026

Choose a reason for hiding this comment

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

I've tried to add a test now but the package does blackbox testing so I can't without more changes,

Copy link
Collaborator

Choose a reason for hiding this comment

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

perhaps the openfga.Relation type could implement ModelAccessString or ControllerAccessString as needed to convert them to juju access strings..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that could work. I'll keep it in mind but won't add it to this PR.

// types preferably of the form base.ModelInfo. Until either that inconsitency
// is fixed or we export the convertParamsModelInfo function, we copy it here
// here have better consistency in our domain layer.
func convertParamsModelInfo(modelInfo params.ModelInfo) (base.ModelInfo, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

again a comment about naming consistency, or having a type that wraps base.ModelInfo and has a method for conversion from params.ModelInfo and a method that returns the wrapped base.ModelInfo

Copy link
Contributor Author

@kian99 kian99 Jul 11, 2025

Choose a reason for hiding this comment

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

This function and name was copied from Juju so maybe this one should stay the same?

@kian99 kian99 force-pushed the replace-model-manager-client branch 3 times, most recently from ec13770 to 82493fa Compare July 23, 2025 09:16
@luci1900
Copy link
Contributor

So this is a change to use the existing machinery that juju clients rely on for resolving RPC methods to specific ones in case of multiple versions or similar?

Looks reasonable overall, but I don't know if I understand it enough to approve.

@kian99 kian99 force-pushed the replace-model-manager-client branch from 477808e to dbc97a1 Compare February 9, 2026 15:38
// a params.ModelInfo type is returned. We want consistent types preferably of
// the form base.ModelInfo. Until the client returns a consistent set of types
// we copy it here here to have better consistency in our domain layer.
func convertParamsModelInfo(modelInfo params.ModelInfo) (ModelInfo, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

An important note here. The params.ModelInfo holds a few more fields than base.ModelInfo. The difference is below.
The following fields appear in params.ModelInfo but don't appear in base.ModelInfo (conversely there is nothing missing):

	DefaultBase        string `json:"default-base,omitempty"`

	// CloudCredentialValidity contains if model credential is valid, if known.
	CloudCredentialValidity *bool `json:"cloud-credential-validity,omitempty"`

	// SecretBackends contains information about the secret backends
	// currently in use by the model.
	SecretBackends []SecretBackendResult `json:"secret-backends"`

	// Migration contains information about the latest failed or
	// currently-running migration. It'll be nil if there isn't one.
	Migration *ModelMigrationStatus `json:"migration,omitempty"`

	// SLA contains the information about the SLA for the model, if set.
	SLA *ModelSLAInfo `json:"sla"`

	// SupportedFeatures provides information about the set of features
	// supported by this model. The feature set contains both Juju-specific
	// entries (e.g. juju version) and other features that depend on the
	// substrate the model is deployed to.
	SupportedFeatures []SupportedFeature `json:"supported-features,omitempty"`

Out of these fields I believe SLA is long deprecated and unused, SupportedFeatures I'm unsure on its usage, Migration is necessary and we've added it to our wrapper, SecretBackends seems useful but not currently parsed and DefaultBase is also not parsed. So I've gone and checked the output of juju show-model after this change via JIMM and via the backing Juju controller. Below is the output from JIMM with the fields from Juju added and marked. Note that the output doesn't include ommitted fields like migration status which is only present in certain circumstances.

foo:
  name: jimm-test@canonical.com/foo
  short-name: foo
  model-uuid: fe9cecbf-676e-4592-8c0c-effcaf981400
  model-type: iaas
  controller-uuid: 3217dbc9-8ea9-4381-9e97-01eab0b3f6bb
  controller-name: jimm-dev
  is-controller: false
  owner: jimm-test@canonical.com
  cloud: localhost
  region: localhost
  type: lxd
  life: alive
  status:
    current: available
    since: 7 seconds ago
  users:
    jimm-test@canonical.com:\
      display-name: jimm-test ---- missing
      access: admin
      last-connection: never connected
  sla: unsupported ---- missing
  agent-version: 3.6.12
  credential:
    name: localhost
    owner: jimm-test@canonical.com
    cloud: localhost
    validity-check: invalid ---- backing Juju controller says valid
  supported-features: ---- this entire block is missing
  - name: juju
    description: the version of Juju used by the model
    version: 3.6.12

What I'll do is expand our ModelInfo struct to include the missing fields so that we can always return them and add a test that verifies the difference between juju show-model against JIMM vs Juju.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm thinking about doing the above in a follow-up PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alternatively, maybe we should just pass around the rpc type when we call this method instead of the base type.

Copy link
Contributor

Choose a reason for hiding this comment

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

It doesn't seem like it's a problem to pass the RPC type.

Copy link
Contributor

Choose a reason for hiding this comment

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

mmm, that's not good. Why would this type be less than the rpc type? Should we fix it in juju?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To summarise the work done here,

  • I tried changing to use the RPC type and that proved challenging because there are functions used on code-paths that are common to both CreateModel and ModelInfo calls. If the one returns a base.ModelInfo (returned by CreateModel) and the other returns a rpc.ModelInfo, we create a lot more friction in the code for nothing.
  • I discussed making the change in Juju and there is an appetite for it, but I've decided to forego making the change in 3.6 as we'll move to 4.0 soon and it would be a breaking change in 3.6.
  • I've added the missing types to our jujuclient package and update our mapping logic in jujuapi. Now the response from JIMM and Juju are nearly identical.

@kian99 kian99 force-pushed the replace-model-manager-client branch from 573ef33 to 8e462f2 Compare February 10, 2026 12:43
}}, nil
}
},
GrantJIMMModelAdmin_: func(_ context.Context, _ names.ModelTag) error {
Copy link
Contributor

@SimoneDutto SimoneDutto Feb 10, 2026

Choose a reason for hiding this comment

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

leave the ctx even if it's useless rn because once we migrate to juju 4 client, all of the methods will require ctx

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking about that, I don't mind adding it back afterwards, wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

We'll definitely need it, I'd imagine it's simpler to not remove it now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah let me add them back

@kian99 kian99 force-pushed the replace-model-manager-client branch from b9c5607 to 648d8c3 Compare February 10, 2026 14:24
@kian99 kian99 force-pushed the replace-model-manager-client branch from 2704a9d to c40fa4c Compare February 11, 2026 07:42
Copy link
Contributor

@luci1900 luci1900 left a comment

Choose a reason for hiding this comment

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

Mostly mechanical changes, should be fine.

I do think we need to QA this somewhat more thoroughly than other changes, simply because it touches so many areas.

Copy link
Contributor

@SimoneDutto SimoneDutto left a comment

Choose a reason for hiding this comment

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

Some questions to be sure everything we're doing here is correct

Comment on lines 93 to +183
@@ -131,13 +132,13 @@ type API interface {
ListApplicationOffers(context.Context, []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error)

// ListModelSummaries lists models summaries
ListModelSummaries(context.Context, jujuparams.ModelSummariesRequest) (jujuparams.ModelSummaryResults, error)
ListModelSummaries(context.Context, jujuparams.ModelSummariesRequest) ([]base.UserModelSummary, error)

// ModelInfo fetches a model's ModelInfo.
ModelInfo(context.Context, *jujuparams.ModelInfo) error
ModelInfo(context.Context, names.ModelTag) (jujuclient.ModelInfo, error)

// ModelStatus fetches a model's ModelStatus.
ModelStatus(context.Context, *jujuparams.ModelStatus) error
ModelStatus(context.Context, names.ModelTag) (base.ModelStatus, error)

// ModelSummaryWatcherNext returns the next set of model summaries from
// the watcher.
@@ -179,7 +180,7 @@ type API interface {
UpdateCredential(context.Context, jujuparams.TaggedCredential) ([]jujuparams.UpdateCredentialModelResult, error)

// ValidateModelUpgrade validates that a model can be upgraded.
ValidateModelUpgrade(context.Context, names.ModelTag, bool) error
ValidateModelUpgrade(ctx context.Context, model names.ModelTag, force bool) error
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need the named args?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added them where it was unclear what the bool/string were. A names.ModelTag is clear, but bool, string, bool was very unclear.


// AddModel adds the specified model to JIMM.
func (j *JujuManager) AddModel(ctx context.Context, user *openfga.User, args *ModelCreateArgs) (_ *jujuparams.ModelInfo, err error) {
func (j *JujuManager) AddModel(ctx context.Context, user *openfga.User, args *ModelCreateArgs) (base.ModelInfo, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

why did we have the named return here? I can't see any defer something, but double check please

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Double checked the current code and looks fine without a named return.

// access to the model then the returned error will have the code
// CodeUnauthorized.
func (j *JujuManager) ModelInfo(ctx context.Context, user *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) {
func (j *JujuManager) ModelInfo(ctx context.Context, user *openfga.User, mt names.ModelTag) (jujuclient.ModelInfo, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

what's the reason of using the struct instead of the pointer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe a pointer was used to make the return values "simpler", i.e. return null, but there was no reason for a pointer.

models = append(models, struct {
model *dbmodel.Model
userAccess jujuparams.UserAccessPermission
userAccess string
Copy link
Contributor

Choose a reason for hiding this comment

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

this was an "enum", converting it to a string isn't dangerous for when we do the permission checks?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the struct value being a string is okay because that's just used for returning to the user later, the signature for the ForEachUserModel callback could be a better type but it's also only being used here so I believe it's okay.

func (j *JujuManager) mergeModelInfo(ctx context.Context, user *openfga.User, modelInfo *jujuparams.ModelInfo, jimmModel dbmodel.Model) (*jujuparams.ModelInfo, error) {
modelInfo.CloudCredentialTag = jimmModel.CloudCredential.Tag().String()
func (j *JujuManager) mergeModelInfo(ctx context.Context, user *openfga.User, modelInfo jujuclient.ModelInfo, jimmModel dbmodel.Model) (jujuclient.ModelInfo, error) {
modelInfo.CloudCredential = jimmModel.CloudCredential.ResourceTag().Id()
Copy link
Contributor

Choose a reason for hiding this comment

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

so now this is just the cloud credential name instead of the full tag?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yip, similarly for ownerTag -> Owner and CloudTag -> Cloud

}

// CloudSpec retrieves the cloud spec of the model connected to.
func (c Connection) CloudSpec(ctx context.Context) (cloudspec.CloudSpec, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need this now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It replaces ControllerModelSummary which was being used to get the controller model's cloud and cloud-credential then we'd fetch the cloud-credential. If/when we remove the cloning of controllers we can remove this and simplify.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I left out a bit of info, there was no Juju API equivalent of ControllerModelSummary, but CloudSpec() exists and did what we needed.

return d.createLoginRequest1(ctx, ctl.ResourceTag(), user.ResourceTag(), p)
}

// toModelAccessString maps relation to a model access string.
Copy link
Contributor

Choose a reason for hiding this comment

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

what do you think about a test to make sure these checks can't drift.

err := c.Call(ctx, "ModelManager", 10, "", "ModelStatus", &args, &resp)
// ModelStatus retrieves the status of a model from the controller.
func (c Connection) ModelStatus(ctx context.Context, modelTag names.ModelTag) (base.ModelStatus, error) {
statuses, err := modelmanager.NewClient(&c).ModelStatus(modelTag)
Copy link
Contributor

Choose a reason for hiding this comment

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

here err is the first error in the Result or does it work in the client?
I'm kinda worried because they don't "type" errors when the result is of Result and it can't potentially mess with our NotFound checks.

I think our errors.E was typing the error, so this change you've made could be breaking

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It should all work out once we get to the API layer and call mapError which will inspect the error with ErrorCode until it finds a code.

// a params.ModelInfo type is returned. We want consistent types preferably of
// the form base.ModelInfo. Until the client returns a consistent set of types
// we copy it here here to have better consistency in our domain layer.
func convertParamsModelInfo(modelInfo params.ModelInfo) (ModelInfo, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

mmm, that's not good. Why would this type be less than the rpc type? Should we fix it in juju?

Copy link
Contributor

@SimoneDutto SimoneDutto left a comment

Choose a reason for hiding this comment

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

ty for the effort

return d.createLoginRequest1(ctx, ctl.ResourceTag(), user.ResourceTag(), p)
}

// toModelAccessString maps relation to a model access string.
Copy link
Collaborator

Choose a reason for hiding this comment

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

perhaps the openfga.Relation type could implement ModelAccessString or ControllerAccessString as needed to convert them to juju access strings..

@kian99 kian99 merged commit ed0a281 into canonical:v3 Feb 16, 2026
8 checks passed
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.

5 participants