open-vault/vault/rollback.go

164 lines
4.6 KiB
Go
Raw Normal View History

package vault
import (
"log"
"sync/atomic"
"time"
"github.com/hashicorp/vault/logical"
)
// RollbackManager is responsible for performing rollbacks of partial
// secrets within logical backends.
//
// During normal operations, it is possible for logical backends to
// error partially through an operation. These are called "partial secrets":
// they are never sent back to a user, but they do need to be cleaned up.
// This manager handles that by periodically (on a timer) requesting that the
// backends clean up.
//
// The RollbackManager periodically (according to the Period option)
// initiates a logical.RollbackOperation on every mounted logical backend.
// It ensures that only one rollback operation is in-flight at any given
// time within a single seal/unseal phase.
type RollbackManager struct {
// NOTE: This must always be at the top of the struct to avoid
// atomic alignment issues. Go bug.
running uint32
Logger *log.Logger
Mounts *MountTable
Router *Router
Period time.Duration // time between rollback calls
}
// Start starts the rollback manager. This will block until Stop is called
// so it should be executed within a goroutine.
func (m *RollbackManager) Start() {
// If we're already running, then don't start again
if !atomic.CompareAndSwapUint32(&m.running, 0, 1) {
return
}
m.Logger.Printf("[INFO] rollback: starting rollback manager")
// mounts is a mapping of a mount path (i.e. "sys") to a uint32 pointer
// we can do atomic operations on. The purpose of this map is to ensure
// we only ever have one RollbackOperation request in-flight for each
// path.
//
// When a RollbackOperation is started, the pointer is changed to 0 to 1
// atomically. When the operation completes, it is atomatically loaded
// to 0 (from anything). Before we start a rollback operation, we use a
// CAS 0 to 1 and only start a rollback if that succeeds.
//
// As a result, we only ever get one in-flight request at one time.
var mounts map[string]*uint32
tick := time.NewTicker(m.Period)
defer tick.Stop()
for {
// Wait for the tick
<-tick.C
// If we're quitting, then stop
if atomic.LoadUint32(&m.running) != 1 {
m.Logger.Printf("[INFO] rollback: stopping rollback manager")
return
}
// Get the list of paths that we should rollback and setup our
// mounts mapping. Mounts that have since been unmounted will
// just "fall off" naturally: they aren't in our new mount mapping
// and when their goroutine ends they'll naturally lose the reference.
//
// The reason we make a new mapping is so that unmounted paths
// are automatically removed. If a mount path was in the last mapping
// we copy the uint32 pointer. So the result of the copy is: new
// mount paths get a new uint32 pointer, unmounted paths are removed
// from the map, and existing mount paths nothing changes.
//
// The purpose of the map is documented above where mounts is defined.
newMounts := make(map[string]*uint32)
m.Mounts.RLock()
for _, e := range m.Mounts.Entries {
if s, ok := mounts[e.Path]; ok {
newMounts[e.Path] = s
} else {
newMounts[e.Path] = new(uint32)
}
}
m.Mounts.RUnlock()
mounts = newMounts
// Go through the mounts and start the rollback if we can
for path, status := range mounts {
// If we can change the status from 0 to 1, we can start it
if !atomic.CompareAndSwapUint32(status, 0, 1) {
continue
}
go m.rollback(path, status)
}
}
}
// Stop stops the running manager. This will not halt any in-flight
// rollbacks.
func (m *RollbackManager) Stop() {
atomic.StoreUint32(&m.running, 0)
}
func (m *RollbackManager) rollback(path string, state *uint32) {
defer atomic.StoreUint32(state, 0)
m.Logger.Printf(
"[DEBUG] rollback: starting rollback for %s",
path)
req := &logical.Request{
Operation: logical.RollbackOperation,
Path: path,
}
if _, err := m.Router.Route(req); err != nil {
// If the error is an unsupported operation, then it doesn't
// matter, the backend doesn't support it.
if err == logical.ErrUnsupportedOperation {
return
}
m.Logger.Printf(
"[ERR] rollback: error rolling back %s: %s",
path, err)
}
}
// The methods below are the hooks from core that are called pre/post seal.
func (c *Core) startRollback() error {
// Ensure if we had a rollback it was stopped. This should never
// be the case but it doesn't hurt to check.
if c.rollback != nil {
c.rollback.Stop()
}
c.rollback = &RollbackManager{
Logger: c.logger,
Router: c.router,
Mounts: c.mounts,
Period: 1 * time.Minute,
}
go c.rollback.Start()
return nil
}
func (c *Core) stopRollback() error {
if c.rollback != nil {
c.rollback.Stop()
c.rollback = nil
}
return nil
}