Lock the state directory and write vault files atomically (closes #34)
check / check (push) Successful in 1m31s
check / check (push) Successful in 1m31s
Each command that changes the state directory holds one lock: flock(2) on `lock` in the state directory, dropped by the kernel if the process dies, or a process-wide mutex on the in-memory test filesystem. It covers the state directory, not each vault, because `currentvault`, `vault create` and cross-vault moves span vaults, and a lock file in a vault would be deleted by `vault remove` under a waiting command. Files go through `secret.WriteFileAtomic`; versions, new secrets and cross-vault copies are built in a temporary directory and renamed into place; removals rename out of the way first. Left for later: replacing an unlocker (#71) and deleting what an interrupted command leaves under a `.tmp-` name (#75). Model: opus-5-5 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #69.
This commit is contained in:
+42
-4
@@ -383,6 +383,15 @@ func (cli *Instance) AddSecret(secretName string, force bool) error {
|
||||
valueBuffer := combineBuffers(buffers, totalSize)
|
||||
defer valueBuffer.Destroy()
|
||||
|
||||
// Locked only now that stdin has been read: in `secret encrypt key |
|
||||
// secret add name`, holding the lock while reading would leave each
|
||||
// command waiting for the other.
|
||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer release()
|
||||
|
||||
// Add the secret to the vault
|
||||
secret.Debug("Calling vault.AddSecret", "secret_name", secretName,
|
||||
"value_length", valueBuffer.Size(), "force", force)
|
||||
@@ -660,6 +669,14 @@ func (cli *Instance) ImportSecret(
|
||||
valueBuffer := combineBuffers(buffers, totalSize)
|
||||
defer valueBuffer.Destroy()
|
||||
|
||||
// Locked only now that the file has been read, as in AddSecret: the
|
||||
// file may be a pipe written by another secret command.
|
||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer release()
|
||||
|
||||
// Store the secret in the vault
|
||||
err = vlt.AddSecret(secretName, valueBuffer, force)
|
||||
if err != nil {
|
||||
@@ -679,6 +696,12 @@ func (cli *Instance) RemoveSecret(cmd *cobra.Command, secretName string, _ bool)
|
||||
return err
|
||||
}
|
||||
|
||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer release()
|
||||
|
||||
// Get current vault
|
||||
currentVlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
@@ -713,7 +736,7 @@ func (cli *Instance) RemoveSecret(cmd *cobra.Command, secretName string, _ bool)
|
||||
}
|
||||
|
||||
// Remove the secret directory
|
||||
err = cli.fs.RemoveAll(secretDir)
|
||||
err = secret.RemoveDirAtomic(cli.fs, secretDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove secret: %w", err)
|
||||
}
|
||||
@@ -724,9 +747,24 @@ func (cli *Instance) RemoveSecret(cmd *cobra.Command, secretName string, _ bool)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveSecret moves or renames a secret (within or across vaults)
|
||||
// MoveSecret moves or renames a secret (within or across vaults), holding
|
||||
// the state directory lock.
|
||||
func (cli *Instance) MoveSecret(
|
||||
cmd *cobra.Command, source, dest string, force bool,
|
||||
) error {
|
||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer release()
|
||||
|
||||
return cli.moveSecret(cmd, source, dest, force)
|
||||
}
|
||||
|
||||
// moveSecret does the work of MoveSecret. The caller holds the state
|
||||
// directory lock.
|
||||
func (cli *Instance) moveSecret(
|
||||
cmd *cobra.Command, source, dest string, force bool,
|
||||
) error {
|
||||
// Parse source and destination
|
||||
srcVaultName, srcSecretName, srcQualified := ParseVaultSecretRef(source)
|
||||
@@ -833,7 +871,7 @@ func (cli *Instance) moveSecretWithinVault(
|
||||
return fmt.Errorf("secret '%s' %w", dest, errSecretExistsNoForce)
|
||||
}
|
||||
|
||||
err = cli.fs.RemoveAll(destDir)
|
||||
err = secret.RemoveDirAtomic(cli.fs, destDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove existing destination: %w", err)
|
||||
}
|
||||
@@ -914,7 +952,7 @@ func (cli *Instance) moveSecretCrossVault(
|
||||
}
|
||||
|
||||
// Delete source secret
|
||||
err = cli.fs.RemoveAll(srcSecretDir)
|
||||
err = secret.RemoveDirAtomic(cli.fs, srcSecretDir)
|
||||
if err != nil {
|
||||
// Copy succeeded but delete failed - warn but don't fail
|
||||
cmd.Printf("Warning: copied secret but failed to remove source: %v\n", err)
|
||||
|
||||
Reference in New Issue
Block a user