From 4943e0b1cd74826baab840aec24e25b82877471f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 20 Jan 2026 09:50:39 +0100 Subject: [PATCH 01/11] microcluster/types: Add Store interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Store interface allows masking the internal truststore implementation and offers helpful utilities to query truststore information purely in memory (except the add/replace operations). By having the interface in a the public types package we can replace the Remotes func in the public State interface. Signed-off-by: Julian Pelizäus --- microcluster/types/truststore.go | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 microcluster/types/truststore.go diff --git a/microcluster/types/truststore.go b/microcluster/types/truststore.go new file mode 100644 index 00000000..03f5dba3 --- /dev/null +++ b/microcluster/types/truststore.go @@ -0,0 +1,37 @@ +package types + +import ( + "crypto/x509" + + "github.com/canonical/lxd/shared" +) + +// Store represents a local truststore. +type Store interface { + // A list of clients for each of the remotes. + RemoteClients(isNotification bool, serverCert *shared.CertInfo, publicKey *x509.Certificate) (Clients, error) + + // All remotes keyed by their name. + RemotesByName() map[string]Remote + + // A specific remote filtered by its address. + RemoteByAddress(addrPort AddrPort) *Remote + + // All remotes' addresses keyed by their name. + RemoteAddresses() map[string]AddrPort + + // All remotes' certificates keyed by their name. + RemoteCertificates() map[string]X509Certificate + + // Same as RemoteCertificates but using the standard libraries type. + RemoteCertificatesNative() map[string]x509.Certificate + + // The total amount of remotes in the truststore. + Count() int + + // Add a new remote to the truststore. + Add(dir string, remotes ...Remote) error + + // Replace remotes in the truststore. + Replace(dir string, newRemotes ...ClusterMember) error +} From 2a14cdc13f78919990c292f3be2768ffdc6eee8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 20 Jan 2026 09:51:13 +0100 Subject: [PATCH 02/11] microcluster/types: Add Location and Remote types representing truststore entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- microcluster/types/truststore.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/microcluster/types/truststore.go b/microcluster/types/truststore.go index 03f5dba3..3b29b9df 100644 --- a/microcluster/types/truststore.go +++ b/microcluster/types/truststore.go @@ -2,8 +2,10 @@ package types import ( "crypto/x509" + "net/url" "github.com/canonical/lxd/shared" + "github.com/canonical/lxd/shared/api" ) // Store represents a local truststore. @@ -35,3 +37,20 @@ type Store interface { // Replace remotes in the truststore. Replace(dir string, newRemotes ...ClusterMember) error } + +// Location represents configurable identifying information about a remote. +type Location struct { + Name string `yaml:"name"` + Address AddrPort `yaml:"address"` +} + +// Remote represents a yaml file with credentials to be read by the daemon. +type Remote struct { + Location `yaml:",inline"` + Certificate X509Certificate `yaml:"certificate"` +} + +// URL returns the parsed URL of the Remote. +func (r *Remote) URL() *url.URL { + return &api.NewURL().Scheme("https").Host(r.Address.String()).URL +} From 65e5072a76d386c1614c07486e0db48fffbe92a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 20 Jan 2026 09:52:50 +0100 Subject: [PATCH 03/11] internal/state: Replace Remotes with new Store interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also change the wording a little bit to represent the actual truststore. Entries in the truststore are called remotes therefore those are returned/used by the various funcs of the Store interface. Signed-off-by: Julian Pelizäus --- internal/state/state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/state/state.go b/internal/state/state.go index dca9dd69..a9358bc2 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -49,7 +49,7 @@ type State interface { Database() db.DB // Local truststore access. - Remotes() *trust.Remotes + Truststore() types.Store // Returns a connector for interconnection with the cluster. Connect() types.Connector From 56048e1d62f00f86b1ef427a293e8dfcff356c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 20 Jan 2026 09:55:02 +0100 Subject: [PATCH 04/11] internal/trust: Remove the types which where moved to the public package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/trust/remotes.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/internal/trust/remotes.go b/internal/trust/remotes.go index fd1f6b75..98d6b01d 100644 --- a/internal/trust/remotes.go +++ b/internal/trust/remotes.go @@ -4,7 +4,6 @@ import ( "crypto/x509" "errors" "fmt" - "net/url" "os" "path/filepath" "strings" @@ -25,18 +24,6 @@ type Remotes struct { updateMu sync.RWMutex } -// Remote represents a yaml file with credentials to be read by the daemon. -type Remote struct { - Location `yaml:",inline"` - Certificate types.X509Certificate `yaml:"certificate"` -} - -// Location represents configurable identifying information about a remote. -type Location struct { - Name string `yaml:"name"` - Address types.AddrPort `yaml:"address"` -} - // disallowedFileNameSubcontents contains the list of disallowed substrings in remote names. var disallowedFilenameSubcontents = []string{"..", "/", "\\"} @@ -339,8 +326,3 @@ func (r *Remotes) RemotesByName() map[string]Remote { return remoteData } - -// URL returns the parsed URL of the Remote. -func (r *Remote) URL() *url.URL { - return &api.NewURL().Scheme("https").Host(r.Address.String()).URL -} From e92d4782176b847cd8a1220f875c835dff928042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 20 Jan 2026 09:56:19 +0100 Subject: [PATCH 05/11] internal/trust: Implement the new Store interface for internal truststore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/trust/remotes.go | 40 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/internal/trust/remotes.go b/internal/trust/remotes.go index 98d6b01d..298ebff8 100644 --- a/internal/trust/remotes.go +++ b/internal/trust/remotes.go @@ -20,7 +20,7 @@ import ( // Remotes is a convenient alias as we will often deal with groups of yaml files. type Remotes struct { - data map[string]Remote + data map[string]types.Remote updateMu sync.RWMutex } @@ -62,7 +62,7 @@ func (r *Remotes) Load(dir string) error { return fmt.Errorf("Unable to read trust directory: %q: %w", dir, err) } - remoteData := map[string]Remote{} + remoteData := map[string]types.Remote{} for _, file := range files { fileName := file.Name() if file.IsDir() || !strings.HasSuffix(fileName, ".yaml") { @@ -74,7 +74,7 @@ func (r *Remotes) Load(dir string) error { return fmt.Errorf("Unable to read file %q: %w", fileName, err) } - remote := &Remote{} + remote := &types.Remote{} err = yaml.Unmarshal(content, remote) if err != nil { return fmt.Errorf("Unable to parse yaml for %q: %w", fileName, err) @@ -99,7 +99,7 @@ func (r *Remotes) Load(dir string) error { } // Add adds a new local cluster member record for the remotes. -func (r *Remotes) Add(dir string, remotes ...Remote) error { +func (r *Remotes) Add(dir string, remotes ...types.Remote) error { r.updateMu.Lock() defer r.updateMu.Unlock() @@ -158,10 +158,10 @@ func (r *Remotes) Replace(dir string, newRemotes ...types.ClusterMember) error { return fmt.Errorf("Received empty remotes") } - remoteData := map[string]Remote{} + remoteData := map[string]types.Remote{} for _, remote := range newRemotes { - newRemote := Remote{ - Location: Location{Name: remote.Name, Address: remote.Address}, + newRemote := types.Remote{ + Location: types.Location{Name: remote.Name, Address: remote.Address}, Certificate: remote.Certificate, } @@ -223,8 +223,8 @@ func (r *Remotes) Replace(dir string, newRemotes ...types.ClusterMember) error { return nil } -// Addresses returns just the host:port addresses of the remotes. -func (r *Remotes) Addresses() map[string]types.AddrPort { +// RemoteAddresses returns just the host:port addresses of the remotes. +func (r *Remotes) RemoteAddresses() map[string]types.AddrPort { r.updateMu.RLock() defer r.updateMu.RUnlock() @@ -236,10 +236,10 @@ func (r *Remotes) Addresses() map[string]types.AddrPort { return addrs } -// Cluster returns a set of clients for every remote, which can be concurrently queried. -func (r *Remotes) Cluster(isNotification bool, serverCert *shared.CertInfo, publicKey *x509.Certificate) (types.Clients, error) { +// RemoteClients returns a set of clients for every remote, which can be concurrently queried. +func (r *Remotes) RemoteClients(isNotification bool, serverCert *shared.CertInfo, publicKey *x509.Certificate) (types.Clients, error) { cluster := make(types.Clients, 0, r.Count()-1) - for _, addr := range r.Addresses() { + for _, addr := range r.RemoteAddresses() { url := &api.NewURL().Scheme("https").Host(addr.String()).URL c, err := internalClient.New(url, serverCert, publicKey, isNotification) if err != nil { @@ -253,7 +253,7 @@ func (r *Remotes) Cluster(isNotification bool, serverCert *shared.CertInfo, publ } // RemoteByAddress returns a Remote matching the given host address (or nil if none are found). -func (r *Remotes) RemoteByAddress(addrPort types.AddrPort) *Remote { +func (r *Remotes) RemoteByAddress(addrPort types.AddrPort) *types.Remote { r.updateMu.RLock() defer r.updateMu.RUnlock() @@ -267,7 +267,7 @@ func (r *Remotes) RemoteByAddress(addrPort types.AddrPort) *Remote { } // RemoteByCertificateFingerprint returns a remote whose certificate fingerprint matches the provided fingerprint. -func (r *Remotes) RemoteByCertificateFingerprint(fingerprint string) *Remote { +func (r *Remotes) RemoteByCertificateFingerprint(fingerprint string) *types.Remote { r.updateMu.RLock() defer r.updateMu.RUnlock() @@ -280,8 +280,8 @@ func (r *Remotes) RemoteByCertificateFingerprint(fingerprint string) *Remote { return nil } -// Certificates returns a map of remotes certificates by fingerprint. -func (r *Remotes) Certificates() map[string]types.X509Certificate { +// RemoteCertificates returns a map of remotes certificates by fingerprint. +func (r *Remotes) RemoteCertificates() map[string]types.X509Certificate { r.updateMu.RLock() defer r.updateMu.RUnlock() @@ -293,8 +293,8 @@ func (r *Remotes) Certificates() map[string]types.X509Certificate { return certMap } -// CertificatesNative returns the Certificates map with values as native x509.Certificate type. -func (r *Remotes) CertificatesNative() map[string]x509.Certificate { +// RemoteCertificatesNative returns the Certificates map with values as native x509.Certificate type. +func (r *Remotes) RemoteCertificatesNative() map[string]x509.Certificate { r.updateMu.RLock() defer r.updateMu.RUnlock() @@ -315,11 +315,11 @@ func (r *Remotes) Count() int { } // RemotesByName returns a copy of the list of peers, keyed by each system's name. -func (r *Remotes) RemotesByName() map[string]Remote { +func (r *Remotes) RemotesByName() map[string]types.Remote { r.updateMu.RLock() defer r.updateMu.RUnlock() - remoteData := make(map[string]Remote, len(r.data)) + remoteData := make(map[string]types.Remote, len(r.data)) for name, data := range r.data { remoteData[name] = data } From a9232cb578299a60b47ee84d6e96ad87a705f7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 20 Jan 2026 09:56:40 +0100 Subject: [PATCH 06/11] internal/trust: Use new truststore implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/trust/truststore.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/trust/truststore.go b/internal/trust/truststore.go index efe2b9cd..ea305632 100644 --- a/internal/trust/truststore.go +++ b/internal/trust/truststore.go @@ -7,6 +7,7 @@ import ( "github.com/fsnotify/fsnotify" "github.com/canonical/microcluster/v3/internal/sys" + "github.com/canonical/microcluster/v3/microcluster/types" ) // Store represents a directory of remotes watched by the fsnotify Watcher. @@ -24,7 +25,7 @@ func Init(watcher *sys.Watcher, onUpdate func(oldRemotes, newRemotes Remotes) er ts.remotesMu.Lock() defer ts.remotesMu.Unlock() - ts.remotes.data = map[string]Remote{} + ts.remotes.data = map[string]types.Remote{} err := ts.remotes.Load(dir) if err != nil { return nil, err From 704ddee6c785fe32631f2c8ac37fba0a44da1b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 20 Jan 2026 09:57:07 +0100 Subject: [PATCH 07/11] internal/daemon: Use new truststore implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/daemon/daemon.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index e4933371..46ea596a 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -500,7 +500,7 @@ func (d *Daemon) initServer(resources ...rest.Resources) *http.Server { return &http.Server{ Handler: mux, - ErrorLog: log.New(newLogFilter(d.log(), state.Remotes().Addresses), "", 0), + ErrorLog: log.New(newLogFilter(d.log(), state.Truststore().RemoteAddresses), "", 0), // Set a base context for the server. // This allows passing the logger on the daemon's shutdown context on to each handler. BaseContext: func(_ net.Listener) context.Context { @@ -510,7 +510,7 @@ func (d *Daemon) initServer(resources ...rest.Resources) *http.Server { } // setConfig applies and commits to memory the supplied daemon configuration. -func (d *Daemon) setConfig(newConfig trust.Location) error { +func (d *Daemon) setConfig(newConfig types.Location) error { d.config.SetAddress(newConfig.Address) d.config.SetName(newConfig.Name) @@ -535,8 +535,8 @@ func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[st return fmt.Errorf("Failed to parse listen address when bootstrapping API: %w", err) } - localNode := trust.Remote{ - Location: trust.Location{Name: d.config.GetName(), Address: addrPort}, + localNode := types.Remote{ + Location: types.Location{Name: d.config.GetName(), Address: addrPort}, Certificate: types.X509Certificate{Certificate: serverCert}, } @@ -619,7 +619,7 @@ func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[st return fmt.Errorf("Failed to join cluster: %w", err) } } else { - err = d.db.StartWithCluster(d.Extensions, d.Address(), d.trustStore.Remotes().Addresses()) + err = d.db.StartWithCluster(d.Extensions, d.Address(), d.trustStore.Remotes().RemoteAddresses()) if err != nil { return fmt.Errorf("Failed to re-establish cluster connection: %w", err) } @@ -636,7 +636,7 @@ func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[st return err } - clients, err := d.trustStore.Remotes().Cluster(false, d.ServerCert(), publicKey) + clients, err := d.trustStore.Remotes().RemoteClients(false, d.ServerCert(), publicKey) if err != nil { return err } From 5bdff586e3856fe74e715bb2bbd744912feede40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 20 Jan 2026 09:57:23 +0100 Subject: [PATCH 08/11] internal/recover: Use new truststore implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/recover/recover.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/recover/recover.go b/internal/recover/recover.go index 4a9712f9..e74de789 100644 --- a/internal/recover/recover.go +++ b/internal/recover/recover.go @@ -111,7 +111,7 @@ func RecoverFromQuorumLoss(ctx context.Context, filesystem types.OS, members []t return "", err } - clients, err := remotes.Cluster(false, serverCert, clusterKey) + clients, err := remotes.RemoteClients(false, serverCert, clusterKey) if err != nil { return "", err } From c8e5f6a289065f5cf3638c1690ad5842406f3c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 20 Jan 2026 09:58:27 +0100 Subject: [PATCH 09/11] internal/rest/resources: Use new truststore implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/rest/resources/cluster.go | 15 +++++++-------- internal/rest/resources/control.go | 21 ++++++++++----------- internal/rest/resources/daemon.go | 2 +- internal/rest/resources/heartbeat.go | 4 ++-- internal/rest/resources/tokens.go | 4 ++-- internal/rest/resources/truststore.go | 11 +++++------ 6 files changed, 27 insertions(+), 30 deletions(-) diff --git a/internal/rest/resources/cluster.go b/internal/rest/resources/cluster.go index a54c8e38..fc2c8768 100644 --- a/internal/rest/resources/cluster.go +++ b/internal/rest/resources/cluster.go @@ -28,7 +28,6 @@ import ( "github.com/canonical/microcluster/v3/internal/rest/access" internalClient "github.com/canonical/microcluster/v3/internal/rest/client" internalState "github.com/canonical/microcluster/v3/internal/state" - "github.com/canonical/microcluster/v3/internal/trust" "github.com/canonical/microcluster/v3/internal/utils" "github.com/canonical/microcluster/v3/microcluster/rest" "github.com/canonical/microcluster/v3/microcluster/rest/response" @@ -94,7 +93,7 @@ func clusterPost(s state.State, r *http.Request) response.Response { } // Check if any of the remote's addresses are currently in use. - existingRemote := s.Remotes().RemoteByAddress(req.Address) + existingRemote := s.Truststore().RemoteByAddress(req.Address) if existingRemote != nil { return response.SmartError(fmt.Errorf("Remote with address %q exists", req.Address.String())) } @@ -168,7 +167,7 @@ func clusterPost(s state.State, r *http.Request) response.Response { return response.SmartError(err) } - remotes := s.Remotes() + remotes := s.Truststore() clusterMembers := make([]types.ClusterMemberLocal, 0, remotes.Count()) for _, clusterMember := range remotes.RemotesByName() { clusterMember := types.ClusterMemberLocal{ @@ -194,13 +193,13 @@ func clusterPost(s state.State, r *http.Request) response.Response { ClusterMembers: clusterMembers, } - newRemote := trust.Remote{ - Location: trust.Location{Name: req.Name, Address: req.Address}, + newRemote := types.Remote{ + Location: types.Location{Name: req.Name, Address: req.Address}, Certificate: req.Certificate, } // Add the cluster member to our local store for authentication. - err = s.Remotes().Add(s.FileSystem().TrustDir(), newRemote) + err = s.Truststore().Add(s.FileSystem().TrustDir(), newRemote) if err != nil { return response.SmartError(err) } @@ -423,7 +422,7 @@ func clusterMemberDelete(s state.State, r *http.Request) response.Response { return response.SmartError(err) } - allRemotes := s.Remotes().RemotesByName() + allRemotes := s.Truststore().RemotesByName() remote, ok := allRemotes[name] if !ok { return response.SmartError(fmt.Errorf("No remote exists with the given name %q", name)) @@ -703,7 +702,7 @@ func clusterMemberDelete(s state.State, r *http.Request) response.Response { } // Run the PostRemove hook on all other members. - remotes := s.Remotes() + remotes := s.Truststore() err = clients.Query(ctx, true, func(ctx context.Context, c types.Client) error { c.SetClusterNotification() addrPort, err := types.ParseAddrPort(c.URL().Host) diff --git a/internal/rest/resources/control.go b/internal/rest/resources/control.go index 262679ba..f13663e4 100644 --- a/internal/rest/resources/control.go +++ b/internal/rest/resources/control.go @@ -18,7 +18,6 @@ import ( "github.com/canonical/microcluster/v3/internal/rest/access" internalClient "github.com/canonical/microcluster/v3/internal/rest/client" internalState "github.com/canonical/microcluster/v3/internal/state" - "github.com/canonical/microcluster/v3/internal/trust" "github.com/canonical/microcluster/v3/internal/utils" "github.com/canonical/microcluster/v3/microcluster/rest" "github.com/canonical/microcluster/v3/microcluster/rest/response" @@ -59,7 +58,7 @@ func controlPost(state state.State, r *http.Request) response.Response { return response.SmartError(err) } - daemonConfig := trust.Location{Address: req.Address, Name: req.Name} + daemonConfig := types.Location{Address: req.Address, Name: req.Name} err = intState.SetConfig(daemonConfig) if err != nil { return response.SmartError(err) @@ -164,7 +163,7 @@ func controlPost(state state.State, r *http.Request) response.Response { } var joinAddrs []string - var localClusterMember *trust.Remote + var localClusterMember *types.Remote if req.JoinToken != "" { joinInfo, localClusterMember, err = joinWithToken(state, r, req) if err != nil { @@ -187,7 +186,7 @@ func controlPost(state state.State, r *http.Request) response.Response { return response.EmptySyncResponse } -func joinWithToken(state state.State, r *http.Request, req *types.Control) (*types.TokenResponse, *trust.Remote, error) { +func joinWithToken(state state.State, r *http.Request, req *types.Control) (*types.TokenResponse, *types.Remote, error) { token, err := types.DecodeToken(req.JoinToken) if err != nil { return nil, nil, err @@ -204,8 +203,8 @@ func joinWithToken(state state.State, r *http.Request, req *types.Control) (*typ } // Add the local node to the list of clusterMembers. - daemonConfig := &trust.Location{Address: req.Address, Name: req.Name} - localClusterMember := trust.Remote{ + daemonConfig := &types.Location{Address: req.Address, Name: req.Name} + localClusterMember := types.Remote{ Location: *daemonConfig, Certificate: types.X509Certificate{Certificate: serverCert}, } @@ -290,7 +289,7 @@ func writeCert(dir, prefix string, cert, key, ca []byte) error { return nil } -func setupLocalMember(state state.State, localClusterMember *trust.Remote, joinInfo *types.TokenResponse) ([]string, error) { +func setupLocalMember(state state.State, localClusterMember *types.Remote, joinInfo *types.TokenResponse) ([]string, error) { // Set up cluster certificate. err := writeCert(state.FileSystem().StateDir(), string(types.ClusterCertificateName), []byte(joinInfo.ClusterCert.String()), []byte(joinInfo.ClusterKey), nil) if err != nil { @@ -312,10 +311,10 @@ func setupLocalMember(state state.State, localClusterMember *trust.Remote, joinI } joinAddrs := types.AddrPorts{} - clusterMembers := make([]trust.Remote, 0, len(joinInfo.ClusterMembers)+1) + clusterMembers := make([]types.Remote, 0, len(joinInfo.ClusterMembers)+1) for _, clusterMember := range joinInfo.ClusterMembers { - remote := trust.Remote{ - Location: trust.Location{Name: clusterMember.Name, Address: clusterMember.Address}, + remote := types.Remote{ + Location: types.Location{Name: clusterMember.Name, Address: clusterMember.Address}, Certificate: clusterMember.Certificate, } @@ -324,7 +323,7 @@ func setupLocalMember(state state.State, localClusterMember *trust.Remote, joinI } clusterMembers = append(clusterMembers, *localClusterMember) - err = state.Remotes().Add(state.FileSystem().TrustDir(), clusterMembers...) + err = state.Truststore().Add(state.FileSystem().TrustDir(), clusterMembers...) if err != nil { return nil, err } diff --git a/internal/rest/resources/daemon.go b/internal/rest/resources/daemon.go index 353e8aa6..c6226ee0 100644 --- a/internal/rest/resources/daemon.go +++ b/internal/rest/resources/daemon.go @@ -94,7 +94,7 @@ func daemonServersPut(s state.State, r *http.Request) response.Response { } // Run the OnDaemonConfigUpdate hook on all other members. - remotes := s.Remotes() + remotes := s.Truststore() err = clients.Query(r.Context(), true, func(ctx context.Context, c types.Client) error { c.SetClusterNotification() addrPort, err := types.ParseAddrPort(c.URL().Host) diff --git a/internal/rest/resources/heartbeat.go b/internal/rest/resources/heartbeat.go index 338fd64f..8d997225 100644 --- a/internal/rest/resources/heartbeat.go +++ b/internal/rest/resources/heartbeat.go @@ -49,7 +49,7 @@ func heartbeatPost(s state.State, r *http.Request) response.Response { clusterMemberList = append(clusterMemberList, clusterMember) } - err = s.Remotes().Replace(s.FileSystem().TrustDir(), clusterMemberList...) + err = s.Truststore().Replace(s.FileSystem().TrustDir(), clusterMemberList...) if err != nil { return response.SmartError(err) } @@ -166,7 +166,7 @@ func beginHeartbeat(ctx context.Context, s state.State, hbReq types.HeartbeatInf logger.Debug("Beginning new heartbeat round", slog.String("address", s.Address().Host)) // Update local record of cluster members from the database, including any pending nodes for authentication. - err = s.Remotes().Replace(s.FileSystem().TrustDir(), clusterMembers...) + err = s.Truststore().Replace(s.FileSystem().TrustDir(), clusterMembers...) if err != nil { return response.SmartError(err) } diff --git a/internal/rest/resources/tokens.go b/internal/rest/resources/tokens.go index a74cbce5..6f427aac 100644 --- a/internal/rest/resources/tokens.go +++ b/internal/rest/resources/tokens.go @@ -75,7 +75,7 @@ func tokensPost(state state.State, r *http.Request) response.Response { } joinAddresses := []types.AddrPort{} - for _, addr := range state.Remotes().Addresses() { + for _, addr := range state.Truststore().RemoteAddresses() { joinAddresses = append(joinAddresses, addr) } @@ -138,7 +138,7 @@ func tokensGet(state state.State, r *http.Request) response.Response { } joinAddresses := []types.AddrPort{} - for _, addr := range state.Remotes().Addresses() { + for _, addr := range state.Truststore().RemoteAddresses() { joinAddresses = append(joinAddresses, addr) } diff --git a/internal/rest/resources/truststore.go b/internal/rest/resources/truststore.go index 1b18d431..71a98757 100644 --- a/internal/rest/resources/truststore.go +++ b/internal/rest/resources/truststore.go @@ -15,7 +15,6 @@ import ( "github.com/canonical/microcluster/v3/internal/log" "github.com/canonical/microcluster/v3/internal/rest/access" internalClient "github.com/canonical/microcluster/v3/internal/rest/client" - "github.com/canonical/microcluster/v3/internal/trust" "github.com/canonical/microcluster/v3/microcluster/rest" "github.com/canonical/microcluster/v3/microcluster/rest/response" "github.com/canonical/microcluster/v3/microcluster/types" @@ -45,8 +44,8 @@ func trustPost(s state.State, r *http.Request) response.Response { return response.BadRequest(err) } - newRemote := trust.Remote{ - Location: trust.Location{Name: req.Name, Address: req.Address}, + newRemote := types.Remote{ + Location: types.Location{Name: req.Name, Address: req.Address}, Certificate: req.Certificate, } @@ -103,7 +102,7 @@ func trustPost(s state.State, r *http.Request) response.Response { } // At this point, the node has joined dqlite so we can add a local record for it if we haven't already from a heartbeat (or if we are the leader). - remotes := s.Remotes() + remotes := s.Truststore() _, ok := remotes.RemotesByName()[newRemote.Name] if !ok { err = remotes.Add(s.FileSystem().TrustDir(), newRemote) @@ -124,7 +123,7 @@ func trustDelete(s state.State, r *http.Request) response.Response { ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() - remotesMap := s.Remotes().RemotesByName() + remotesMap := s.Truststore().RemotesByName() nodeToRemove, ok := remotesMap[name] if !ok { return response.SmartError(fmt.Errorf("No truststore entry found for node with name %q", name)) @@ -149,7 +148,7 @@ func trustDelete(s state.State, r *http.Request) response.Response { } } - remotes := s.Remotes() + remotes := s.Truststore() remotesMap = remotes.RemotesByName() delete(remotesMap, name) From ce5d2fa395325293a67aa6124378577a4bdb09d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 20 Jan 2026 09:58:39 +0100 Subject: [PATCH 10/11] internal/rest: Use new truststore implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/rest/rest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/rest/rest.go b/internal/rest/rest.go index 1496f56f..f260d692 100644 --- a/internal/rest/rest.go +++ b/internal/rest/rest.go @@ -273,7 +273,7 @@ func HandleEndpoint(state state.State, mux *mux.Router, version string, e rest.E handleRequest = handleDatabaseRequest } - trusted, err := access.Authenticate(state, r, state.Address().Host, state.Remotes().CertificatesNative()) + trusted, err := access.Authenticate(state, r, state.Address().Host, state.Truststore().RemoteCertificatesNative()) if err != nil && !errors.As(err, &access.ErrInvalidHost{}) { resp = response.Forbidden(fmt.Errorf("Failed to authenticate request: %w", err)) } else { From 9d988f467bcad28ac2b69a132b4f6b19550f7d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 20 Jan 2026 09:59:15 +0100 Subject: [PATCH 11/11] internal/state: Use new truststore implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/state/state.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/state/state.go b/internal/state/state.go index a9358bc2..7800358a 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -79,7 +79,7 @@ type InternalState struct { LocalConfig func() *internalConfig.DaemonConfig // SetConfig Applies and commits to memory the supplied daemon configuration. - SetConfig func(trust.Location) error + SetConfig func(types.Location) error // Initialize APIs and bootstrap/join database. StartAPI func(ctx context.Context, bootstrap bool, initConfig map[string]string, joinAddresses ...string) error @@ -149,8 +149,8 @@ func (s *InternalState) Database() db.DB { return s.InternalDatabase } -// Remotes returns the local record of cluster members in the truststore. -func (s *InternalState) Remotes() *trust.Remotes { +// Truststore returns the local record of cluster members in the truststore. +func (s *InternalState) Truststore() types.Store { return s.InternalRemotes() } @@ -182,8 +182,8 @@ func (s *InternalState) Cluster(isNotification bool) (types.Clients, error) { // Use trust store instead of database - it's updated on heartbeats // and is more likely to reflect current reachable cluster state - remotes := s.Remotes() - allClients, err := remotes.Cluster(isNotification, s.ServerCert(), publicKey) + remotes := s.Truststore() + allClients, err := remotes.RemoteClients(isNotification, s.ServerCert(), publicKey) if err != nil { return nil, err } @@ -319,7 +319,7 @@ func (s *InternalState) CheckMembershipConsistency(ctx context.Context) error { } // getMembershipData retrieves membership information from all sources. -func (s *InternalState) getMembershipData(ctx context.Context) ([]cluster.CoreClusterMember, map[string]trust.Remote, []dqliteClient.NodeInfo, error) { +func (s *InternalState) getMembershipData(ctx context.Context) ([]cluster.CoreClusterMember, map[string]types.Remote, []dqliteClient.NodeInfo, error) { // Get database core cluster members var coreClusterMembers []cluster.CoreClusterMember err := s.Database().Transaction(ctx, func(ctx context.Context, tx *sql.Tx) error { @@ -332,7 +332,7 @@ func (s *InternalState) getMembershipData(ctx context.Context) ([]cluster.CoreCl } // Get truststore remotes - truststoreRemotes := s.Remotes().RemotesByName() + truststoreRemotes := s.Truststore().RemotesByName() // Get dqlite cluster info leaderClient, err := s.Database().Leader(ctx) @@ -350,7 +350,7 @@ func (s *InternalState) getMembershipData(ctx context.Context) ([]cluster.CoreCl } // checkMembershipConsistency checks consistency across all three membership sources using addresses. -func (s *InternalState) checkMembershipConsistency(coreClusterMembers []cluster.CoreClusterMember, truststoreRemotes map[string]trust.Remote, dqliteNodes []dqliteClient.NodeInfo) error { +func (s *InternalState) checkMembershipConsistency(coreClusterMembers []cluster.CoreClusterMember, truststoreRemotes map[string]types.Remote, dqliteNodes []dqliteClient.NodeInfo) error { // Collect addresses from each source into sorted slices var coreClusterAddresses []string for _, member := range coreClusterMembers {