Skip to content

Restructure Microcluster packages#534

Draft
roosterfish wants to merge 12 commits intocanonical:v3from
roosterfish:move_db_packages
Draft

Restructure Microcluster packages#534
roosterfish wants to merge 12 commits intocanonical:v3from
roosterfish:move_db_packages

Conversation

@roosterfish
Copy link
Contributor

@roosterfish roosterfish commented Nov 12, 2025

This started out small but after some changes I realized we have to properly hide structs behind interfaces to not anymore return datatypes which aren't usable by the user as they are placed inside the internal package.

With this PR I tried to remove all the pain points we have identified over the last years. Furthermore a user of Microcluster can now properly mock all the types to allow testing their DB and API handler logic.

More description and commit messages pending.

@tomponline
Copy link
Member

This started out small but after some changes I realized we have to properly hide structs behind interfaces to not anymore return datatypes which aren't usable by the user as they are placed inside the internal package.

More description and commit messages pending.

I've not looked at the code, but if these structs need to be accessible to the caller why are they being moved to internal and then interfaces being used to solve the problem?

@roosterfish
Copy link
Contributor Author

I've not looked at the code, but if these structs need to be accessible to the caller why are they being moved to internal and then interfaces being used to solve the problem?

One aspect is that we don't have to expose code which is not intended for public use. Another (most important in my opinion) is that you can mock the types for unit testing. Just updated the initial comment.

@tomponline
Copy link
Member

I've not looked at the code, but if these structs need to be accessible to the caller why are they being moved to internal and then interfaces being used to solve the problem?

One aspect is that we don't have to expose code which is not intended for public use. Another (most important in my opinion) is that you can mock the types for unit testing. Just updated the initial comment.

Can you define the types externally to internal and then use them from both the /internal functions and the call sites (like shared.api)?

@roosterfish
Copy link
Contributor Author

Can you define the types externally to internal and then use them from both the /internal functions and the call sites (like shared.api)?

Yes that is the idea. This PR introduces a microcluster/types package which doesn't import "back" to prevent cycles (same as shared.api).
But in order to do this lots of structs required to move some of their fields from structs to interfaces to prevent import cycles.
Also a pattern this PR gets rid of is the following, exposing internal types by redefining them publicly, see https://github.com/canonical/microcluster/blob/v3/state/state.go.

I'll rework both commits and PR description to be more concrete about what is happening but just wanted to push so that I have something to discuss with @markylaing in our next meet.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR restructures Microcluster packages to improve API design by moving types from internal packages to public ones and introducing interfaces to enable better testing and mocking. The refactoring hides implementation details behind interfaces while making core types publicly accessible.

Key changes:

  • Relocated types from rest/, state/, and internal packages to microcluster/types/
  • Introduced interfaces (State, DB, OS, Client, Connector) to hide internal implementations
  • Converted client methods to standalone functions for better interface compliance
  • Updated all import paths throughout the codebase

Reviewed Changes

Copilot reviewed 98 out of 103 changed files in this pull request and generated no comments.

Show a summary per file
File Description
microcluster/types/*.go New public type definitions moved from internal packages including State, DB, OS interfaces and core types
rest/, state/ packages Deleted type alias files, functionality moved to microcluster/types
internal/rest/client/client.go Client methods converted to standalone functions, added HTTP() accessor
internal/state/state.go Updated to use new types package, added Connect() and Member() methods
internal/db/ Updated schema and query imports to use public microcluster/db package
internal/sys/os.go OS struct fields made private, added public accessor methods
microcluster/app.go Updated to use new types and standalone client functions
Test files Package names updated to reflect new structure (e.g., query_testdb_test)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@tomponline
Copy link
Member

Can you define the types externally to internal and then use them from both the /internal functions and the call sites (like shared.api)?

Yes that is the idea. This PR introduces a microcluster/types package which doesn't import "back" to prevent cycles (same as shared.api). But in order to do this lots of structs required to move some of their fields from structs to interfaces to prevent import cycles. Also a pattern this PR gets rid of is the following, exposing internal types by redefining them publicly, see https://github.com/canonical/microcluster/blob/v3/state/state.go.

I'll rework both commits and PR description to be more concrete about what is happening but just wanted to push so that I have something to discuss with @markylaing in our next meet.

OK thanks for explaining. I would just say be cautious in using too many Interfaces and check they are truly needed because there is a cost to using them. I myself have used interfaces to break import cycles in the past so I understand what you are doing.

Also ensure that all endpoints are covered by a client func in the app construct.
@markylaing
Copy link
Contributor

Few points from meeting:

  1. Summary of which endpoints have been moved where and why. Anything /internal should be something that a client will never call. Any changes need careful consideration for upgrades from v2 to v3. Mixed versions could be breaking if APIs have moved around.
  2. Package moves all good 👍
  3. /internal/sql should not be on TLS listener (unix only).

@tomponline
Copy link
Member

/internal/sql should not be on TLS listener (unix only).

So important!

roosterfish added a commit that referenced this pull request Nov 26, 2025
This is the first PR taken from a single commit in
#534.
The final package layout will be different but to allow reviewing the
changes in smaller chunks I will split this PR by its commits.

It consolidates the DB funcs and ensures the ones that should be used
externally are public and not in the `internal` package.

After the restructuring all the DB related functionality will be under
the `microcluster/db` package alongside the app construct which is
located in `microcluster`. See
https://github.com/canonical/microcluster/pull/534/files
Copy link
Member

@kadinsayani kadinsayani left a comment

Choose a reason for hiding this comment

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

The new package structure LGTM overall. Shall we also update the Makefile update-schema target in this PR?

❯ make update-schema
go generate ./cluster/...
pattern ./cluster/...: lstat ./cluster/: no such file or directory
make: *** [Makefile:66: update-schema] Error 1

@roosterfish
Copy link
Contributor Author

The new package structure LGTM overall. Shall we also update the Makefile update-schema target in this PR?

Good catch. We should drop this overall as we agreed on not anymore using lxd-generate and instead maintain the few queries manually.

roosterfish added a commit that referenced this pull request Dec 2, 2025
This is the second PR taken from a single commit in
#534.
The final package layout will be different but to allow reviewing the
changes in smaller chunks I will split this PR by its commits.

In this PR types which might end up on the user's end through the
various channels (client funcs, app funcs, API handlers) are now moved
to the central `microcluster/*` package which is the regular entry point
for Microcluster users to derive an app construct.
roosterfish added a commit that referenced this pull request Dec 3, 2025
This is the third PR taken from a single commit in
#534.
The final package layout will be different but to allow reviewing the
changes in smaller chunks I will split this PR by its commits.

In this PR the state directory utils (internal `sys` package) are not
anymore returned directly from the state but are now hidden behind a
common `OS` interface inside `microcluster/types` package. As the state
is exposed to API handlers written by downstream users, this allows
proper mocking.

We should expose this to allow easy access to the daemon's config and
certificates where necessary.
roosterfish added a commit that referenced this pull request Jan 15, 2026
This is the fourth PR taken from a single commit in
#534.
The final package layout will be different but to allow reviewing the
changes in smaller chunks I will split this PR by its commits.

In this PR two new `Client` and `Connector` interfaces are added to
address all use cases when it comes to cluster wide HTTP connections.
In addition we now have app level funcs for all user facing operations
which previously required going to a separate client package.
Furthermore the specific query functions are now moved to `internal/` as
they should not be exposed directly to the end user.
markylaing added a commit that referenced this pull request Jan 19, 2026
This is the fifth PR taken from a single commit in
#534.
The final package layout will be different but to allow reviewing the
changes in smaller chunks I will split this PR by its commits.

In this PR the opinionated `api.URL` type from LXD (which embeds the std
libs `url.URL`) is replaced with the std libs `url.URL` to lower the
amount of deps on LXD's `shared` package.
But we still use it to allow fast crafting of URLs internally.

Also it simplifies many statements that require access to an URL's
`Host` as there is one less level of struct nesting.
roosterfish added a commit that referenced this pull request Jan 20, 2026
This is the sixth PR taken from a single commit in
#534 (even though in this
case the actual changes are very different from the original commit).
The final package layout will be different but to allow reviewing the
changes in smaller chunks I will split this PR by its commits.

In this PR the `Remotes` func from the `State` interface is replaced
with an interface to not anymore return an implementation of the
truststore with types located inside the `internal/` package.
Furthermore the naming is made a bit more consistent:

* `Store` represents a truststore
* `Remote` represents an entry in the truststore

The initial idea was to just hide the truststore access. However it's a
very inexpensive way of querying for the existence and details of
cluster members as the truststore is already located in memory.
roosterfish added a commit that referenced this pull request Feb 3, 2026
This is the seventh PR taken from a single commit in
#534 (even though in this
case the actual changes are very different from the original commit).
The final package layout will be different but to allow reviewing the
changes in smaller chunks I will split this PR by its commits.

In this PR the extensions package is made public so that the actual
types can be accessed by importers of Microcluster.
roosterfish added a commit that referenced this pull request Feb 4, 2026
Can only be merged after (won't build before):
* #607

This is the eigth PR taken from a single commit in
#534 (even though in this
case the actual changes are very different from the original commit).
The final package layout will be different but to allow reviewing the
changes in smaller chunks I will split this PR by its commits.

In this PR the DB interface is made public. It's returned by the public
`State` interface and should therefore not be located inside the
`internal/` package.
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