From 301eec838e3f68486cce49cc8ab5bb399ae7e78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Jan 2026 10:35:34 +0100 Subject: [PATCH 01/11] internal/endpoints: Use std library URL type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/endpoints/network.go | 8 ++++---- internal/endpoints/socket.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/endpoints/network.go b/internal/endpoints/network.go index ef5dc277a..9edaf02bd 100644 --- a/internal/endpoints/network.go +++ b/internal/endpoints/network.go @@ -7,19 +7,19 @@ import ( "log/slog" "net" "net/http" + "net/url" "strings" "sync" "time" "github.com/canonical/lxd/shared" - "github.com/canonical/lxd/shared/api" "github.com/canonical/microcluster/v3/internal/log" ) // Network represents an HTTPS listener and its server. type Network struct { - address api.URL + address *url.URL certMu sync.RWMutex cert *shared.CertInfo networkType EndpointType @@ -34,7 +34,7 @@ type Network struct { } // NewNetwork assigns an address, certificate, and server to the Network. -func NewNetwork(ctx context.Context, endpointType EndpointType, server *http.Server, address api.URL, cert *shared.CertInfo, drainConnTimeout time.Duration) *Network { +func NewNetwork(ctx context.Context, endpointType EndpointType, server *http.Server, address *url.URL, cert *shared.CertInfo, drainConnTimeout time.Duration) *Network { ctx, cancel := context.WithCancel(ctx) return &Network{ @@ -63,7 +63,7 @@ func (n *Network) Type() EndpointType { // Listen on the given address. func (n *Network) Listen() error { - listenAddress := canonicalNetworkAddress(n.address.URL.Host, shared.HTTPSDefaultPort) + listenAddress := canonicalNetworkAddress(n.address.Host, shared.HTTPSDefaultPort) protocol := "tcp" if strings.HasPrefix(listenAddress, "0.0.0.0") { diff --git a/internal/endpoints/socket.go b/internal/endpoints/socket.go index b5035b237..a854cd2db 100644 --- a/internal/endpoints/socket.go +++ b/internal/endpoints/socket.go @@ -7,13 +7,13 @@ import ( "log/slog" "net" "net/http" + "net/url" "os" "os/user" "strconv" "time" "github.com/canonical/lxd/shared" - "github.com/canonical/lxd/shared/api" "github.com/canonical/microcluster/v3/internal/log" ) @@ -33,7 +33,7 @@ type Socket struct { } // NewSocket returns a Socket struct with no listener attached yet. -func NewSocket(ctx context.Context, server *http.Server, path api.URL, group string, drainConnTimeout time.Duration) *Socket { +func NewSocket(ctx context.Context, server *http.Server, path *url.URL, group string, drainConnTimeout time.Duration) *Socket { ctx, cancel := context.WithCancel(ctx) return &Socket{ Path: path.Hostname(), From 08379c062308f6ff6609c492921b8cdcbb48b130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Jan 2026 11:40:42 +0100 Subject: [PATCH 02/11] internal/trust: Use std library URL type 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 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/trust/remotes.go b/internal/trust/remotes.go index 3dd01c07d..fd1f6b750 100644 --- a/internal/trust/remotes.go +++ b/internal/trust/remotes.go @@ -4,6 +4,7 @@ import ( "crypto/x509" "errors" "fmt" + "net/url" "os" "path/filepath" "strings" @@ -252,8 +253,8 @@ func (r *Remotes) Addresses() map[string]types.AddrPort { func (r *Remotes) Cluster(isNotification bool, serverCert *shared.CertInfo, publicKey *x509.Certificate) (types.Clients, error) { cluster := make(types.Clients, 0, r.Count()-1) for _, addr := range r.Addresses() { - url := api.NewURL().Scheme("https").Host(addr.String()) - c, err := internalClient.New(*url, serverCert, publicKey, isNotification) + url := &api.NewURL().Scheme("https").Host(addr.String()).URL + c, err := internalClient.New(url, serverCert, publicKey, isNotification) if err != nil { return nil, err } @@ -340,6 +341,6 @@ func (r *Remotes) RemotesByName() map[string]Remote { } // URL returns the parsed URL of the Remote. -func (r *Remote) URL() api.URL { - return *api.NewURL().Scheme("https").Host(r.Address.String()) +func (r *Remote) URL() *url.URL { + return &api.NewURL().Scheme("https").Host(r.Address.String()).URL } From 8efef4c38dfd238362d840888f6b0ba8b6b7924c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Jan 2026 10:36:15 +0100 Subject: [PATCH 03/11] internal/db: Use std library URL type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/db/dqlite.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/internal/db/dqlite.go b/internal/db/dqlite.go index 0d9c277ba..6e58c3a28 100644 --- a/internal/db/dqlite.go +++ b/internal/db/dqlite.go @@ -39,7 +39,7 @@ type DqliteDB struct { memberName func() string // Local cluster member name clusterCert func() *shared.CertInfo // Cluster certificate for dqlite authentication. serverCert func() *shared.CertInfo // Server certificate for dqlite authentication. - listenAddr api.URL // Listen address for this dqlite node. + listenAddr *url.URL // Listen address for this dqlite node. dbName string // This is db.bin. os types.OS @@ -146,11 +146,11 @@ func (db *DqliteDB) isInitialized() (bool, error) { } // Bootstrap dqlite. -func (db *DqliteDB) Bootstrap(extensions extensions.Extensions, addr api.URL, clusterRecord cluster.CoreClusterMember) error { +func (db *DqliteDB) Bootstrap(extensions extensions.Extensions, addr *url.URL, clusterRecord cluster.CoreClusterMember) error { var err error db.listenAddr = addr db.dqlite, err = dqlite.New(db.os.DatabaseDir(), - dqlite.WithAddress(db.listenAddr.URL.Host), + dqlite.WithAddress(db.listenAddr.Host), dqlite.WithRolesAdjustmentFrequency(db.heartbeatInterval), dqlite.WithRolesAdjustmentHook(db.heartbeat), dqlite.WithConcurrentLeaderConns(&db.maxConns), @@ -196,14 +196,14 @@ func (db *DqliteDB) Bootstrap(extensions extensions.Extensions, addr api.URL, cl } // Join a dqlite cluster with the address of a member. -func (db *DqliteDB) Join(extensions extensions.Extensions, addr api.URL, joinAddresses ...string) error { +func (db *DqliteDB) Join(extensions extensions.Extensions, addr *url.URL, joinAddresses ...string) error { var err error db.listenAddr = addr db.dqlite, err = dqlite.New(db.os.DatabaseDir(), dqlite.WithCluster(joinAddresses), dqlite.WithRolesAdjustmentFrequency(db.heartbeatInterval), dqlite.WithRolesAdjustmentHook(db.heartbeat), - dqlite.WithAddress(db.listenAddr.URL.Host), + dqlite.WithAddress(db.listenAddr.Host), dqlite.WithConcurrentLeaderConns(&db.maxConns), dqlite.WithExternalConn(db.dialFunc(), db.acceptCh), dqlite.WithUnixSocket(os.Getenv(sys.DqliteSocket))) @@ -248,7 +248,7 @@ func (db *DqliteDB) Join(extensions extensions.Extensions, addr api.URL, joinAdd } // StartWithCluster starts up dqlite and joins the cluster. -func (db *DqliteDB) StartWithCluster(extensions extensions.Extensions, addr api.URL, clusterMembers map[string]types.AddrPort) error { +func (db *DqliteDB) StartWithCluster(extensions extensions.Extensions, addr *url.URL, clusterMembers map[string]types.AddrPort) error { allClusterAddrs := []string{} for _, clusterMemberAddrs := range clusterMembers { allClusterAddrs = append(allClusterAddrs, clusterMemberAddrs.String()) @@ -319,7 +319,7 @@ func (db *DqliteDB) IsOpen(ctx context.Context) error { } for _, member := range allMembers { - if member.Address == db.listenAddr.URL.Host { + if member.Address == db.listenAddr.Host { continue } @@ -385,15 +385,12 @@ func (db *DqliteDB) heartbeat(leaderInfo dqliteClient.NodeInfo, servers []dqlite return nil } - if leaderInfo.Address != db.listenAddr.URL.Host { + if leaderInfo.Address != db.listenAddr.Host { db.log().Debug("Not performing heartbeat, this system is not the dqlite leader", slog.String("address", db.listenAddr.String())) return nil } - url := api.NewURL() - url.URL = *db.os.ControlSocket() - - client, err := internalClient.New(*url, nil, nil, false) + client, err := internalClient.New(db.os.ControlSocket(), nil, nil, false) if err != nil { db.log().Error("Failed to get local client", slog.String("address", db.listenAddr.String()), slog.String("error", err.Error())) return nil From 446904e51a418d47817ef73962e5f507ab289bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Jan 2026 10:36:57 +0100 Subject: [PATCH 04/11] internal/daemon: Use std library URL type 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 | 38 ++++++++++++++++------------------ internal/daemon/daemon_test.go | 8 +++---- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 6cb39c33a..e4933371b 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -8,6 +8,7 @@ import ( "log/slog" "net" "net/http" + "net/url" "os" "path/filepath" "slices" @@ -334,7 +335,7 @@ func (d *Daemon) init(listenAddress string, socketGroup string, heartbeatInterva if listenAddress != "" { serverEndpoints = []rest.Resources{resources.PublicEndpoints} - err = d.addCoreServers(true, *listenAddr, d.ServerCert(), serverEndpoints) + err = d.addCoreServers(true, &listenAddr.URL, d.ServerCert(), serverEndpoints) if err != nil { return err } @@ -520,7 +521,7 @@ func (d *Daemon) setConfig(newConfig trust.Location) error { // StartAPI starts up the admin and consumer APIs, and generates a cluster cert // if we are bootstrapping the first node. func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[string]string, joinAddresses ...string) error { - if d.Address().URL.Host == "" || d.config.GetName() == "" { + if d.Address().Host == "" || d.config.GetName() == "" { return fmt.Errorf("Cannot start network API without valid daemon configuration") } @@ -529,7 +530,7 @@ func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[st return fmt.Errorf("Failed to parse server certificate when bootstrapping API: %w", err) } - addrPort, err := types.ParseAddrPort(d.Address().URL.Host) + addrPort, err := types.ParseAddrPort(d.Address().Host) if err != nil { return fmt.Errorf("Failed to parse listen address when bootstrapping API: %w", err) } @@ -553,7 +554,7 @@ func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[st // Validate the extension servers again now that we have applied addresses. d.extensionServersMu.RLock() - err = resources.ValidateEndpoints(d.extensionServers, d.Address().URL.Host) + err = resources.ValidateEndpoints(d.extensionServers, d.Address().Host) if err != nil { return err } @@ -568,13 +569,13 @@ func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[st } serverEndpoints := []rest.Resources{resources.InternalEndpoints, resources.PublicEndpoints} - err = d.addCoreServers(false, *d.Address(), d.ClusterCert(), serverEndpoints) + err = d.addCoreServers(false, d.Address(), d.ClusterCert(), serverEndpoints) if err != nil { return err } // Add extension servers before post-join hook. - err = d.addExtensionServers(false, d.ClusterCert(), d.Address().URL.Host) + err = d.addExtensionServers(false, d.ClusterCert(), d.Address().Host) if err != nil { return err } @@ -591,7 +592,7 @@ func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[st clusterMember.SchemaInternal, clusterMember.SchemaExternal, _ = d.db.Schema().Version() - err = d.db.Bootstrap(d.Extensions, *d.Address(), clusterMember) + err = d.db.Bootstrap(d.Extensions, d.Address(), clusterMember) if err != nil { return err } @@ -613,12 +614,12 @@ func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[st } if len(joinAddresses) != 0 { - err = d.db.Join(d.Extensions, *d.Address(), joinAddresses...) + err = d.db.Join(d.Extensions, d.Address(), joinAddresses...) if err != nil { 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().Addresses()) if err != nil { return fmt.Errorf("Failed to re-establish cluster connection: %w", err) } @@ -655,7 +656,7 @@ func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[st var clusterConfirmation bool err = clients.Query(d.shutdownCtx, true, func(ctx context.Context, c types.Client) error { // No need to send a request to ourselves. - if d.Address().URL.Host == c.URL().Host { + if d.Address().Host == c.URL().Host { return nil } @@ -695,7 +696,7 @@ func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[st c.SetClusterNotification() // No need to send a request to ourselves. - if d.Address().URL.Host == c.URL().Host { + if d.Address().Host == c.URL().Host { return nil } @@ -826,7 +827,7 @@ func (d *Daemon) UpdateServers() error { // Start any additional listener. // This operation is idempotent. - err := d.addExtensionServers(false, d.ClusterCert(), d.Address().URL.Host) + err := d.addExtensionServers(false, d.ClusterCert(), d.Address().Host) if err != nil { return err } @@ -838,10 +839,7 @@ func (d *Daemon) UpdateServers() error { func (d *Daemon) startUnixServer(serverEndpoints []rest.Resources, socketGroup string) error { ctlServer := d.initServer(serverEndpoints...) - url := api.NewURL() - url.URL = *d.os.ControlSocket() - - ctl := endpoints.NewSocket(d.shutdownCtx, ctlServer, *url, socketGroup, d.drainConnectionsTimeout) + ctl := endpoints.NewSocket(d.shutdownCtx, ctlServer, d.os.ControlSocket(), socketGroup, d.drainConnectionsTimeout) d.endpoints = endpoints.NewEndpoints(d.shutdownCtx, map[string]endpoints.Endpoint{ endpoints.EndpointsUnix: ctl, }) @@ -851,7 +849,7 @@ func (d *Daemon) startUnixServer(serverEndpoints []rest.Resources, socketGroup s // addCoreServers initializes the default resources with the default address and certificate. // If the default address and certificate may be applied to any extension servers, those will be started as well. -func (d *Daemon) addCoreServers(preInit bool, defaultURL api.URL, defaultCert *shared.CertInfo, defaultResources []rest.Resources) error { +func (d *Daemon) addCoreServers(preInit bool, defaultURL *url.URL, defaultCert *shared.CertInfo, defaultResources []rest.Resources) error { serverEndpoints := []rest.Resources{} serverEndpoints = append(serverEndpoints, defaultResources...) @@ -940,7 +938,7 @@ func (d *Daemon) addExtensionServers(preInit bool, fallbackCert *shared.CertInfo } server := d.initServer(extensionServer.Resources...) - network := endpoints.NewNetwork(d.shutdownCtx, endpoints.EndpointNetwork, server, *url, cert, extensionServer.DrainConnectionsTimeout) + network := endpoints.NewNetwork(d.shutdownCtx, endpoints.EndpointNetwork, server, &url.URL, cert, extensionServer.DrainConnectionsTimeout) networks[serverName] = network } @@ -1060,8 +1058,8 @@ func (d *Daemon) ServerCert() *shared.CertInfo { } // Address is the listen address for the daemon. -func (d *Daemon) Address() *api.URL { - return api.NewURL().Scheme("https").Host(d.config.GetAddress().String()) +func (d *Daemon) Address() *url.URL { + return &api.NewURL().Scheme("https").Host(d.config.GetAddress().String()).URL } // Name is this daemon's cluster member name. diff --git a/internal/daemon/daemon_test.go b/internal/daemon/daemon_test.go index 85c1b1570..054251f3c 100644 --- a/internal/daemon/daemon_test.go +++ b/internal/daemon/daemon_test.go @@ -228,14 +228,14 @@ func (t *daemonsSuite) Test_UpdateServers() { // Check if servers are up. for _, addr := range test.listeningOn { - url := api.NewURL().Scheme("https").Host(addr.String()) + url := &api.NewURL().Scheme("https").Host(addr.String()).URL // The remote server uses the cluster certificate. remoteCert, err := daemon.ClusterCert().PublicKeyX509() require.NoError(t.T(), err) // We also use the cluster certificate as a client certificate for this test. - client, err := client.New(*url, daemon.ClusterCert(), remoteCert, false) + client, err := client.New(url, daemon.ClusterCert(), remoteCert, false) require.NoError(t.T(), err) // Use embedded Get from Go's HTTP client. @@ -247,12 +247,12 @@ func (t *daemonsSuite) Test_UpdateServers() { // Check if servers are down. for _, addr := range test.notListeningOn { - url := api.NewURL().Scheme("https").Host(addr.String()) + url := &api.NewURL().Scheme("https").Host(addr.String()).URL remoteCert, err := daemon.ClusterCert().PublicKeyX509() require.NoError(t.T(), err) - client, err := client.New(*url, daemon.ClusterCert(), remoteCert, false) + client, err := client.New(url, daemon.ClusterCert(), remoteCert, false) require.NoError(t.T(), err) _, err = client.Get(url.String()) From 55848206bf46a3db1f5e0c7d50b275ad04f389d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Jan 2026 10:37:29 +0100 Subject: [PATCH 05/11] internal/rest/resources: Use std library URL type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/rest/resources/api_1.0.go | 2 +- internal/rest/resources/cluster.go | 14 ++++++-------- internal/rest/resources/daemon.go | 2 +- internal/rest/resources/heartbeat.go | 8 ++++---- internal/rest/resources/tokens.go | 4 ++-- internal/rest/resources/truststore.go | 4 ++-- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/internal/rest/resources/api_1.0.go b/internal/rest/resources/api_1.0.go index ab7346344..462b68423 100644 --- a/internal/rest/resources/api_1.0.go +++ b/internal/rest/resources/api_1.0.go @@ -17,7 +17,7 @@ var api10Cmd = rest.Endpoint{ } func api10Get(s state.State, r *http.Request) response.Response { - addrPort, err := types.ParseAddrPort(s.Address().URL.Host) + addrPort, err := types.ParseAddrPort(s.Address().Host) if err != nil { return response.SmartError(err) } diff --git a/internal/rest/resources/cluster.go b/internal/rest/resources/cluster.go index d6cfa3e55..a54c8e382 100644 --- a/internal/rest/resources/cluster.go +++ b/internal/rest/resources/cluster.go @@ -112,7 +112,7 @@ func clusterPost(s state.State, r *http.Request) response.Response { } // Forward request to leader. - if leaderInfo.Address != s.Address().URL.Host { + if leaderInfo.Address != s.Address().Host { client, err := s.Connect().Leader(false) if err != nil { return response.SmartError(err) @@ -301,8 +301,8 @@ func clusterGet(s state.State, r *http.Request) response.Response { } for i, clusterMember := range apiClusterMembers { - addr := api.NewURL().Scheme("https").Host(clusterMember.Address.String()) - d, err := internalClient.New(*addr, s.ServerCert(), clusterCert, false) + addr := &api.NewURL().Scheme("https").Host(clusterMember.Address.String()).URL + d, err := internalClient.New(addr, s.ServerCert(), clusterCert, false) if err != nil { return response.SmartError(fmt.Errorf("Failed to create HTTPS client for cluster member with address %q: %w", addr.String(), err)) } @@ -461,8 +461,8 @@ func clusterMemberDelete(s state.State, r *http.Request) response.Response { } // If we are not the leader, just forward the request. - if leaderInfo.Address != s.Address().URL.Host { - if allRemotes[name].Address.String() == s.Address().URL.Host { + if leaderInfo.Address != s.Address().Host { + if allRemotes[name].Address.String() == s.Address().Host { // If the member being removed is ourselves and we are not the leader, then lock the // clusterPutDisableMu before we forward the request to the leader, so that when the leader // goes on to request clusterPutDisable back to ourselves it won't be actioned until we @@ -674,9 +674,7 @@ func clusterMemberDelete(s state.State, r *http.Request) response.Response { return response.SmartError(err) } - remoteURL := remote.URL() - - client, err := s.Connect().Member(&remoteURL.URL, false, publicKey) + client, err := s.Connect().Member(remote.URL(), false, publicKey) if err != nil { return response.SmartError(err) } diff --git a/internal/rest/resources/daemon.go b/internal/rest/resources/daemon.go index edccb2b9f..353e8aa68 100644 --- a/internal/rest/resources/daemon.go +++ b/internal/rest/resources/daemon.go @@ -57,7 +57,7 @@ func daemonServersPut(s state.State, r *http.Request) response.Response { // Validate if there is an address conflict. // Initialize the list of active server addresses with the server's address. - var serverAddresses = []string{s.Address().URL.Host} + var serverAddresses = []string{s.Address().Host} for _, server := range req { serverAddress := server.Address.String() diff --git a/internal/rest/resources/heartbeat.go b/internal/rest/resources/heartbeat.go index 0dd3b7a10..338fd64fa 100644 --- a/internal/rest/resources/heartbeat.go +++ b/internal/rest/resources/heartbeat.go @@ -90,7 +90,7 @@ func heartbeatPost(s state.State, r *http.Request) response.Response { // beginHeartbeat initiates a heartbeat from the leader node to all other cluster members, if we haven't sent one out // recently. func beginHeartbeat(ctx context.Context, s state.State, hbReq types.HeartbeatInfo) response.Response { - if s.Address().URL.Host != hbReq.LeaderAddress { + if s.Address().Host != hbReq.LeaderAddress { return response.SmartError(fmt.Errorf("Attempt to initiate heartbeat from non-leader")) } @@ -154,7 +154,7 @@ func beginHeartbeat(ctx context.Context, s state.State, hbReq types.HeartbeatInf return response.SmartError(err) } - leaderEntry := clusterMap[s.Address().URL.Host] + leaderEntry := clusterMap[s.Address().Host] heartbeatInterval := time.Duration(intState.InternalDatabase.GetHeartbeatInterval()) timeSinceLast := time.Since(leaderEntry.LastHeartbeat) if timeSinceLast < heartbeatInterval { @@ -163,7 +163,7 @@ func beginHeartbeat(ctx context.Context, s state.State, hbReq types.HeartbeatInf return response.EmptySyncResponse } - logger.Debug("Beginning new heartbeat round", slog.String("address", s.Address().URL.Host)) + 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...) @@ -173,7 +173,7 @@ func beginHeartbeat(ctx context.Context, s state.State, hbReq types.HeartbeatInf // Set the time of the last heartbeat to now. leaderEntry.LastHeartbeat = time.Now() - clusterMap[s.Address().URL.Host] = leaderEntry + clusterMap[s.Address().Host] = leaderEntry // Record the maximum schema version discovered. hbInfo := types.HeartbeatInfo{ClusterMembers: clusterMap} diff --git a/internal/rest/resources/tokens.go b/internal/rest/resources/tokens.go index 85d6c51fc..a74cbce57 100644 --- a/internal/rest/resources/tokens.go +++ b/internal/rest/resources/tokens.go @@ -85,8 +85,8 @@ func tokensPost(state state.State, r *http.Request) response.Response { } if len(joinAddresses) == 0 { - logger.Warn(fmt.Sprintf("Failed to check trust store for eligible join addresses. Issuing token with join address %q", state.Address().URL.Host)) - joinAddresses, err = types.ParseAddrPorts([]string{state.Address().URL.Host}) + logger.Warn(fmt.Sprintf("Failed to check trust store for eligible join addresses. Issuing token with join address %q", state.Address().Host)) + joinAddresses, err = types.ParseAddrPorts([]string{state.Address().Host}) if err != nil { return response.SmartError(err) } diff --git a/internal/rest/resources/truststore.go b/internal/rest/resources/truststore.go index b578f952d..1b18d4312 100644 --- a/internal/rest/resources/truststore.go +++ b/internal/rest/resources/truststore.go @@ -72,7 +72,7 @@ func trustPost(s state.State, r *http.Request) response.Response { // We don't fail the entire operation if some nodes are unreachable. err = clients.Query(ctx, true, func(ctx context.Context, c types.Client) error { // No need to send a request to ourselves, or to the node we are adding. - if s.Address().URL.Host == c.URL().Host || req.Address.String() == c.URL().Host { + if s.Address().Host == c.URL().Host || req.Address.String() == c.URL().Host { return nil } @@ -138,7 +138,7 @@ func trustDelete(s state.State, r *http.Request) response.Response { err = clients.Query(ctx, true, func(ctx context.Context, c types.Client) error { // No need to send a request to ourselves, or to the node we are adding. - if s.Address().URL.Host == c.URL().Host || nodeToRemove.URL().URL.Host == c.URL().Host { + if s.Address().Host == c.URL().Host || nodeToRemove.URL().Host == c.URL().Host { return nil } From b1d7e0f8bfd8079aac70c9c5d245abc0b2f85396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Jan 2026 10:37:46 +0100 Subject: [PATCH 06/11] internal/rest: Use std library URL type 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 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/rest/rest.go b/internal/rest/rest.go index 9eaaa98cd..1496f56f9 100644 --- a/internal/rest/rest.go +++ b/internal/rest/rest.go @@ -91,14 +91,14 @@ func proxyTarget(action rest.EndpointAction, s state.State, r *http.Request) res return action.Handler(s, r) } - var targetURL *api.URL + var targetURL *url.URL err = s.Database().Transaction(r.Context(), func(ctx context.Context, tx *sql.Tx) error { clusterMember, err := cluster.GetCoreClusterMember(ctx, tx, target) if err != nil { return fmt.Errorf("Failed to get cluster member for request target name %q: %w", target, err) } - targetURL = api.NewURL().Scheme("https").Host(clusterMember.Address).Path(r.URL.Path) + targetURL = &api.NewURL().Scheme("https").Host(clusterMember.Address).Path(r.URL.Path).URL return nil }) @@ -111,16 +111,16 @@ func proxyTarget(action rest.EndpointAction, s state.State, r *http.Request) res return response.InternalError(fmt.Errorf("Failed to parse cluster certificate for request: %w", err)) } - client, err := client.New(*targetURL, s.ServerCert(), clusterCert, false) + client, err := client.New(targetURL, s.ServerCert(), clusterCert, false) if err != nil { - return response.InternalError(fmt.Errorf("Failed to get a client for the target %q at address %q: %w", target, targetURL.URL.Host, err)) + return response.InternalError(fmt.Errorf("Failed to get a client for the target %q at address %q: %w", target, targetURL.Host, err)) } // Update request URL. r.RequestURI = "" - r.URL.Scheme = targetURL.URL.Scheme - r.URL.Host = targetURL.URL.Host - r.Host = targetURL.URL.Host + r.URL.Scheme = targetURL.Scheme + r.URL.Host = targetURL.Host + r.Host = targetURL.Host logger.Info("Forwarding request to specified target", slog.String("source", s.Name()), slog.String("target", target)) @@ -141,7 +141,7 @@ func proxyTarget(action rest.EndpointAction, s state.State, r *http.Request) res // Use the actual request URL to retain query parameters. connToTarget, err := client.RawWebsocket(r.Context(), "", r.URL) if err != nil { - return fmt.Errorf("Failed to upgrade connection for the target %q at address %q to websocket: %w", target, targetURL.URL.Host, err) + return fmt.Errorf("Failed to upgrade connection for the target %q at address %q to websocket: %w", target, targetURL.Host, err) } // Close connection to target when the proxy returns. @@ -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().URL.Host, state.Remotes().CertificatesNative()) + trusted, err := access.Authenticate(state, r, state.Address().Host, state.Remotes().CertificatesNative()) if err != nil && !errors.As(err, &access.ErrInvalidHost{}) { resp = response.Forbidden(fmt.Errorf("Failed to authenticate request: %w", err)) } else { From 79337642247180f41958e71865ce5ffa56afba4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Jan 2026 10:38:17 +0100 Subject: [PATCH 07/11] internal/state: Use std library URL type 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 | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/internal/state/state.go b/internal/state/state.go index 20ed93e4f..dca9dd691 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -31,7 +31,7 @@ type State interface { FileSystem() types.OS // Listen Address. - Address() *api.URL + Address() *url.URL // Name of the cluster member. Name() string @@ -103,7 +103,7 @@ type InternalState struct { Hooks *Hooks InternalFileSystem func() types.OS - InternalAddress func() *api.URL + InternalAddress func() *url.URL InternalName func() string InternalVersion func() string InternalServerCert func() *shared.CertInfo @@ -119,7 +119,7 @@ func (s *InternalState) FileSystem() types.OS { } // Address returns the core microcluster listen address. -func (s *InternalState) Address() *api.URL { +func (s *InternalState) Address() *url.URL { return s.InternalAddress() } @@ -191,7 +191,7 @@ func (s *InternalState) Cluster(isNotification bool) (types.Clients, error) { // Filter out ourselves from the client list clients := make(types.Clients, 0, len(allClients)-1) for _, client := range allClients { - if s.Address().URL.Host != client.URL().Host { + if s.Address().Host != client.URL().Host { clients = append(clients, client) } } @@ -224,8 +224,8 @@ func (s *InternalState) Leader(isNotification bool) (types.Client, error) { return nil, err } - url := api.NewURL().Scheme("https").Host(leaderInfo.Address) - c, err := internalClient.New(*url, s.ServerCert(), publicKey, isNotification) + url := &api.NewURL().Scheme("https").Host(leaderInfo.Address).URL + c, err := internalClient.New(url, s.ServerCert(), publicKey, isNotification) if err != nil { return nil, err } @@ -246,10 +246,7 @@ func (s *InternalState) Member(url *url.URL, isNotification bool, cert *x509.Cer } } - apiURL := api.NewURL() - apiURL.URL = *url - - c, err := internalClient.New(*apiURL, s.ServerCert(), cert, isNotification) + c, err := internalClient.New(url, s.ServerCert(), cert, isNotification) if err != nil { return nil, err } From 8034bc35a7203345d3fc3f8effa9296faebd8d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Jan 2026 10:38:35 +0100 Subject: [PATCH 08/11] internal/db: Use std library URL type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/db/db_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/db/db_test.go b/internal/db/db_test.go index b12a754d6..6b2561dde 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -657,7 +657,7 @@ func NewTestDB(extensionsExternal []clusterDB.Update) (*DqliteDB, error) { db := &DqliteDB{ ctx: ctx, memberName: func() string { return fmt.Sprintf("cluster-member-%d", 0) }, - listenAddr: *api.NewURL().Host("10.0.0.0:8443"), + listenAddr: &api.NewURL().Host("10.0.0.0:8443").URL, upgradeCh: make(chan struct{}, 1), os: &sys.OS{}, } From 11f3d5d36631ae4bd7772f7f9f4bca1bdff8a358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Jan 2026 10:38:57 +0100 Subject: [PATCH 09/11] example/api: Use std library URL type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- example/api/extended.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example/api/extended.go b/example/api/extended.go index a62017e36..1cfd3aead 100644 --- a/example/api/extended.go +++ b/example/api/extended.go @@ -51,9 +51,9 @@ func cmdSimple(state state.State, r *http.Request) response.Response { messages := make([]string, 0, len(clients)) err = clients.Query(r.Context(), true, func(ctx context.Context, c types.Client) error { - addrPort, err := types.ParseAddrPort(state.Address().URL.Host) + addrPort, err := types.ParseAddrPort(state.Address().Host) if err != nil { - return fmt.Errorf("Failed to parse addr:port of listen address %q: %w", state.Address().URL.Host, err) + return fmt.Errorf("Failed to parse addr:port of listen address %q: %w", state.Address().Host, err) } // Our payload in this case is defined by us as ExtendedType. @@ -94,7 +94,7 @@ func cmdSimple(state state.State, r *http.Request) response.Response { } // Return some identifying information. - message := fmt.Sprintf("cluster member at address %q received message %q from cluster member at address %q", state.Address().URL.Host, info.Message, info.Sender.String()) + message := fmt.Sprintf("cluster member at address %q received message %q from cluster member at address %q", state.Address().Host, info.Message, info.Sender.String()) return response.SyncResponse(true, message) } @@ -117,7 +117,7 @@ func cmdWebsocket(state state.State, r *http.Request) response.Response { defer conn.Close() for i := range 3 { - text := fmt.Sprintf("Testing from %q, iteration %d ...", state.Address().URL.Host, i+1) + text := fmt.Sprintf("Testing from %q, iteration %d ...", state.Address().Host, i+1) err := conn.WriteMessage(websocket.TextMessage, []byte(text)) if err != nil { return err From ce482f7ea7068cab10fc7773d95ab15bbf4c18fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Jan 2026 11:39:41 +0100 Subject: [PATCH 10/11] internal/rest/client: Use std library URL type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- internal/rest/client/client.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/rest/client/client.go b/internal/rest/client/client.go index fa12727b9..1538008e1 100644 --- a/internal/rest/client/client.go +++ b/internal/rest/client/client.go @@ -28,7 +28,7 @@ import ( // Client is a rest client for the daemon. type Client struct { *http.Client - url api.URL + url *url.URL } // CtxKey is the type used for all fields stored in the request context by Microcluster. @@ -40,14 +40,14 @@ const ( ) // New returns a new client configured with the given url and certificates. -func New(url api.URL, clientCert *shared.CertInfo, remoteCert *x509.Certificate, forwarding bool) (*Client, error) { +func New(url *url.URL, clientCert *shared.CertInfo, remoteCert *x509.Certificate, forwarding bool) (*Client, error) { var err error var httpClient *http.Client // If the url is an absolute path to the control.socket, return a client to the local unix socket. if strings.HasSuffix(url.String(), "control.socket") && path.IsAbs(url.Hostname()) { httpClient, err = unixHTTPClient(shared.HostPath(url.Hostname())) - url.Host(filepath.Base(url.Hostname())) + url.Host = filepath.Base(url.Hostname()) } else { proxy := shared.ProxyFromEnvironment if forwarding { @@ -274,8 +274,8 @@ func (c *Client) mergeURL(endpointType types.EndpointPrefix, endpoint *url.URL) localURL = &newURL } - localURL.Host = c.url.URL.Host - localURL.Scheme = c.url.URL.Scheme + localURL.Host = c.url.Host + localURL.Scheme = c.url.Scheme localURL.Path = filepath.Join("/", string(endpointType), localURL.Path) localURL.RawPath = filepath.Join("/", string(endpointType), localURL.RawPath) @@ -356,7 +356,7 @@ func (c *Client) RawWebsocket(ctx context.Context, endpointType types.EndpointPr localURL := c.mergeURL(endpointType, endpoint) // Pick the right scheme based on the client configuration. - if c.url.URL.Scheme == "http" { + if c.url.Scheme == "http" { localURL.Scheme = "ws" } else { localURL.Scheme = "wss" @@ -403,20 +403,20 @@ func (c *Client) RawWebsocket(ctx context.Context, endpointType types.EndpointPr // URL returns the address used for the client. func (c *Client) URL() *url.URL { - return &c.url.URL + return c.url } // UseTarget returns a new client with the query "?target=name" set. func (c *Client) UseTarget(name string) types.Client { localURL := api.NewURL() - localURL.URL.Host = c.url.URL.Host - localURL.URL.Scheme = c.url.URL.Scheme - localURL.URL.Path = c.url.URL.Path + localURL.URL.Host = c.url.Host + localURL.URL.Scheme = c.url.Scheme + localURL.URL.Path = c.url.Path localURL.RawQuery = c.url.RawQuery localURL = localURL.WithQuery("target", name) return &Client{ Client: c.Client, - url: *localURL, + url: &localURL.URL, } } From b5489769521d49060550a3afe7ea5e4fde3779c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Jan 2026 11:41:06 +0100 Subject: [PATCH 11/11] microcluster: Use std library URL type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- microcluster/app.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/microcluster/app.go b/microcluster/app.go index 540d4ed8b..f5f54463f 100644 --- a/microcluster/app.go +++ b/microcluster/app.go @@ -336,10 +336,7 @@ func (m *MicroCluster) RemoveClusterMember(ctx context.Context, name string, for func (m *MicroCluster) LocalClient() (types.Client, error) { c := m.args.Client if c == nil { - url := api.NewURL() - url.URL = *m.FileSystem.ControlSocket() - - internalClient, err := internalClient.New(*url, nil, nil, false) + internalClient, err := internalClient.New(m.FileSystem.ControlSocket(), nil, nil, false) if err != nil { return nil, err } @@ -385,8 +382,8 @@ func (m *MicroCluster) RemoteClientWithCert(address string, cert *x509.Certifica return nil, err } - url := api.NewURL().Scheme("https").Host(address) - internalClient, err := internalClient.New(*url, serverCert, cert, false) + url := &api.NewURL().Scheme("https").Host(address).URL + internalClient, err := internalClient.New(url, serverCert, cert, false) if err != nil { return nil, err }