|
|
|
@@ -19,12 +19,20 @@ package cmd
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
"errors"
|
|
|
|
|
"maps"
|
|
|
|
|
"slices"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
|
|
|
|
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
|
|
|
|
"github.com/minio/minio/internal/bucket/replication"
|
|
|
|
|
xhttp "github.com/minio/minio/internal/http"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// An ordinary ILM rejection is not evidence that the version is absent. In
|
|
|
|
@@ -51,17 +59,25 @@ var errLifecycleNotApplicable = errors.New("lifecycle expiry no longer applies")
|
|
|
|
|
// recheck of a set does it: the scanner queues it again once the rule
|
|
|
|
|
// applies to the object.
|
|
|
|
|
|
|
|
|
|
// objectVersions returns the versions of object that a read quorum of the
|
|
|
|
|
// drives of the set agree on, newest first, without free versions. It returns
|
|
|
|
|
// no versions when the set does not hold the object, and an error when too
|
|
|
|
|
// few drives answer to tell.
|
|
|
|
|
// objectVersions returns the versions of object that the drives of the set
|
|
|
|
|
// hold authoritatively, newest first, without free versions. It returns no
|
|
|
|
|
// versions when the set does not hold the object, and an error when the
|
|
|
|
|
// drives cannot tell its complete history.
|
|
|
|
|
//
|
|
|
|
|
// Expiry deletes by this history, so a revision of a version, its header
|
|
|
|
|
// and its metadata (versionRevision), counts only when more than half of
|
|
|
|
|
// the configured drives and the read quorum of its metadata hold it. A
|
|
|
|
|
// version that some drive shows without such a revision, or too many
|
|
|
|
|
// unreadable drives, could hide what an expiry must not delete. Free
|
|
|
|
|
// versions are tier GC records and do not count.
|
|
|
|
|
func (er erasureObjects) objectVersions(ctx context.Context, bucket, object string) ([]FileInfo, error) {
|
|
|
|
|
disks := er.getDisks()
|
|
|
|
|
rawFileInfos, errs := readAllRawFileInfo(ctx, disks, bucket, object, false)
|
|
|
|
|
readQuorum := (len(disks) + 1) / 2
|
|
|
|
|
majority := len(disks)/2 + 1
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
metas [][]xlMetaV2ShallowVersion
|
|
|
|
|
metaVs []uint8
|
|
|
|
|
metaV uint8
|
|
|
|
|
unknown int
|
|
|
|
|
)
|
|
|
|
@@ -78,23 +94,23 @@ func (er erasureObjects) objectVersions(ctx context.Context, bucket, object stri
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
metas = append(metas, xl.versions)
|
|
|
|
|
metaVs = append(metaVs, xl.metaV)
|
|
|
|
|
metaV = xl.metaV
|
|
|
|
|
}
|
|
|
|
|
if len(metas) < readQuorum {
|
|
|
|
|
if len(metas)+unknown < readQuorum {
|
|
|
|
|
return nil, nil // not in this set
|
|
|
|
|
}
|
|
|
|
|
// Without an unknown drive the history is complete, also on one drive.
|
|
|
|
|
if unknown > 0 && unknown >= len(disks)/2 {
|
|
|
|
|
return nil, toObjectErr(errErasureReadQuorum, bucket, object)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
merged := mergeXLV2Versions(readQuorum, false, 0, metas...)
|
|
|
|
|
if len(merged) == 0 {
|
|
|
|
|
for _, versions := range metas {
|
|
|
|
|
if len(versions) > 0 {
|
|
|
|
|
return nil, toObjectErr(errErasureReadQuorum, bucket, object)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil, nil
|
|
|
|
|
quorumMetas, quorumErrs := make([]FileInfo, len(disks)), make([]error, len(disks))
|
|
|
|
|
merged, err := authoritativeHistory(ctx, metas, metaVs, metaV, majority, func(ver xlMetaV2ShallowVersion, metaV uint8) int {
|
|
|
|
|
return er.versionReadQuorum(ctx, bucket, object, ver, metaV, quorumMetas, quorumErrs)
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, toObjectErr(err, bucket, object)
|
|
|
|
|
}
|
|
|
|
|
if merged == nil {
|
|
|
|
|
return nil, nil // not in this set
|
|
|
|
|
}
|
|
|
|
|
all, err := xlMetaV2{versions: merged, metaV: metaV}.ListVersions(bucket, object, false)
|
|
|
|
|
if err != nil {
|
|
|
|
@@ -109,10 +125,253 @@ func (er erasureObjects) objectVersions(ctx context.Context, bucket, object stri
|
|
|
|
|
return versions, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// revisionKey identifies a revision of a version: its header and a strong
|
|
|
|
|
// digest of its metadata. The header Signature is a 32-bit fold of weak
|
|
|
|
|
// hashes; two revisions can share it.
|
|
|
|
|
type revisionKey struct {
|
|
|
|
|
header xlMetaV2VersionHeader
|
|
|
|
|
digest [sha256.Size]byte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// versionRevision returns the revision of ver, whose metadata is encoded as
|
|
|
|
|
// metaV describes. As Signature does, it leaves out the fields that differ
|
|
|
|
|
// between the drives of one write: the erasure index, empty part lists and,
|
|
|
|
|
// for legacy versions, the release and the per-shard checksums. The digest
|
|
|
|
|
// input is canonical: each map, including the part checksums of a legacy
|
|
|
|
|
// version, is its own section with its entry count and sorted,
|
|
|
|
|
// length-prefixed entries, since msgp encodes maps in iteration order and an
|
|
|
|
|
// entry must not move between maps unnoticed. The header of a
|
|
|
|
|
// legacy version carries a signature over its per-shard checksums, so the
|
|
|
|
|
// key replaces it with one from the digest.
|
|
|
|
|
func versionRevision(ver xlMetaV2ShallowVersion, metaV uint8) revisionKey {
|
|
|
|
|
h := sha256.New()
|
|
|
|
|
writeUint := func(n int) {
|
|
|
|
|
var b [8]byte
|
|
|
|
|
binary.LittleEndian.PutUint64(b[:], uint64(n))
|
|
|
|
|
h.Write(b[:])
|
|
|
|
|
}
|
|
|
|
|
writeField := func(b []byte) {
|
|
|
|
|
writeUint(len(b))
|
|
|
|
|
h.Write(b)
|
|
|
|
|
}
|
|
|
|
|
section := func(name string, entries int) {
|
|
|
|
|
writeField([]byte(name))
|
|
|
|
|
writeUint(entries)
|
|
|
|
|
}
|
|
|
|
|
writeStrings := func(name string, m map[string]string) {
|
|
|
|
|
section(name, len(m))
|
|
|
|
|
for _, k := range slices.Sorted(maps.Keys(m)) {
|
|
|
|
|
writeField([]byte(k))
|
|
|
|
|
writeField([]byte(m[k]))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
writeBytes := func(name string, m map[string][]byte) {
|
|
|
|
|
section(name, len(m))
|
|
|
|
|
for _, k := range slices.Sorted(maps.Keys(m)) {
|
|
|
|
|
writeField([]byte(k))
|
|
|
|
|
writeField(m[k])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
key := revisionKey{header: ver.header}
|
|
|
|
|
var v xlMetaV2Version
|
|
|
|
|
if _, err := v.unmarshalV(metaV, ver.meta); err == nil {
|
|
|
|
|
var (
|
|
|
|
|
bts []byte
|
|
|
|
|
err error = errFileCorrupt
|
|
|
|
|
)
|
|
|
|
|
section("type", 1)
|
|
|
|
|
writeField([]byte{byte(v.Type)})
|
|
|
|
|
switch {
|
|
|
|
|
case v.Type == ObjectType && v.ObjectV2 != nil:
|
|
|
|
|
c := *v.ObjectV2
|
|
|
|
|
c.ErasureIndex = 0
|
|
|
|
|
if !slices.ContainsFunc(c.PartETags, func(tag string) bool { return tag != "" }) {
|
|
|
|
|
c.PartETags = nil
|
|
|
|
|
}
|
|
|
|
|
if len(c.PartActualSizes) == 0 {
|
|
|
|
|
c.PartActualSizes = nil
|
|
|
|
|
}
|
|
|
|
|
writeStrings("user", c.MetaUser)
|
|
|
|
|
writeBytes("sys", c.MetaSys)
|
|
|
|
|
c.MetaUser, c.MetaSys = nil, nil
|
|
|
|
|
bts, err = c.MarshalMsg(nil)
|
|
|
|
|
case v.Type == DeleteType && v.DeleteMarker != nil:
|
|
|
|
|
c := *v.DeleteMarker
|
|
|
|
|
writeBytes("sys", c.MetaSys)
|
|
|
|
|
c.MetaSys = nil
|
|
|
|
|
bts, err = c.MarshalMsg(nil)
|
|
|
|
|
case v.Type == LegacyType && v.ObjectV1 != nil:
|
|
|
|
|
c := *v.ObjectV1
|
|
|
|
|
c.Erasure.Index = 0
|
|
|
|
|
c.Erasure.Checksums = nil
|
|
|
|
|
c.Minio.Release = ""
|
|
|
|
|
writeStrings("meta", c.Meta)
|
|
|
|
|
c.Meta = nil
|
|
|
|
|
// Part checksums are the only other map: one section per part.
|
|
|
|
|
section("parts", len(c.Parts))
|
|
|
|
|
c.Parts = slices.Clone(c.Parts)
|
|
|
|
|
for i := range c.Parts {
|
|
|
|
|
writeStrings("part-checksums", c.Parts[i].Checksums)
|
|
|
|
|
c.Parts[i].Checksums = nil
|
|
|
|
|
}
|
|
|
|
|
bts, err = c.MarshalMsg(nil)
|
|
|
|
|
}
|
|
|
|
|
if err == nil {
|
|
|
|
|
section("fields", 1)
|
|
|
|
|
writeField(bts)
|
|
|
|
|
h.Sum(key.digest[:0])
|
|
|
|
|
if v.Type == LegacyType {
|
|
|
|
|
copy(key.header.Signature[:], key.digest[:])
|
|
|
|
|
}
|
|
|
|
|
return key
|
|
|
|
|
}
|
|
|
|
|
h.Reset()
|
|
|
|
|
}
|
|
|
|
|
// Undecodable metadata only matches the same bytes.
|
|
|
|
|
section("raw", 1)
|
|
|
|
|
writeField(ver.meta)
|
|
|
|
|
h.Sum(key.digest[:0])
|
|
|
|
|
return key
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// authoritativeHistory returns the versions of metas, the version lists of
|
|
|
|
|
// the drives that answered, as merged newest first, when every version
|
|
|
|
|
// that is not a free version has a revision that at least majority drives
|
|
|
|
|
// and its readQuorum hold. It returns no versions when none is live,
|
|
|
|
|
// errErasureReadQuorum when the drives cannot tell the history, and the
|
|
|
|
|
// error of ctx once it ends. metaVs are the metadata versions of metas,
|
|
|
|
|
// metaV the one the merged result is decoded with.
|
|
|
|
|
func authoritativeHistory(ctx context.Context, metas [][]xlMetaV2ShallowVersion, metaVs []uint8, metaV uint8, majority int, readQuorum func(xlMetaV2ShallowVersion, uint8) int) ([]xlMetaV2ShallowVersion, error) {
|
|
|
|
|
// Count the drives that hold each revision; a drive counts once for each
|
|
|
|
|
// version.
|
|
|
|
|
type revision struct {
|
|
|
|
|
ver xlMetaV2ShallowVersion
|
|
|
|
|
metaV uint8
|
|
|
|
|
count int
|
|
|
|
|
}
|
|
|
|
|
keys := make([][]revisionKey, len(metas))
|
|
|
|
|
revisions := make(map[[16]byte]map[revisionKey]*revision)
|
|
|
|
|
live := make(map[[16]byte]bool)
|
|
|
|
|
for i, versions := range metas {
|
|
|
|
|
keys[i] = make([]revisionKey, len(versions))
|
|
|
|
|
seen := make(map[[16]byte]bool, len(versions))
|
|
|
|
|
for j, ver := range versions {
|
|
|
|
|
// Decoding and hashing every history has a cost; stop with the
|
|
|
|
|
// request.
|
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
id := ver.header.VersionID
|
|
|
|
|
if !ver.header.FreeVersion() {
|
|
|
|
|
live[id] = true
|
|
|
|
|
}
|
|
|
|
|
if seen[id] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[id] = true
|
|
|
|
|
key := versionRevision(ver, metaVs[i])
|
|
|
|
|
keys[i][j] = key
|
|
|
|
|
if revisions[id] == nil {
|
|
|
|
|
revisions[id] = make(map[revisionKey]*revision)
|
|
|
|
|
}
|
|
|
|
|
r := revisions[id][key]
|
|
|
|
|
if r == nil {
|
|
|
|
|
r = &revision{ver: ver, metaV: metaVs[i]}
|
|
|
|
|
revisions[id][key] = r
|
|
|
|
|
}
|
|
|
|
|
r.count++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
accepted := make(map[[16]byte]revisionKey, len(live))
|
|
|
|
|
for id := range live {
|
|
|
|
|
var best *revision
|
|
|
|
|
var bestKey revisionKey
|
|
|
|
|
for key, r := range revisions[id] {
|
|
|
|
|
if !key.header.FreeVersion() && (best == nil || r.count > best.count) {
|
|
|
|
|
best, bestKey = r, key
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Only one revision can hold more than half of the drives.
|
|
|
|
|
if best == nil || best.count < majority || best.count < readQuorum(best.ver, best.metaV) {
|
|
|
|
|
return nil, errErasureReadQuorum
|
|
|
|
|
}
|
|
|
|
|
accepted[id] = bestKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The strict merge orders the history. It has to hold exactly the
|
|
|
|
|
// accepted revisions. A minority revision that sorts first would reset
|
|
|
|
|
// its count, and one that shares the header could stand in for the
|
|
|
|
|
// accepted one, so the merge only sees the accepted revision of each
|
|
|
|
|
// version and free versions as they are. It sees the headers of the
|
|
|
|
|
// keys, which do not differ between the drives of a legacy version.
|
|
|
|
|
filtered := make([][]xlMetaV2ShallowVersion, len(metas))
|
|
|
|
|
for i, versions := range metas {
|
|
|
|
|
for j, ver := range versions {
|
|
|
|
|
key, ok := accepted[ver.header.VersionID]
|
|
|
|
|
switch {
|
|
|
|
|
case ver.header.FreeVersion():
|
|
|
|
|
case ok && keys[i][j] == key:
|
|
|
|
|
ver.header = key.header
|
|
|
|
|
default:
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
filtered[i] = append(filtered[i], ver)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
merged := mergeXLV2Versions(majority, true, 0, filtered...)
|
|
|
|
|
emitted := make(map[[16]byte]bool, len(accepted))
|
|
|
|
|
for _, ver := range merged {
|
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if ver.header.FreeVersion() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
id := ver.header.VersionID
|
|
|
|
|
if key, ok := accepted[id]; !ok || emitted[id] || versionRevision(ver, metaV) != key {
|
|
|
|
|
return nil, errErasureReadQuorum
|
|
|
|
|
}
|
|
|
|
|
emitted[id] = true
|
|
|
|
|
}
|
|
|
|
|
if len(emitted) != len(accepted) {
|
|
|
|
|
return nil, errErasureReadQuorum
|
|
|
|
|
}
|
|
|
|
|
if len(accepted) == 0 {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
return merged, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// versionReadQuorum returns the read quorum of the metadata of ver, as
|
|
|
|
|
// objectQuorumFromMeta derives it when all drives of the set hold it, or
|
|
|
|
|
// more than the drives when its metadata cannot be read. metaArr and errs
|
|
|
|
|
// have one entry for each drive; errs stays nil.
|
|
|
|
|
func (er erasureObjects) versionReadQuorum(ctx context.Context, bucket, object string, ver xlMetaV2ShallowVersion, metaV uint8, metaArr []FileInfo, errs []error) int {
|
|
|
|
|
var v xlMetaV2Version
|
|
|
|
|
if _, err := v.unmarshalV(metaV, ver.meta); err != nil {
|
|
|
|
|
return len(metaArr) + 1
|
|
|
|
|
}
|
|
|
|
|
fi, err := v.ToFileInfo(bucket, object, false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return len(metaArr) + 1
|
|
|
|
|
}
|
|
|
|
|
for i := range metaArr {
|
|
|
|
|
metaArr[i] = fi
|
|
|
|
|
}
|
|
|
|
|
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return len(metaArr) + 1
|
|
|
|
|
}
|
|
|
|
|
return readQuorum
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// lifecycleEventsAllPools returns the versions of object in all pools,
|
|
|
|
|
// newest first, and the lifecycle event of each as the scanner evaluates it
|
|
|
|
|
// on them. A version with copies in several pools, as an interrupted move
|
|
|
|
|
// leaves it, counts once.
|
|
|
|
|
// leaves it, counts once, and so do other writes of its version ID.
|
|
|
|
|
func (z *erasureServerPools) lifecycleEventsAllPools(ctx context.Context, bucket, object string) ([]ObjectInfo, []lifecycle.Event, error) {
|
|
|
|
|
lc, err := globalLifecycleSys.Get(bucket)
|
|
|
|
|
if err != nil {
|
|
|
|
@@ -169,14 +428,36 @@ func (z *erasureServerPools) lifecycleEventsAllPools(ctx context.Context, bucket
|
|
|
|
|
versions = append(versions, []PoolObjInfo{copy})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e := lifecycle.NewEvaluator(*lc).WithLockRetention(&lr).WithReplicationConfig(rcfg)
|
|
|
|
|
objInfos := make([]ObjectInfo, len(versions))
|
|
|
|
|
objOpts := make([]lifecycle.ObjectOpts, len(versions))
|
|
|
|
|
for i, copies := range versions {
|
|
|
|
|
// Copies with another ModTime are other writes of the version ID,
|
|
|
|
|
// such as null versions written in several pools, not duplicates of
|
|
|
|
|
// one write. The newest is the version. Data movement writes a new
|
|
|
|
|
// data dir, so only ModTime tells the duplicates of a write apart.
|
|
|
|
|
// An older write that is still protected keeps the whole object.
|
|
|
|
|
newest := 1
|
|
|
|
|
for newest < len(copies) && copies[newest].ObjInfo.ModTime.Equal(copies[0].ObjInfo.ModTime) {
|
|
|
|
|
newest++
|
|
|
|
|
}
|
|
|
|
|
for _, older := range copies[newest:] {
|
|
|
|
|
if opts := older.ObjInfo.ToLifecycleOpts(); e.IsObjectLocked(opts) || e.IsPendingReplication(opts) {
|
|
|
|
|
return nil, nil, errLifecycleNotApplicable
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
copies = copies[:newest]
|
|
|
|
|
oi := copies[0].ObjInfo
|
|
|
|
|
if len(copies) > 1 {
|
|
|
|
|
// The copies must tell which protection is the newest, and no
|
|
|
|
|
// copy may carry replication state that the merge would drop.
|
|
|
|
|
if !lockRevisionsOrdered(copies) || !replicationStatesAgree(copies) {
|
|
|
|
|
return nil, nil, errLifecycleNotApplicable
|
|
|
|
|
}
|
|
|
|
|
// Count the version once, but retain independently ordered lock
|
|
|
|
|
// and tag revisions, including explicit removals, from every copy.
|
|
|
|
|
oi = mergedPoolObjectInfo(copies)
|
|
|
|
|
setLifecycleLockState(oi.UserDefined, newestLockState(copies))
|
|
|
|
|
}
|
|
|
|
|
oi.IsLatest = i == 0
|
|
|
|
|
oi.NumVersions = len(versions)
|
|
|
|
@@ -187,13 +468,160 @@ func (z *erasureServerPools) lifecycleEventsAllPools(ctx context.Context, bucket
|
|
|
|
|
objInfos[i] = oi
|
|
|
|
|
objOpts[i] = oi.ToLifecycleOpts()
|
|
|
|
|
}
|
|
|
|
|
events, err := lifecycle.NewEvaluator(*lc).WithLockRetention(&lr).WithReplicationConfig(rcfg).Eval(objOpts)
|
|
|
|
|
events, err := e.Eval(objOpts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
return objInfos, events, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// lockRevisionTime parses the timestamp of an Object Lock revision. Unlike
|
|
|
|
|
// olderThan, a missing, malformed or zero timestamp orders nothing.
|
|
|
|
|
func lockRevisionTime(stamp string) (time.Time, bool) {
|
|
|
|
|
t, err := time.Parse(time.RFC3339Nano, stamp)
|
|
|
|
|
return t, err == nil && !t.IsZero()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// lockRevisionNewer reports whether the revision stamped newer strictly
|
|
|
|
|
// supersedes the one stamped older.
|
|
|
|
|
func lockRevisionNewer(newer, older string) bool {
|
|
|
|
|
n, ok := lockRevisionTime(newer)
|
|
|
|
|
o, ok2 := lockRevisionTime(older)
|
|
|
|
|
return ok && ok2 && n.After(o)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// lifecycleLockState returns the Object Lock state of metadata as the
|
|
|
|
|
// lifecycle evaluator reads it (objectlock.GetObjectRetentionMeta and
|
|
|
|
|
// GetObjectLegalHoldMeta): a lowercase key before its canonical form. The
|
|
|
|
|
// stored state (storedObjectLockState) only has lowercase keys.
|
|
|
|
|
func lifecycleLockState(metadata map[string]string) objectLockState {
|
|
|
|
|
get := func(canonical string) string {
|
|
|
|
|
if v, ok := metadata[strings.ToLower(canonical)]; ok {
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
return metadata[canonical]
|
|
|
|
|
}
|
|
|
|
|
return objectLockState{
|
|
|
|
|
mode: get(objectlock.AmzObjectLockMode),
|
|
|
|
|
retainUntil: get(objectlock.AmzObjectLockRetainUntilDate),
|
|
|
|
|
retentionTimestamp: get(ReservedMetadataPrefix + ObjectLockRetentionTimestamp),
|
|
|
|
|
legalHold: get(objectlock.AmzObjectLockLegalHold),
|
|
|
|
|
legalHoldTimestamp: get(ReservedMetadataPrefix + ObjectLockLegalHoldTimestamp),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setLifecycleLockState sets state in metadata so that the evaluator reads
|
|
|
|
|
// it: lowercase keys take precedence over canonical ones.
|
|
|
|
|
func setLifecycleLockState(metadata map[string]string, state objectLockState) {
|
|
|
|
|
set := func(canonical, value string) {
|
|
|
|
|
lower := strings.ToLower(canonical)
|
|
|
|
|
_, hasLower := metadata[lower]
|
|
|
|
|
_, hasCanonical := metadata[canonical]
|
|
|
|
|
if value != "" || hasLower || hasCanonical {
|
|
|
|
|
metadata[lower] = value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
set(objectlock.AmzObjectLockMode, state.mode)
|
|
|
|
|
set(objectlock.AmzObjectLockRetainUntilDate, state.retainUntil)
|
|
|
|
|
set(ReservedMetadataPrefix+ObjectLockRetentionTimestamp, state.retentionTimestamp)
|
|
|
|
|
set(objectlock.AmzObjectLockLegalHold, state.legalHold)
|
|
|
|
|
set(ReservedMetadataPrefix+ObjectLockLegalHoldTimestamp, state.legalHoldTimestamp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newestLockState returns the newest hold and the newest retention of the
|
|
|
|
|
// copies of a version, which lockRevisionsOrdered found equal or ordered.
|
|
|
|
|
func newestLockState(copies []PoolObjInfo) objectLockState {
|
|
|
|
|
state := lifecycleLockState(copies[0].ObjInfo.UserDefined)
|
|
|
|
|
for _, copy := range copies[1:] {
|
|
|
|
|
next := lifecycleLockState(copy.ObjInfo.UserDefined)
|
|
|
|
|
if lockRevisionNewer(next.legalHoldTimestamp, state.legalHoldTimestamp) {
|
|
|
|
|
state.legalHold, state.legalHoldTimestamp = next.legalHold, next.legalHoldTimestamp
|
|
|
|
|
}
|
|
|
|
|
if lockRevisionNewer(next.retentionTimestamp, state.retentionTimestamp) {
|
|
|
|
|
state.mode, state.retainUntil, state.retentionTimestamp = next.mode, next.retainUntil, next.retentionTimestamp
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return state
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// checkedExpiryRefused reports whether a set must keep a version whose ILM
|
|
|
|
|
// expiry the pools approved with the Object Lock state approved, by the
|
|
|
|
|
// metadata goi the set reads under the same lock. A pending purge always
|
|
|
|
|
// keeps it: its status orders nothing. The approved hold or retention only
|
|
|
|
|
// replaces the one the set reads when it is strictly newer; otherwise the
|
|
|
|
|
// protection the set reads stands, also when it is newer than the approval.
|
|
|
|
|
func checkedExpiryRefused(lc lifecycle.Lifecycle, lr objectlock.Retention, rcfg *replication.Config, goi ObjectInfo, approved objectLockState) bool {
|
|
|
|
|
e := lifecycle.NewEvaluator(lc).WithLockRetention(&lr).WithReplicationConfig(rcfg)
|
|
|
|
|
obj := goi.ToLifecycleOpts()
|
|
|
|
|
if e.IsPendingReplication(obj) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
local := lifecycleLockState(goi.UserDefined)
|
|
|
|
|
obj.UserDefined = maps.Clone(goi.UserDefined)
|
|
|
|
|
if obj.UserDefined == nil {
|
|
|
|
|
obj.UserDefined = make(map[string]string)
|
|
|
|
|
}
|
|
|
|
|
if lockRevisionNewer(approved.legalHoldTimestamp, local.legalHoldTimestamp) {
|
|
|
|
|
obj.UserDefined[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = approved.legalHold
|
|
|
|
|
}
|
|
|
|
|
if lockRevisionNewer(approved.retentionTimestamp, local.retentionTimestamp) {
|
|
|
|
|
obj.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)] = approved.mode
|
|
|
|
|
obj.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = approved.retainUntil
|
|
|
|
|
}
|
|
|
|
|
return e.IsObjectLocked(obj)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// lockRevisionsOrdered reports whether the copies of a version agree on the
|
|
|
|
|
// legal hold and on the retention, each on its own, or carry revisions with
|
|
|
|
|
// distinct valid timestamps, so that the newest one is known.
|
|
|
|
|
func lockRevisionsOrdered(copies []PoolObjInfo) bool {
|
|
|
|
|
ordered := func(revision func(objectLockState) (value, stamp string)) bool {
|
|
|
|
|
stamps := make(map[[2]string]string, len(copies))
|
|
|
|
|
for _, copy := range copies {
|
|
|
|
|
value, stamp := revision(lifecycleLockState(copy.ObjInfo.UserDefined))
|
|
|
|
|
stamps[[2]string{value, stamp}] = stamp
|
|
|
|
|
}
|
|
|
|
|
if len(stamps) == 1 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
seen := make([]time.Time, 0, len(stamps))
|
|
|
|
|
for _, stamp := range stamps {
|
|
|
|
|
t, ok := lockRevisionTime(stamp)
|
|
|
|
|
if !ok {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, other := range seen {
|
|
|
|
|
if t.Equal(other) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
seen = append(seen, t)
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return ordered(func(s objectLockState) (string, string) {
|
|
|
|
|
return s.legalHold, s.legalHoldTimestamp
|
|
|
|
|
}) && ordered(func(s objectLockState) (string, string) {
|
|
|
|
|
return s.mode + "\x00" + s.retainUntil, s.retentionTimestamp
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// replicationStatesAgree reports whether the copies of a version carry the
|
|
|
|
|
// same replication and purge state. The per-target states are compared as
|
|
|
|
|
// parsed, regardless of their order.
|
|
|
|
|
func replicationStatesAgree(copies []PoolObjInfo) bool {
|
|
|
|
|
first := copies[0].ObjInfo
|
|
|
|
|
for _, copy := range copies[1:] {
|
|
|
|
|
oi := copy.ObjInfo
|
|
|
|
|
if oi.ReplicationStatus != first.ReplicationStatus || oi.VersionPurgeStatus != first.VersionPurgeStatus ||
|
|
|
|
|
!maps.Equal(replicationStatusesMap(oi.ReplicationStatusInternal), replicationStatusesMap(first.ReplicationStatusInternal)) ||
|
|
|
|
|
!maps.Equal(versionPurgeStatusesMap(oi.VersionPurgeStatusInternal), versionPurgeStatusesMap(first.VersionPurgeStatusInternal)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// lifecycleAllPools holds the versions of an object in all pools and their
|
|
|
|
|
// lifecycle events, see lifecycleEventsAllPools.
|
|
|
|
|
type lifecycleAllPools struct {
|
|
|
|
@@ -204,11 +632,37 @@ type lifecycleAllPools struct {
|
|
|
|
|
|
|
|
|
|
// checkExpiryAllPools returns nil when the ILM expiry that opts describe
|
|
|
|
|
// still applies to object with the versions it has in all pools, see
|
|
|
|
|
// lifecycleAllPools.expiryApplies.
|
|
|
|
|
func (z *erasureServerPools) checkExpiryAllPools(ctx context.Context, bucket, object string, opts ObjectOptions) error {
|
|
|
|
|
// lifecycleAllPools.expiryApplies, and the Object Lock state with which it
|
|
|
|
|
// approved the expiry of a version.
|
|
|
|
|
func (z *erasureServerPools) checkExpiryAllPools(ctx context.Context, bucket, object string, opts ObjectOptions) (objectLockState, error) {
|
|
|
|
|
var all lifecycleAllPools
|
|
|
|
|
all.versions, all.events, all.err = z.lifecycleEventsAllPools(ctx, bucket, object)
|
|
|
|
|
return all.expiryApplies(bucket, object, opts)
|
|
|
|
|
if err := all.expiryApplies(bucket, object, opts); err != nil {
|
|
|
|
|
return objectLockState{}, err
|
|
|
|
|
}
|
|
|
|
|
return all.protection(opts.VersionID), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// version returns the index of the version versionID, or -1.
|
|
|
|
|
func (all lifecycleAllPools) version(versionID string) int {
|
|
|
|
|
for i, oi := range all.versions {
|
|
|
|
|
if oi.VersionID == versionID || oi.VersionID == "" && versionID == nullVersionID {
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// protection returns the Object Lock state of the version versionID that
|
|
|
|
|
// the evaluator decided on, to be checked against the state the set reads.
|
|
|
|
|
func (all lifecycleAllPools) protection(versionID string) objectLockState {
|
|
|
|
|
if versionID == "" {
|
|
|
|
|
return objectLockState{}
|
|
|
|
|
}
|
|
|
|
|
if i := all.version(versionID); i >= 0 {
|
|
|
|
|
return lifecycleLockState(all.versions[i].UserDefined)
|
|
|
|
|
}
|
|
|
|
|
return objectLockState{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// expiryApplies returns nil when the ILM expiry that opts describe applies
|
|
|
|
@@ -237,13 +691,11 @@ func (all lifecycleAllPools) expiryApplies(bucket, object string, opts ObjectOpt
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
for i, oi := range all.versions {
|
|
|
|
|
if oi.VersionID == opts.VersionID || oi.VersionID == "" && opts.VersionID == nullVersionID {
|
|
|
|
|
if all.events[i].Action == lifecycle.DeleteVersionAction {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return errLifecycleNotApplicable
|
|
|
|
|
if i := all.version(opts.VersionID); i >= 0 {
|
|
|
|
|
if all.events[i].Action == lifecycle.DeleteVersionAction {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return errLifecycleNotApplicable
|
|
|
|
|
}
|
|
|
|
|
return VersionNotFound{Bucket: bucket, Object: name, VersionID: opts.VersionID}
|
|
|
|
|
}
|
|
|
|
@@ -328,5 +780,6 @@ func (z *erasureServerPools) migrationExpiryApplies(ctx context.Context, oi Obje
|
|
|
|
|
if !z.expiryNeedsAllPools(opts) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return z.checkExpiryAllPools(ctx, oi.Bucket, encodeDirObject(oi.Name), opts) == nil
|
|
|
|
|
_, err := z.checkExpiryAllPools(ctx, oi.Bucket, encodeDirObject(oi.Name), opts)
|
|
|
|
|
return err == nil
|
|
|
|
|
}
|
|
|
|
|