diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 15fa61f3..f490ef38 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -81,7 +81,7 @@ type Daemon struct { config *internalConfig.DaemonConfig // Local daemon's configuration from daemon.yaml file. - os *sys.OS + os types.OS serverCert *shared.CertInfo clusterMu sync.RWMutex @@ -186,7 +186,7 @@ func (d *Daemon) Run(ctx context.Context, stateDir string, args Args) error { d.drainConnectionsTimeout = args.DrainConnectionsTimeout // Setup the deamon's internal config. - d.config = internalConfig.NewDaemonConfig(filepath.Join(d.os.StateDir, "daemon.yaml")) + d.config = internalConfig.NewDaemonConfig(filepath.Join(d.os.StateDir(), "daemon.yaml")) // Clean up the daemon state on an error during init. reverter := revert.New() @@ -446,12 +446,12 @@ func (d *Daemon) reload() error { func (d *Daemon) initStore() error { var err error - d.fsWatcher, err = sys.NewWatcher(d.shutdownCtx, d.os.StateDir) + d.fsWatcher, err = sys.NewWatcher(d.shutdownCtx, d.os.StateDir()) if err != nil { return err } - d.trustStore, err = trust.Init(d.fsWatcher, nil, d.os.TrustDir) + d.trustStore, err = trust.Init(d.fsWatcher, nil, d.os.TrustDir()) if err != nil { return err } @@ -541,7 +541,7 @@ func (d *Daemon) StartAPI(ctx context.Context, bootstrap bool, initConfig map[st } if bootstrap { - err = d.trustStore.Remotes().Add(d.os.TrustDir, localNode) + err = d.trustStore.Remotes().Add(d.os.TrustDir(), localNode) if err != nil { return fmt.Errorf("Failed to initialize local remote entry: %w", err) } @@ -838,7 +838,11 @@ func (d *Daemon) UpdateServers() error { // startUnixServer starts up the core unix listener with the given resources. func (d *Daemon) startUnixServer(serverEndpoints []rest.Resources, socketGroup string) error { ctlServer := d.initServer(serverEndpoints...) - ctl := endpoints.NewSocket(d.shutdownCtx, ctlServer, d.os.ControlSocket(), socketGroup, d.drainConnectionsTimeout) + + url := api.NewURL() + url.URL = *d.os.ControlSocket() + + ctl := endpoints.NewSocket(d.shutdownCtx, ctlServer, *url, socketGroup, d.drainConnectionsTimeout) d.endpoints = endpoints.NewEndpoints(d.shutdownCtx, map[string]endpoints.Endpoint{ endpoints.EndpointsUnix: ctl, }) @@ -920,7 +924,7 @@ func (d *Daemon) addExtensionServers(preInit bool, fallbackCert *shared.CertInfo continue } - customCertExists := shared.PathExists(filepath.Join(d.os.CertificatesDir, fmt.Sprintf("%s.crt", serverName))) + customCertExists := shared.PathExists(filepath.Join(d.os.CertificatesDir(), fmt.Sprintf("%s.crt", serverName))) var err error var cert *shared.CertInfo @@ -930,7 +934,7 @@ func (d *Daemon) addExtensionServers(preInit bool, fallbackCert *shared.CertInfo } else { // Generate a dedicated certificate or load the custom one if it exists. // When updating the additional listeners the dedicated certificate from before will be reused. - cert, err = shared.KeyPairAndCA(d.os.CertificatesDir, serverName, shared.CertServer, shared.CertOptions{AddHosts: true, CommonName: serverName}) + cert, err = shared.KeyPairAndCA(d.os.CertificatesDir(), serverName, shared.CertServer, shared.CertOptions{AddHosts: true, CommonName: serverName}) if err != nil { return fmt.Errorf("Failed to setup dedicated certificate for additional server %q: %w", serverName, err) } @@ -1000,9 +1004,9 @@ func (d *Daemon) ReloadCert(name types.CertificateName) error { var dir string if name == types.ClusterCertificateName || name == types.ServerCertificateName { - dir = d.os.StateDir + dir = d.os.StateDir() } else { - dir = d.os.CertificatesDir + dir = d.os.CertificatesDir() } cert, err := shared.KeyPairAndCA(dir, string(name), shared.CertServer, shared.CertOptions{AddHosts: true, CommonName: d.Name()}) @@ -1035,7 +1039,7 @@ func (d *Daemon) ReloadCert(name types.CertificateName) error { // - and cannot load a custom certificate which shares their name d.extensionServersMu.RLock() for name, server := range d.extensionServers { - certExists := shared.PathExists(filepath.Join(d.os.CertificatesDir, fmt.Sprintf("%s.crt", name))) + certExists := shared.PathExists(filepath.Join(d.os.CertificatesDir(), fmt.Sprintf("%s.crt", name))) if !server.CoreAPI && !server.DedicatedCertificate && !certExists { d.endpoints.UpdateTLSByName(name, cert) } @@ -1099,8 +1103,8 @@ func (d *Daemon) ExtensionServers() []string { } // FileSystem returns the filesystem structure for the daemon. -func (d *Daemon) FileSystem() *sys.OS { - copyOS := *d.os +func (d *Daemon) FileSystem() types.OS { + copyOS := *(d.os.(*sys.OS)) return ©OS } diff --git a/internal/db/db.go b/internal/db/db.go index faf3ea6e..3804bff9 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -145,7 +145,7 @@ func (db *DqliteDB) waitUpgrade(bootstrap bool, ext extensions.Extensions) error otherNodesBehind := false newSchema := db.Schema() - newSchema.File(path.Join(db.os.StateDir, "patch.global.sql")) + newSchema.File(path.Join(db.os.StateDir(), "patch.global.sql")) if !bootstrap { checkVersions := func(ctx context.Context, current int, tx *sql.Tx) error { diff --git a/internal/db/dqlite.go b/internal/db/dqlite.go index 44c8c832..231030f1 100644 --- a/internal/db/dqlite.go +++ b/internal/db/dqlite.go @@ -42,7 +42,7 @@ type DqliteDB struct { listenAddr api.URL // Listen address for this dqlite node. dbName string // This is db.bin. - os *sys.OS + os types.OS db *sql.DB dqlite *dqlite.App @@ -73,7 +73,7 @@ func (db *DqliteDB) Accept(conn net.Conn) { } // NewDB creates an empty db struct with no dqlite connection. -func NewDB(ctx context.Context, serverCert func() *shared.CertInfo, clusterCert func() *shared.CertInfo, memberName func() string, os *sys.OS, heartbeatInterval time.Duration) (*DqliteDB, error) { +func NewDB(ctx context.Context, serverCert func() *shared.CertInfo, clusterCert func() *shared.CertInfo, memberName func() string, os types.OS, heartbeatInterval time.Duration) (*DqliteDB, error) { shutdownCtx, shutdownCancel := context.WithCancel(ctx) if heartbeatInterval == 0 { @@ -133,7 +133,7 @@ func (db *DqliteDB) SchemaVersion() (versionInternal uint64, versionExternal uin // isInitialized determines whether the database has been bootstrapped or joined to a cluster. // This is an internal helper function; external callers should use Status() instead. func (db *DqliteDB) isInitialized() (bool, error) { - _, err := os.Stat(filepath.Join(db.os.DatabaseDir, "info.yaml")) + _, err := os.Stat(filepath.Join(db.os.DatabaseDir(), "info.yaml")) if err != nil { if os.IsNotExist(err) { return false, nil @@ -149,7 +149,7 @@ func (db *DqliteDB) isInitialized() (bool, error) { func (db *DqliteDB) Bootstrap(extensions extensions.Extensions, addr api.URL, clusterRecord cluster.CoreClusterMember) error { var err error db.listenAddr = addr - db.dqlite, err = dqlite.New(db.os.DatabaseDir, + db.dqlite, err = dqlite.New(db.os.DatabaseDir(), dqlite.WithAddress(db.listenAddr.URL.Host), dqlite.WithRolesAdjustmentFrequency(db.heartbeatInterval), dqlite.WithRolesAdjustmentHook(db.heartbeat), @@ -183,7 +183,7 @@ func (db *DqliteDB) Bootstrap(extensions extensions.Extensions, addr api.URL, cl func (db *DqliteDB) Join(extensions extensions.Extensions, addr api.URL, joinAddresses ...string) error { var err error db.listenAddr = addr - db.dqlite, err = dqlite.New(db.os.DatabaseDir, + db.dqlite, err = dqlite.New(db.os.DatabaseDir(), dqlite.WithCluster(joinAddresses), dqlite.WithRolesAdjustmentFrequency(db.heartbeatInterval), dqlite.WithRolesAdjustmentHook(db.heartbeat), @@ -357,7 +357,10 @@ func (db *DqliteDB) heartbeat(leaderInfo dqliteClient.NodeInfo, servers []dqlite return nil } - client, err := internalClient.New(db.os.ControlSocket(), nil, nil, false) + url := api.NewURL() + url.URL = *db.os.ControlSocket() + + client, err := internalClient.New(*url, 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 diff --git a/internal/recover/recover.go b/internal/recover/recover.go index 3e7ba937..f97d6d6a 100644 --- a/internal/recover/recover.go +++ b/internal/recover/recover.go @@ -24,15 +24,14 @@ import ( "github.com/canonical/microcluster/v3/client" "github.com/canonical/microcluster/v3/internal/config" "github.com/canonical/microcluster/v3/internal/log" - "github.com/canonical/microcluster/v3/internal/sys" "github.com/canonical/microcluster/v3/internal/trust" "github.com/canonical/microcluster/v3/microcluster/types" ) // GetDqliteClusterMembers parses the trust store and // path.Join(filesystem.DatabaseDir, "cluster.yaml"). -func GetDqliteClusterMembers(filesystem *sys.OS) ([]types.DqliteMember, error) { - storePath := path.Join(filesystem.DatabaseDir, "cluster.yaml") +func GetDqliteClusterMembers(filesystem types.OS) ([]types.DqliteMember, error) { + storePath := path.Join(filesystem.DatabaseDir(), "cluster.yaml") var nodeInfo []dqlite.NodeInfo err := readYaml(storePath, &nodeInfo) @@ -40,7 +39,7 @@ func GetDqliteClusterMembers(filesystem *sys.OS) ([]types.DqliteMember, error) { return nil, err } - remotes, err := readTrustStore(filesystem.TrustDir) + remotes, err := readTrustStore(filesystem.TrustDir()) if err != nil { return nil, err } @@ -68,7 +67,7 @@ func GetDqliteClusterMembers(filesystem *sys.OS) ([]types.DqliteMember, error) { // files, modifies the daemon and trust store, and writes a recovery tarball. // It does not check members to ensure that the new configuration is valid; use // ValidateMemberChanges to ensure that the inputs to this function are correct. -func RecoverFromQuorumLoss(ctx context.Context, filesystem *sys.OS, members []types.DqliteMember) (string, error) { +func RecoverFromQuorumLoss(ctx context.Context, filesystem types.OS, members []types.DqliteMember) (string, error) { // Set up our new cluster configuration nodeInfo := make([]dqlite.NodeInfo, 0, len(members)) for _, member := range members { @@ -93,7 +92,7 @@ func RecoverFromQuorumLoss(ctx context.Context, filesystem *sys.OS, members []ty // Check each cluster member's /1.0 to ensure that they are unreachable. // This is a sanity check to ensure that we're not reconfiguring a cluster // that's still partially up. - remotes, err := readTrustStore(filesystem.TrustDir) + remotes, err := readTrustStore(filesystem.TrustDir()) if err != nil { return "", err } @@ -138,13 +137,13 @@ func RecoverFromQuorumLoss(ctx context.Context, filesystem *sys.OS, members []ty return "", err } - err = dqlite.ReconfigureMembershipExt(filesystem.DatabaseDir, nodeInfo) + err = dqlite.ReconfigureMembershipExt(filesystem.DatabaseDir(), nodeInfo) if err != nil { return "", fmt.Errorf("Dqlite recovery: %w", err) } // Update local info.yaml with our new address - localInfoYamlPath := path.Join(filesystem.DatabaseDir, "info.yaml") + localInfoYamlPath := path.Join(filesystem.DatabaseDir(), "info.yaml") var localInfo dqlite.NodeInfo err = readYaml(localInfoYamlPath, &localInfo) @@ -164,7 +163,7 @@ func RecoverFromQuorumLoss(ctx context.Context, filesystem *sys.OS, members []ty return "", err } - err = writeDqliteClusterYaml(path.Join(filesystem.DatabaseDir, "cluster.yaml"), members) + err = writeDqliteClusterYaml(path.Join(filesystem.DatabaseDir(), "cluster.yaml"), members) if err != nil { return "", err } @@ -180,7 +179,7 @@ func RecoverFromQuorumLoss(ctx context.Context, filesystem *sys.OS, members []ty return recoveryTarballPath, err } - err = updateTrustStore(filesystem.TrustDir, members) + err = updateTrustStore(filesystem.TrustDir(), members) if err != nil { return recoveryTarballPath, fmt.Errorf("Failed to update trust store: %w", err) } @@ -235,13 +234,13 @@ func writeDqliteClusterYaml(path string, members []types.DqliteMember) error { return writeYaml(path, &nodeInfo) } -func updateDaemonAddress(filesystem *sys.OS, address string) error { +func updateDaemonAddress(filesystem types.OS, address string) error { newAddress, err := types.ParseAddrPort(address) if err != nil { return fmt.Errorf("Failed to update daemon.yaml: %w", err) } - daemonConfig := config.NewDaemonConfig(path.Join(filesystem.StateDir, "daemon.yaml")) + daemonConfig := config.NewDaemonConfig(path.Join(filesystem.StateDir(), "daemon.yaml")) err = daemonConfig.Load() if err != nil { return fmt.Errorf("Failed to load daemon.yaml: %w", err) @@ -350,14 +349,14 @@ func ValidateMemberChanges(oldMembers []types.DqliteMember, newMembers []types.D return nil } -func writeGlobalMembersPatch(filesystem *sys.OS, members []types.DqliteMember) error { +func writeGlobalMembersPatch(filesystem types.OS, members []types.DqliteMember) error { sql := "" for _, member := range members { sql += fmt.Sprintf("UPDATE core_cluster_members SET address = %q WHERE name = %q;\n", member.Address, member.Name) } if len(sql) > 0 { - patchPath := path.Join(filesystem.StateDir, "patch.global.sql") + patchPath := path.Join(filesystem.StateDir(), "patch.global.sql") patchFile, err := os.OpenFile(patchPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err @@ -384,9 +383,9 @@ func writeGlobalMembersPatch(filesystem *sys.OS, members []types.DqliteMember) e // go-dqlite's info.yaml is excluded from the tarball. // The new cluster configuration is included as `recovery.yaml`. // This function returns the path to the tarball. -func createRecoveryTarball(ctx context.Context, filesystem *sys.OS, members []types.DqliteMember) (string, error) { - tarballPath := path.Join(filesystem.StateDir, "recovery_db.tar.gz") - recoveryYamlPath := path.Join(filesystem.DatabaseDir, "recovery.yaml") +func createRecoveryTarball(ctx context.Context, filesystem types.OS, members []types.DqliteMember) (string, error) { + tarballPath := path.Join(filesystem.StateDir(), "recovery_db.tar.gz") + recoveryYamlPath := path.Join(filesystem.DatabaseDir(), "recovery.yaml") err := writeYaml(recoveryYamlPath, members) if err != nil { @@ -396,7 +395,7 @@ func createRecoveryTarball(ctx context.Context, filesystem *sys.OS, members []ty // info.yaml is used by go-dqlite to keep track of the current cluster member's // ID and address. We shouldn't replicate the recovery member's info.yaml // to all other members, so exclude it from the tarball: - err = createTarball(ctx, tarballPath, filesystem.DatabaseDir, ".", []string{"info.yaml"}) + err = createTarball(ctx, tarballPath, filesystem.DatabaseDir(), ".", []string{"info.yaml"}) return tarballPath, err } @@ -405,9 +404,9 @@ func createRecoveryTarball(ctx context.Context, filesystem *sys.OS, members []ty // fiesystem.StateDir. If it exists, unpack it into a temporary directory, // ensure that it is a valid microcluster recovery tarball, and replace the // existing filesystem.DatabaseDir. -func MaybeUnpackRecoveryTarball(ctx context.Context, filesystem *sys.OS) error { - tarballPath := path.Join(filesystem.StateDir, "recovery_db.tar.gz") - unpackDir := path.Join(filesystem.StateDir, "recovery_db") +func MaybeUnpackRecoveryTarball(ctx context.Context, filesystem types.OS) error { + tarballPath := path.Join(filesystem.StateDir(), "recovery_db.tar.gz") + unpackDir := path.Join(filesystem.StateDir(), "recovery_db") recoveryYamlPath := path.Join(unpackDir, "recovery.yaml") // Determine if the recovery tarball exists @@ -430,7 +429,7 @@ func MaybeUnpackRecoveryTarball(ctx context.Context, filesystem *sys.OS) error { // We need to set the local info.yaml address with the (possibly changed) // incoming address for this member. - localInfoYamlPath := path.Join(filesystem.DatabaseDir, "info.yaml") + localInfoYamlPath := path.Join(filesystem.DatabaseDir(), "info.yaml") recoveryInfoYamlPath := path.Join(unpackDir, "info.yaml") var localInfo dqlite.NodeInfo @@ -465,7 +464,7 @@ func MaybeUnpackRecoveryTarball(ctx context.Context, filesystem *sys.OS) error { } // Update the local trust store with the incoming cluster configuration - err = updateTrustStore(filesystem.TrustDir, incomingMembers) + err = updateTrustStore(filesystem.TrustDir(), incomingMembers) if err != nil { return err } @@ -477,7 +476,7 @@ func MaybeUnpackRecoveryTarball(ctx context.Context, filesystem *sys.OS) error { // Now that we're as sure as we can be that the recovery DB is valid, we can // replace the existing DB - err = os.RemoveAll(filesystem.DatabaseDir) + err = os.RemoveAll(filesystem.DatabaseDir()) if err != nil { return err } @@ -487,7 +486,7 @@ func MaybeUnpackRecoveryTarball(ctx context.Context, filesystem *sys.OS) error { return err } - err = os.Rename(unpackDir, filesystem.DatabaseDir) + err = os.Rename(unpackDir, filesystem.DatabaseDir()) if err != nil { return err } @@ -510,13 +509,13 @@ func MaybeUnpackRecoveryTarball(ctx context.Context, filesystem *sys.OS) error { // CreateDatabaseBackup writes a tarball of filesystem.DatabaseDir to // filesystem.StateDir as db_backup.TIMESTAMP.tar.gz. It does not check to // to ensure that the database is stopped. -func CreateDatabaseBackup(ctx context.Context, filesystem *sys.OS) error { +func CreateDatabaseBackup(ctx context.Context, filesystem types.OS) error { // tar interprets `:` as a remote drive; ISO8601 allows a 'basic format' // with the colons omitted (as opposed to time.RFC3339) // https://en.wikipedia.org/wiki/ISO_8601 backupFileName := fmt.Sprintf("db_backup.%s.tar.gz", time.Now().Format("2006-01-02T150405Z0700")) - backupFilePath := path.Join(filesystem.StateDir, backupFileName) + backupFilePath := path.Join(filesystem.StateDir(), backupFileName) logger, err := log.LoggerFromContext(ctx) if err != nil { @@ -527,13 +526,13 @@ func CreateDatabaseBackup(ctx context.Context, filesystem *sys.OS) error { // For DB backups the tarball should contain the subdirs (usually `database/`) // so that the user can easily untar the backup from the state dir. - rootDir := filesystem.StateDir - walkDir, err := filepath.Rel(filesystem.StateDir, filesystem.DatabaseDir) + rootDir := filesystem.StateDir() + walkDir, err := filepath.Rel(filesystem.StateDir(), filesystem.DatabaseDir()) // Don't bother if DatabaseDir is not inside StateDir if err != nil { - logger.Warn("DB backup: DatabaseDir (%q) not in StateDir (%q)", slog.String("databaseDir", filesystem.DatabaseDir), slog.String("stateDir", filesystem.StateDir)) - rootDir = filesystem.DatabaseDir + logger.Warn("DB backup: DatabaseDir (%q) not in StateDir (%q)", slog.String("databaseDir", filesystem.DatabaseDir()), slog.String("stateDir", filesystem.StateDir())) + rootDir = filesystem.DatabaseDir() walkDir = "." } diff --git a/internal/rest/resources/certificates.go b/internal/rest/resources/certificates.go index e16ff2dd..0afaf3f6 100644 --- a/internal/rest/resources/certificates.go +++ b/internal/rest/resources/certificates.go @@ -87,14 +87,14 @@ func clusterCertificatesPut(s state.State, r *http.Request) response.Response { var certificateDir string if certificateName == string(types.ClusterCertificateName) { - certificateDir = s.FileSystem().StateDir + certificateDir = s.FileSystem().StateDir() } else if certificateName == string(types.ServerCertificateName) { - certificateDir = s.FileSystem().StateDir + certificateDir = s.FileSystem().StateDir() if s.Database().Status() != types.DatabaseNotReady { return response.SmartError(fmt.Errorf("Cannot replace server certificate after initialization")) } } else { - certificateDir = s.FileSystem().CertificatesDir + certificateDir = s.FileSystem().CertificatesDir() // Check if an additional listener exists for that name. // We cannot query the daemon's config of the additional listeners as diff --git a/internal/rest/resources/cluster.go b/internal/rest/resources/cluster.go index f6c2a4c1..dce31bb3 100644 --- a/internal/rest/resources/cluster.go +++ b/internal/rest/resources/cluster.go @@ -195,7 +195,7 @@ func clusterPost(s state.State, r *http.Request) response.Response { } // Add the cluster member to our local store for authentication. - err = s.Remotes().Add(s.FileSystem().TrustDir, newRemote) + err = s.Remotes().Add(s.FileSystem().TrustDir(), newRemote) if err != nil { return response.SmartError(err) } @@ -203,7 +203,7 @@ func clusterPost(s state.State, r *http.Request) response.Response { tokenResponse.ClusterAdditionalCerts = make(map[string]types.KeyPair) // Load the list of custom certificates from its state directory. - err = filepath.WalkDir(s.FileSystem().CertificatesDir, func(path string, d fs.DirEntry, err error) error { + err = filepath.WalkDir(s.FileSystem().CertificatesDir(), func(path string, d fs.DirEntry, err error) error { // Skip directories if d.IsDir() { return nil @@ -213,7 +213,7 @@ func clusterPost(s state.State, r *http.Request) response.Response { splittedPath := strings.Split(filepath.Base(path), ".") if len(splittedPath) == 2 && splittedPath[1] == "crt" { // Load the certificate - cert, err := shared.KeyPairAndCA(s.FileSystem().CertificatesDir, splittedPath[0], shared.CertServer, shared.CertOptions{}) + cert, err := shared.KeyPairAndCA(s.FileSystem().CertificatesDir(), splittedPath[0], shared.CertServer, shared.CertOptions{}) if err != nil { return fmt.Errorf("Failed to load certificate for additional server %q: %w", splittedPath[0], err) } @@ -383,7 +383,7 @@ func resetClusterMember(ctx context.Context, s state.State, force bool) (reExec logger.Error("Failed shutting down", slog.String("error", err.Error())) } - err = os.RemoveAll(s.FileSystem().StateDir) + err = os.RemoveAll(s.FileSystem().StateDir()) if err != nil && !force { logger.Error("Failed to remove the state directory", slog.String("error", err.Error())) } @@ -643,7 +643,10 @@ func clusterMemberDelete(s state.State, r *http.Request) response.Response { } } - localClient, err := internalClient.New(s.FileSystem().ControlSocket(), nil, nil, false) + url := api.NewURL() + url.URL = *s.FileSystem().ControlSocket() + + localClient, err := internalClient.New(*url, nil, nil, false) if err != nil { return response.SmartError(err) } diff --git a/internal/rest/resources/control.go b/internal/rest/resources/control.go index 981f5eec..108b4fe3 100644 --- a/internal/rest/resources/control.go +++ b/internal/rest/resources/control.go @@ -141,18 +141,18 @@ func controlPost(state state.State, r *http.Request) response.Response { // Replace the server keypair if the cluster member name has changed upon initialization. if !certNameMatches { - err := os.Remove(filepath.Join(state.FileSystem().StateDir, "server.crt")) + err := os.Remove(filepath.Join(state.FileSystem().StateDir(), "server.crt")) if err != nil { return response.SmartError(err) } - err = os.Remove(filepath.Join(state.FileSystem().StateDir, "server.key")) + err = os.Remove(filepath.Join(state.FileSystem().StateDir(), "server.key")) if err != nil { return response.SmartError(err) } // Generate a new keypair with the new subject name. - _, err = shared.KeyPairAndCA(state.FileSystem().StateDir, string(types.ServerCertificateName), shared.CertServer, shared.CertOptions{AddHosts: true, CommonName: req.Name}) + _, err = shared.KeyPairAndCA(state.FileSystem().StateDir(), string(types.ServerCertificateName), shared.CertServer, shared.CertOptions{AddHosts: true, CommonName: req.Name}) if err != nil { return response.SmartError(err) } @@ -292,7 +292,7 @@ func writeCert(dir, prefix string, cert, key, ca []byte) error { func setupLocalMember(state state.State, localClusterMember *trust.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) + err := writeCert(state.FileSystem().StateDir(), string(types.ClusterCertificateName), []byte(joinInfo.ClusterCert.String()), []byte(joinInfo.ClusterKey), nil) if err != nil { return nil, err } @@ -305,7 +305,7 @@ func setupLocalMember(state state.State, localClusterMember *trust.Remote, joinI ca = []byte(cert.CA) } - err := writeCert(state.FileSystem().CertificatesDir, name, []byte(cert.Cert), []byte(cert.Key), ca) + err := writeCert(state.FileSystem().CertificatesDir(), name, []byte(cert.Cert), []byte(cert.Key), ca) if err != nil { return nil, err } @@ -324,7 +324,7 @@ func setupLocalMember(state state.State, localClusterMember *trust.Remote, joinI } clusterMembers = append(clusterMembers, *localClusterMember) - err = state.Remotes().Add(state.FileSystem().TrustDir, clusterMembers...) + err = state.Remotes().Add(state.FileSystem().TrustDir(), clusterMembers...) if err != nil { return nil, err } diff --git a/internal/rest/resources/heartbeat.go b/internal/rest/resources/heartbeat.go index 29c14964..7686904b 100644 --- a/internal/rest/resources/heartbeat.go +++ b/internal/rest/resources/heartbeat.go @@ -50,7 +50,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.Remotes().Replace(s.FileSystem().TrustDir(), clusterMemberList...) if err != nil { return response.SmartError(err) } @@ -167,7 +167,7 @@ func beginHeartbeat(ctx context.Context, s state.State, hbReq types.HeartbeatInf logger.Debug("Beginning new heartbeat round", slog.String("address", s.Address().URL.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.Remotes().Replace(s.FileSystem().TrustDir(), clusterMembers...) if err != nil { return response.SmartError(err) } diff --git a/internal/rest/resources/truststore.go b/internal/rest/resources/truststore.go index 9bbec4ae..bb764e4b 100644 --- a/internal/rest/resources/truststore.go +++ b/internal/rest/resources/truststore.go @@ -107,7 +107,7 @@ func trustPost(s state.State, r *http.Request) response.Response { remotes := s.Remotes() _, ok := remotes.RemotesByName()[newRemote.Name] if !ok { - err = remotes.Add(s.FileSystem().TrustDir, newRemote) + err = remotes.Add(s.FileSystem().TrustDir(), newRemote) if err != nil { return response.SmartError(fmt.Errorf("Failed adding local record of newly joined node %q: %w", req.Name, err)) } @@ -167,7 +167,7 @@ func trustDelete(s state.State, r *http.Request) response.Response { newRemotes = append(newRemotes, newRemote) } - err = remotes.Replace(s.FileSystem().TrustDir, newRemotes...) + err = remotes.Replace(s.FileSystem().TrustDir(), newRemotes...) if err != nil { return response.SmartError(fmt.Errorf("Failed to remove truststore entry for node with name %q: %w", name, err)) } diff --git a/internal/state/state.go b/internal/state/state.go index 35ac3a3b..8326c4d4 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -14,7 +14,6 @@ import ( "github.com/canonical/microcluster/v3/internal/endpoints" "github.com/canonical/microcluster/v3/internal/extensions" internalClient "github.com/canonical/microcluster/v3/internal/rest/client" - "github.com/canonical/microcluster/v3/internal/sys" "github.com/canonical/microcluster/v3/internal/trust" "github.com/canonical/microcluster/v3/microcluster/types" ) @@ -22,7 +21,7 @@ import ( // State exposes the internal daemon state for use with extended API handlers. type State interface { // FileSystem structure. - FileSystem() *sys.OS + FileSystem() types.OS // Listen Address. Address() *api.URL @@ -99,7 +98,7 @@ type InternalState struct { // Hooks contain external implementations that are triggered by specific cluster actions. Hooks *Hooks - InternalFileSystem func() *sys.OS + InternalFileSystem func() types.OS InternalAddress func() *api.URL InternalName func() string InternalVersion func() string @@ -111,7 +110,7 @@ type InternalState struct { } // FileSystem can be used to inspect the microcluster filesystem. -func (s *InternalState) FileSystem() *sys.OS { +func (s *InternalState) FileSystem() types.OS { return s.InternalFileSystem() } diff --git a/internal/sys/os.go b/internal/sys/os.go index 934c68ff..3d5bf715 100644 --- a/internal/sys/os.go +++ b/internal/sys/os.go @@ -3,6 +3,7 @@ package sys import ( "errors" "fmt" + "net/url" "os" "path/filepath" @@ -14,15 +15,15 @@ import ( // OS contains fields and methods for interacting with the state directory. type OS struct { - StateDir string - DatabaseDir string - TrustDir string - CertificatesDir string - LogFile string + stateDir string + databaseDir string + trustDir string + certificatesDir string + logFile string } // DefaultOS returns a fresh uninitialized OS instance with default values. -func DefaultOS(stateDir string, createDir bool) (*OS, error) { +func DefaultOS(stateDir string, createDir bool) (types.OS, error) { if stateDir == "" { stateDir = os.Getenv(StateDir) } @@ -30,11 +31,11 @@ func DefaultOS(stateDir string, createDir bool) (*OS, error) { // TODO: Configurable log file path. os := &OS{ - StateDir: stateDir, - DatabaseDir: filepath.Join(stateDir, "database"), - TrustDir: filepath.Join(stateDir, "truststore"), - CertificatesDir: filepath.Join(stateDir, "certificates"), - LogFile: "", + stateDir: stateDir, + databaseDir: filepath.Join(stateDir, "database"), + trustDir: filepath.Join(stateDir, "truststore"), + certificatesDir: filepath.Join(stateDir, "certificates"), + logFile: "", } err := os.init(createDir) @@ -50,10 +51,10 @@ func (s *OS) init(createDir bool) error { path string mode os.FileMode }{ - {s.StateDir, 0711}, - {s.DatabaseDir, 0700}, - {s.TrustDir, 0700}, - {s.CertificatesDir, 0700}, + {s.stateDir, 0711}, + {s.databaseDir, 0700}, + {s.trustDir, 0700}, + {s.certificatesDir, 0700}, } for _, dir := range dirs { @@ -101,23 +102,23 @@ func (s *OS) IsControlSocketPresent() (bool, error) { } // ControlSocket returns the full path to the control.socket file that this daemon is listening on. -func (s *OS) ControlSocket() api.URL { - return *api.NewURL().Scheme("http").Host(s.ControlSocketPath()) +func (s *OS) ControlSocket() *url.URL { + return &api.NewURL().Scheme("http").Host(s.ControlSocketPath()).URL } // ControlSocketPath returns the filesystem path to the control socket. func (s *OS) ControlSocketPath() string { - return filepath.Join(s.StateDir, "control.socket") + return filepath.Join(s.stateDir, "control.socket") } // DatabasePath returns the path of the database file managed by dqlite. func (s *OS) DatabasePath() string { - return filepath.Join(s.DatabaseDir, "db.bin") + return filepath.Join(s.databaseDir, "db.bin") } // ServerCert gets the local server certificate from the state directory. func (s *OS) ServerCert() (*shared.CertInfo, error) { - cert, err := shared.KeyPairAndCA(s.StateDir, string(types.ServerCertificateName), shared.CertServer, shared.CertOptions{}) + cert, err := shared.KeyPairAndCA(s.stateDir, string(types.ServerCertificateName), shared.CertServer, shared.CertOptions{}) if err != nil { return nil, fmt.Errorf("Failed to load TLS certificate: %w", err) } @@ -127,10 +128,30 @@ func (s *OS) ServerCert() (*shared.CertInfo, error) { // ClusterCert gets the local cluster certificate from the state directory. func (s *OS) ClusterCert() (*shared.CertInfo, error) { - cert, err := shared.KeyPairAndCA(s.StateDir, string(types.ClusterCertificateName), shared.CertServer, shared.CertOptions{}) + cert, err := shared.KeyPairAndCA(s.stateDir, string(types.ClusterCertificateName), shared.CertServer, shared.CertOptions{}) if err != nil { return nil, fmt.Errorf("Failed to load TLS certificate: %w", err) } return cert, nil } + +// StateDir gets the state's base directory. +func (s *OS) StateDir() string { + return s.stateDir +} + +// DatabaseDir gets the state's database directory. +func (s *OS) DatabaseDir() string { + return s.databaseDir +} + +// TrustDir gets the state's trust directory. +func (s *OS) TrustDir() string { + return s.trustDir +} + +// CertificatesDir gets the state's certificates directory. +func (s *OS) CertificatesDir() string { + return s.certificatesDir +} diff --git a/microcluster/app.go b/microcluster/app.go index 4c831052..d652ef51 100644 --- a/microcluster/app.go +++ b/microcluster/app.go @@ -30,7 +30,7 @@ type DaemonArgs = daemon.Args // MicroCluster contains some basic filesystem information for interacting with the MicroCluster daemon. type MicroCluster struct { - FileSystem *sys.OS + FileSystem types.OS args Args } @@ -93,7 +93,7 @@ func (m *MicroCluster) Start(ctx context.Context, daemonArgs DaemonArgs) error { // Attach the logger to the parent context. ctx = context.WithValue(ctx, log.CtxLogger, logger) - err := d.Run(ctx, m.FileSystem.StateDir, daemonArgs) + err := d.Run(ctx, m.FileSystem.StateDir(), daemonArgs) if err != nil { return fmt.Errorf("Daemon stopped with error: %w", err) } @@ -307,7 +307,10 @@ func (m *MicroCluster) RevokeJoinToken(ctx context.Context, name string) error { func (m *MicroCluster) LocalClient() (*client.Client, error) { c := m.args.Client if c == nil { - internalClient, err := internalClient.New(m.FileSystem.ControlSocket(), nil, nil, false) + url := api.NewURL() + url.URL = *m.FileSystem.ControlSocket() + + internalClient, err := internalClient.New(*url, nil, nil, false) if err != nil { return nil, err } diff --git a/microcluster/types/os.go b/microcluster/types/os.go new file mode 100644 index 00000000..8cedfe80 --- /dev/null +++ b/microcluster/types/os.go @@ -0,0 +1,21 @@ +package types + +import ( + "net/url" + + "github.com/canonical/lxd/shared" +) + +// OS represents Microcluster's state on disk. +type OS interface { + StateDir() string + DatabaseDir() string + TrustDir() string + CertificatesDir() string + DatabasePath() string + ControlSocket() *url.URL + ControlSocketPath() string + IsControlSocketPresent() (bool, error) + ServerCert() (*shared.CertInfo, error) + ClusterCert() (*shared.CertInfo, error) +}