From 0528429d56282b8280eb20e2665855c078d2ed01 Mon Sep 17 00:00:00 2001 From: Joakim Hindersson Date: Thu, 24 Sep 2026 21:36:57 +0200 Subject: [PATCH 1/3] Add a eager fetcher and fan-out fetch of parity shards on slow reads --- cmd/erasure-decode.go | 251 ++++++++++++++- cmd/erasure-decode_test.go | 420 +++++++++++++++++++++++++ docs/changelog/hedged-erasure-reads.md | 77 +++++ internal/config/drive/drive.go | 29 ++ internal/config/drive/help.go | 9 + 5 files changed, 778 insertions(+), 8 deletions(-) create mode 100644 docs/changelog/hedged-erasure-reads.md diff --git a/cmd/erasure-decode.go b/cmd/erasure-decode.go index f0cc90ab09fda..18ec7e9f5b710 100644 --- a/cmd/erasure-decode.go +++ b/cmd/erasure-decode.go @@ -24,6 +24,7 @@ import ( "io" "sync" "sync/atomic" + "time" xioutil "github.com/minio/minio/internal/ioutil" ) @@ -39,10 +40,39 @@ type parallelReader struct { buf [][]byte readerToBuf []int stashBuffer []byte + + // Hedged read state. When fanoutDelay is > 0 reads start with + // dataBlocks+1 shards in parallel and fan out to all remaining + // shards after fanoutDelay. + fanoutDelay time.Duration + events chan hedgedResult + inflight []bool + curBlock *hedgedBlock + stateLK sync.Mutex + missingPartsHeal atomic.Bool + bitrotHeal atomic.Bool + disksNotFound atomic.Int32 +} + +// hedgedBlock tracks per block launch state of a hedged read. +// All fields are guarded by parallelReader.stateLK. +type hedgedBlock struct { + launched []bool + outstanding int +} + +// hedgedResult is the outcome of one launched shard read. blk +// identifies the block the read was launched for. +type hedgedResult struct { + blk *hedgedBlock + i int + buf []byte + n int + err error } // newParallelReader returns parallelReader. -func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int64) *parallelReader { +func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int64, fanoutDelay time.Duration) *parallelReader { r2b := make([]int, len(readers)) for i := range r2b { r2b[i] = i @@ -51,9 +81,11 @@ func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int shardSize := int(e.ShardSize()) var b []byte - // We should always have enough capacity, but older objects may be bigger - // we do not need stashbuffer for them. - if globalBytePoolCap.Load().WidthCap() >= len(readers)*shardSize { + // When hedging is enabled shard buffers are allocated per reader + // on first use instead of seeding from the byte pool: straggler + // goroutines may outlive the reader and keep writing into their + // buffer after Done() has returned pool memory. + if fanoutDelay <= 0 && globalBytePoolCap.Load().WidthCap() >= len(readers)*shardSize { // Fill buffers b = globalBytePoolCap.Load().Get() // Seed the buffers. @@ -62,7 +94,7 @@ func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int } } - return ¶llelReader{ + p := ¶llelReader{ readers: readers, orgReaders: readers, dataBlocks: e.dataBlocks, @@ -73,6 +105,12 @@ func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int readerToBuf: r2b, stashBuffer: b, } + if fanoutDelay > 0 { + p.fanoutDelay = fanoutDelay + p.events = make(chan hedgedResult, len(readers)) + p.inflight = make([]bool, len(readers)) + } + return p } // Done will release any resources used by the parallelReader. @@ -81,6 +119,18 @@ func (p *parallelReader) Done() { globalBytePoolCap.Load().Put(p.stashBuffer) p.stashBuffer = nil } + if p.inflight != nil { + // Readers still in flight are stragglers of a returned block. + // Nil them in orgReaders so closeBitrotReaders skips them; their + // in-flight goroutine closes the reader after the read returns. + p.stateLK.Lock() + for i := range p.inflight { + if p.inflight[i] { + p.orgReaders[p.readerToBuf[i]] = nil + } + } + p.stateLK.Unlock() + } } // preferReaders can mark readers as preferred. @@ -133,7 +183,6 @@ func (p *parallelReader) Read(dst [][]byte) ([][]byte, error) { newBuf[i] = newBuf[i][:0] } } - var newBufLK sync.RWMutex if p.offset+p.shardSize > p.shardFileSize { p.shardSize = p.shardFileSize - p.offset @@ -142,6 +191,12 @@ func (p *parallelReader) Read(dst [][]byte) ([][]byte, error) { return newBuf, nil } + if p.fanoutDelay > 0 { + return p.readHedged(newBuf) + } + + var newBufLK sync.RWMutex + readTriggerCh := make(chan bool, len(p.readers)) defer xioutil.SafeClose(readTriggerCh) // close the channel upon return @@ -234,6 +289,186 @@ func (p *parallelReader) Read(dst [][]byte) ([][]byte, error) { return nil, fmt.Errorf("%w (offline-disks=%d/%d)", errErasureReadQuorum, disksNotFound, len(p.readers)) } +// readHedged implements the hedged read strategy: it launches +// dataBlocks+1 reads in parallel, substitutes a new shard on error, +// and fans out to all remaining shards after p.fanoutDelay. It +// returns as soon as dataBlocks shards have been delivered, without +// waiting for stragglers. A reader that completes after its block +// returned re-joins on the next block; a straggler still in flight is +// never launched twice, so stateful readers are safe. +func (p *parallelReader) readHedged(newBuf [][]byte) ([][]byte, error) { + p.stateLK.Lock() + + // Drain results of stragglers from a previous block, their + // readers are available again. + for len(p.events) > 0 { + <-p.events + } + + blk := &hedgedBlock{launched: make([]bool, len(p.readers))} + p.curBlock = blk + + // launchNext launches the next launchable reader for this block. + // Must be called with p.stateLK held. + launchNext := func() bool { + for i := range p.readers { + if p.readers[i] == nil || p.inflight[i] || blk.launched[i] { + continue + } + blk.launched[i] = true + p.inflight[i] = true + blk.outstanding++ + bufIdx := p.readerToBuf[i] + if p.buf[bufIdx] == nil { + p.buf[bufIdx] = make([]byte, p.shardSize) + } + buf := p.buf[bufIdx][:p.shardSize] + offset := p.offset + rr := p.readers[i] + go func(i int, bufIdx int, rr io.ReaderAt, buf []byte, offset int64) { + n, err := rr.ReadAt(buf, offset) + p.stateLK.Lock() + p.inflight[i] = false + closeReader := false + if p.orgReaders[bufIdx] == nil { + // Abandoned straggler, marked by Done(); the + // caller no longer tracks this reader so it is + // closed here instead of by closeBitrotReaders. + closeReader = true + } else if err != nil { + switch { + case errors.Is(err, errFileNotFound): + p.missingPartsHeal.Store(true) + case errors.Is(err, errFileCorrupt): + p.bitrotHeal.Store(true) + case errors.Is(err, errDiskNotFound): + p.disksNotFound.Add(1) + } + p.readers[i] = nil + // This will be communicated upstream. + p.orgReaders[bufIdx] = nil + closeReader = true + } + p.stateLK.Unlock() + if closeReader { + if closer, ok := rr.(io.Closer); ok { + closer.Close() + } + } + p.events <- hedgedResult{blk: blk, i: i, buf: buf, n: n, err: err} + }(i, bufIdx, rr, buf, offset) + return true + } + return false + } + + for n := 0; n < p.dataBlocks+1; n++ { + // Launch dataBlocks+1 reads so that a single slow or + // missing shard is covered without waiting for the fan-out. + if !launchNext() { + break + } + } + + fanoutPossible := false + for i := range p.readers { + if p.readers[i] != nil && !p.inflight[i] && !blk.launched[i] { + fanoutPossible = true + break + } + } + var timer *time.Timer + if fanoutPossible { + timer = time.AfterFunc(p.fanoutDelay, func() { + p.stateLK.Lock() + defer p.stateLK.Unlock() + if p.curBlock != blk { + return + } + for launchNext() { + } + }) + } + p.stateLK.Unlock() + + defer func() { + if timer != nil { + timer.Stop() + } + p.stateLK.Lock() + if p.curBlock == blk { + p.curBlock = nil + } + p.stateLK.Unlock() + }() + + bufCount := 0 + for bufCount < p.dataBlocks { + p.stateLK.Lock() + if blk.outstanding == 0 { + launched := launchNext() + waiting := false + if !launched { + for i := range p.inflight { + if p.inflight[i] { + waiting = true + break + } + } + } + p.stateLK.Unlock() + switch { + case launched: + continue + case waiting: + // All launchable readers are exhausted but stragglers + // of earlier blocks may still deliver. + default: + // If we cannot decode, just return read quorum error. + return nil, fmt.Errorf("%w (offline-disks=%d/%d)", errErasureReadQuorum, p.disksNotFound.Load(), len(p.readers)) + } + } else { + p.stateLK.Unlock() + } + + ev := <-p.events + p.stateLK.Lock() + if ev.blk == blk { + blk.outstanding-- + } + switch { + case ev.blk == blk && ev.err == nil: + p.stateLK.Unlock() + newBuf[p.readerToBuf[ev.i]] = ev.buf[:ev.n] + bufCount++ + case ev.blk == blk: + // Since ReadAt returned error, launch a substitute shard. + launchNext() + p.stateLK.Unlock() + default: + // Stale result of an earlier block; its reader has been + // marked available and can be relaunched on the next + // iteration. + p.stateLK.Unlock() + } + } + + p.stateLK.Lock() + if p.curBlock == blk { + p.curBlock = nil + } + p.stateLK.Unlock() + + p.offset += p.shardSize + switch { + case p.missingPartsHeal.Load(): + return newBuf, errFileNotFound + case p.bitrotHeal.Load(): + return newBuf, errFileCorrupt + } + return newBuf, nil +} + // Decode reads from readers, reconstructs data if needed and writes the data to the writer. // A set of preferred drives can be supplied. In that case they will be used and the data reconstructed. func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []io.ReaderAt, offset, length, totalLength int64, prefer []bool) (written int64, derr error) { @@ -248,7 +483,7 @@ func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []io.Read return 0, nil } - reader := newParallelReader(readers, e, offset, totalLength) + reader := newParallelReader(readers, e, offset, totalLength, globalDriveConfig.GetReadFanoutDelay()) if len(prefer) == len(readers) { reader.preferReaders(prefer) } @@ -319,7 +554,7 @@ func (e Erasure) Heal(ctx context.Context, writers []io.Writer, readers []io.Rea return errInvalidArgument } - reader := newParallelReader(readers, e, 0, totalLength) + reader := newParallelReader(readers, e, 0, totalLength, 0) if len(readers) == len(prefer) { reader.preferReaders(prefer) } diff --git a/cmd/erasure-decode_test.go b/cmd/erasure-decode_test.go index 229047e16a750..5d3bd6ca38c7a 100644 --- a/cmd/erasure-decode_test.go +++ b/cmd/erasure-decode_test.go @@ -21,11 +21,17 @@ import ( "bytes" "context" crand "crypto/rand" + "errors" "io" "math/rand" + "sync" + "sync/atomic" "testing" + "time" "github.com/dustin/go-humanize" + "github.com/minio/minio/internal/bpool" + "github.com/minio/minio/internal/config/drive" ) func (a badDisk) ReadFile(ctx context.Context, volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) { @@ -84,6 +90,12 @@ var erasureDecodeTests = []struct { } func TestErasureDecode(t *testing.T) { + // The streaming bitrot writer draws from the global byte pool + // which is otherwise only initialized during server pool setup. + if globalBytePoolCap.Load() == nil { + globalBytePoolCap.Store(bpool.NewBytePoolCap(64, int(blockSizeV2), 2*int(blockSizeV2))) + } + for i, test := range erasureDecodeTests { setup, err := newErasureTestSetup(t, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize) if err != nil { @@ -201,6 +213,11 @@ func TestErasureDecodeRandomOffsetLength(t *testing.T) { if testing.Short() { t.Skip() } + // The streaming bitrot writer draws from the global byte pool + // which is otherwise only initialized during server pool setup. + if globalBytePoolCap.Load() == nil { + globalBytePoolCap.Store(bpool.NewBytePoolCap(64, int(blockSizeV2), 2*int(blockSizeV2))) + } // Initialize environment needed for the test. dataBlocks := 7 parityBlocks := 7 @@ -383,3 +400,406 @@ func BenchmarkErasureDecode_16_40MB(b *testing.B) { b.Run(" XXXX0000|XXXX0000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 4, 4, size, b) }) b.Run(" XXXXXXXX|00000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 8, 0, size, b) }) } + +// gatedReaderAt blocks the first ReadAt call until gate is closed. +// All later calls pass through. started is closed once the first +// ReadAt is blocked, done once it has returned. +type gatedReaderAt struct { + inner io.ReaderAt + gate chan struct{} + started chan struct{} + done chan struct{} + block sync.Once + calls atomic.Int32 +} + +func (g *gatedReaderAt) ReadAt(p []byte, off int64) (int, error) { + g.calls.Add(1) + g.block.Do(func() { + close(g.started) + <-g.gate + close(g.done) + }) + return g.inner.ReadAt(p, off) +} + +func (g *gatedReaderAt) Close() error { + if c, ok := g.inner.(io.Closer); ok { + return c.Close() + } + return nil +} + +// missingShardReader always fails with errFileNotFound. +type missingShardReader struct{} + +func (missingShardReader) ReadAt([]byte, int64) (int, error) { return 0, errFileNotFound } + +// hedgedEncodeShards encodes data block by block and returns the +// shard files, mimicking the layout produced by Erasure.Encode. +func hedgedEncodeShards(t *testing.T, e Erasure, data []byte) [][]byte { + t.Helper() + shards := make([][]byte, e.dataBlocks+e.parityBlocks) + for off := 0; off < len(data); off += int(e.blockSize) { + chunk := data[off:] + if len(chunk) > int(e.blockSize) { + chunk = chunk[:int(e.blockSize)] + } + encoded, err := e.EncodeData(t.Context(), chunk) + if err != nil { + t.Fatalf("EncodeData failed: %v", err) + } + for i := range shards { + shards[i] = append(shards[i], encoded[i]...) + } + } + return shards +} + +func hedgedDecodeShards(t *testing.T, e Erasure, bufs [][]byte) []byte { + t.Helper() + if err := e.DecodeDataBlocks(bufs); err != nil { + t.Fatalf("DecodeDataBlocks failed: %v", err) + } + var out []byte + for i := 0; i < e.dataBlocks; i++ { + out = append(out, bufs[i]...) + } + return out +} + +func hedgedDeliveredCount(bufs [][]byte) int { + n := 0 + for _, b := range bufs { + if len(b) > 0 { + n++ + } + } + return n +} + +func hedgedShardReaders(t *testing.T, shards [][]byte) []io.ReaderAt { + t.Helper() + readers := make([]io.ReaderAt, len(shards)) + for i := range shards { + readers[i] = bytes.NewReader(shards[i]) + } + return readers +} + +// TestParallelReaderHedgedOneSlow verifies that a single slow shard is +// covered by the dataBlocks+1 initial launch without waiting for the +// fan-out delay or the slow disk. +func TestParallelReaderHedgedOneSlow(t *testing.T) { + const dataBlocks = 3 + e, err := NewErasure(t.Context(), dataBlocks, 2, 30) + if err != nil { + t.Fatal(err) + } + data := make([]byte, 60) + for i := range data { + data[i] = byte(i) + } + shards := hedgedEncodeShards(t, e, data) + readers := hedgedShardReaders(t, shards) + + gated := &gatedReaderAt{ + inner: readers[0], + gate: make(chan struct{}), + started: make(chan struct{}), + done: make(chan struct{}), + } + readers[0] = gated + + const fanoutDelay = 10 * time.Second + p := newParallelReader(readers, e, 0, int64(len(data)), fanoutDelay) + defer p.Done() + + for block := 0; block < 2; block++ { + start := time.Now() + bufs, err := p.Read(nil) + if err != nil { + t.Fatalf("block %d: Read failed: %v", block, err) + } + if elapsed := time.Since(start); elapsed > 5*time.Second { + t.Fatalf("block %d: Read blocked for %v, hedged read should not wait for the slow shard", block, elapsed) + } + if got := hedgedDeliveredCount(bufs); got != dataBlocks { + t.Fatalf("block %d: got %d delivered shards, want %d", block, got, dataBlocks) + } + if got := hedgedDecodeShards(t, e, bufs); !bytes.Equal(got, data[block*30:(block+1)*30]) { + t.Fatalf("block %d: decoded data mismatch", block) + } + } + + // The slow reader was launched once per block, the never-needed + // 5th shard was never launched since the hedge sufficed. + if got := gated.calls.Load(); got != 1 { + t.Fatalf("gated reader launched %d times, want 1", got) + } + + p.Done() + p.stateLK.Lock() + if readers[0] != nil { + t.Fatal("straggler reader should be marked nil in orgReaders after Done()") + } + p.stateLK.Unlock() + + close(gated.gate) + <-gated.done +} + +// TestParallelReaderHedgedFanout verifies that the fan-out timer +// launches the remaining shards after fanoutDelay when two shards +// are slow. +func TestParallelReaderHedgedFanout(t *testing.T) { + const dataBlocks = 3 + e, err := NewErasure(t.Context(), dataBlocks, 2, 30) + if err != nil { + t.Fatal(err) + } + data := make([]byte, 30) + for i := range data { + data[i] = byte(i) + } + shards := hedgedEncodeShards(t, e, data) + readers := hedgedShardReaders(t, shards) + + gated0 := &gatedReaderAt{inner: readers[0], gate: make(chan struct{}), started: make(chan struct{}), done: make(chan struct{})} + gated1 := &gatedReaderAt{inner: readers[1], gate: make(chan struct{}), started: make(chan struct{}), done: make(chan struct{})} + readers[0] = gated0 + readers[1] = gated1 + + const fanoutDelay = 300 * time.Millisecond + p := newParallelReader(readers, e, 0, int64(len(data)), fanoutDelay) + defer p.Done() + + start := time.Now() + bufs, err := p.Read(nil) + if err != nil { + t.Fatalf("Read failed: %v", err) + } + if elapsed := time.Since(start); elapsed < 200*time.Millisecond { + t.Fatalf("Read returned after %v, fan-out delay did not trigger", elapsed) + } + if elapsed := time.Since(start); elapsed > 5*time.Second { + t.Fatalf("Read returned after %v, too slow", elapsed) + } + if got := hedgedDeliveredCount(bufs); got != dataBlocks { + t.Fatalf("got %d delivered shards, want %d", got, dataBlocks) + } + if got := hedgedDecodeShards(t, e, bufs); !bytes.Equal(got, data) { + t.Fatal("decoded data mismatch") + } + + close(gated0.gate) + close(gated1.gate) +} + +// TestParallelReaderHedgedErrorSubstitution verifies that a shard +// failing immediately is substituted right away, without waiting +// for the fan-out delay. +func TestParallelReaderHedgedErrorSubstitution(t *testing.T) { + const dataBlocks = 3 + e, err := NewErasure(t.Context(), dataBlocks, 2, 30) + if err != nil { + t.Fatal(err) + } + data := make([]byte, 30) + for i := range data { + data[i] = byte(i) + } + shards := hedgedEncodeShards(t, e, data) + readers := hedgedShardReaders(t, shards) + readers[0] = missingShardReader{} + + const fanoutDelay = 10 * time.Second + p := newParallelReader(readers, e, 0, int64(len(data)), fanoutDelay) + defer p.Done() + + start := time.Now() + bufs, err := p.Read(nil) + if !errors.Is(err, errFileNotFound) { + t.Fatalf("expected errFileNotFound heal flag, got %v", err) + } + if elapsed := time.Since(start); elapsed > 5*time.Second { + t.Fatalf("Read blocked for %v, substitution should be immediate", elapsed) + } + if got := hedgedDeliveredCount(bufs); got != dataBlocks { + t.Fatalf("got %d delivered shards, want %d", got, dataBlocks) + } + if got := hedgedDecodeShards(t, e, bufs); !bytes.Equal(got, data) { + t.Fatal("decoded data mismatch") + } +} + +// TestParallelReaderHedgedQuorum verifies the read quorum error when +// too many shards fail. +func TestParallelReaderHedgedQuorum(t *testing.T) { + const dataBlocks = 3 + e, err := NewErasure(t.Context(), dataBlocks, 2, 30) + if err != nil { + t.Fatal(err) + } + data := make([]byte, 30) + shards := hedgedEncodeShards(t, e, data) + readers := hedgedShardReaders(t, shards) + readers[0] = missingShardReader{} + readers[1] = missingShardReader{} + readers[2] = missingShardReader{} + + p := newParallelReader(readers, e, 0, int64(len(data)), 10*time.Second) + defer p.Done() + + bufs, err := p.Read(nil) + if !errors.Is(err, errErasureReadQuorum) { + t.Fatalf("expected errErasureReadQuorum, got %v", err) + } + if bufs != nil { + t.Fatal("expected nil bufs on quorum error") + } +} + +// TestParallelReaderHedgedRejoin verifies that a straggler that +// completes after its block returned rejoins for the next block. +func TestParallelReaderHedgedRejoin(t *testing.T) { + const dataBlocks = 3 + e, err := NewErasure(t.Context(), dataBlocks, 2, 30) + if err != nil { + t.Fatal(err) + } + data := make([]byte, 60) + for i := range data { + data[i] = byte(i) + } + shards := hedgedEncodeShards(t, e, data) + readers := hedgedShardReaders(t, shards) + + gated := &gatedReaderAt{ + inner: readers[0], + gate: make(chan struct{}), + started: make(chan struct{}), + done: make(chan struct{}), + } + readers[0] = gated + + p := newParallelReader(readers, e, 0, int64(len(data)), 10*time.Second) + defer p.Done() + + // Block 0: reader 0 straggles, delivered via hedge. + bufs, err := p.Read(nil) + if err != nil { + t.Fatalf("block 0: Read failed: %v", err) + } + if got := hedgedDecodeShards(t, e, bufs); !bytes.Equal(got, data[0:30]) { + t.Fatal("block 0: decoded data mismatch") + } + + // Release the straggler and wait until it is available again. + close(gated.gate) + deadline := time.Now().Add(5 * time.Second) + for { + p.stateLK.Lock() + inflight := p.inflight[0] + p.stateLK.Unlock() + if !inflight { + break + } + if time.Now().After(deadline) { + t.Fatal("straggler never completed") + } + time.Sleep(time.Millisecond) + } + + // Block 1: reader 0 must have rejoined and be launched again. + bufs, err = p.Read(nil) + if err != nil { + t.Fatalf("block 1: Read failed: %v", err) + } + if got := hedgedDeliveredCount(bufs); got != dataBlocks { + t.Fatalf("block 1: got %d delivered shards, want %d", got, dataBlocks) + } + if got := hedgedDecodeShards(t, e, bufs); !bytes.Equal(got, data[30:60]) { + t.Fatal("block 1: decoded data mismatch") + } + if got := gated.calls.Load(); got != 2 { + t.Fatalf("gated reader launched %d times, want 2", got) + } +} + +// TestErasureDecodeHedgedSlowDisk verifies the full decode path with +// hedged reads enabled: a disk stuck in a read does not stall the +// object read. +func TestErasureDecodeHedgedSlowDisk(t *testing.T) { + // The streaming bitrot writer draws from the global byte pool + // which is otherwise only initialized during server pool setup. + if globalBytePoolCap.Load() == nil { + globalBytePoolCap.Store(bpool.NewBytePoolCap(64, int(blockSizeV2), 2*int(blockSizeV2))) + } + + dataBlocks := 3 + parityBlocks := 2 + setup, err := newErasureTestSetup(t, dataBlocks, parityBlocks, blockSizeV2) + if err != nil { + t.Fatalf("failed to create test setup: %v", err) + } + erasure, err := NewErasure(t.Context(), dataBlocks, parityBlocks, blockSizeV2) + if err != nil { + t.Fatalf("failed to create ErasureStorage: %v", err) + } + + data := make([]byte, oneMiByte) + if _, err = io.ReadFull(crand.Reader, data); err != nil { + t.Fatal(err) + } + + buffer := make([]byte, blockSizeV2, 2*blockSizeV2) + writers := make([]io.Writer, len(setup.disks)) + for i, disk := range setup.disks { + writers[i] = newBitrotWriter(disk, "", "testbucket", "object", erasure.ShardFileSize(int64(len(data))), DefaultBitrotAlgorithm, erasure.ShardSize()) + } + if _, err = erasure.Encode(t.Context(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1); err != nil { + t.Fatal(err) + } + closeBitrotWriters(writers) + + bitrotReaders := make([]io.ReaderAt, len(setup.disks)) + for index, disk := range setup.disks { + tillOffset := erasure.ShardFileOffset(0, int64(len(data)), int64(len(data))) + bitrotReaders[index] = newBitrotReader(disk, nil, "testbucket", "object", tillOffset, DefaultBitrotAlgorithm, bitrotWriterSum(writers[index]), erasure.ShardSize()) + } + gated := &gatedReaderAt{ + inner: bitrotReaders[0], + gate: make(chan struct{}), + started: make(chan struct{}), + done: make(chan struct{}), + } + bitrotReaders[0] = gated + + globalDriveConfig.Update(drive.Config{ReadFanoutDelay: 10 * time.Second}) + defer globalDriveConfig.Update(drive.Config{}) + + writer := bytes.NewBuffer(nil) + start := time.Now() + _, err = erasure.Decode(t.Context(), writer, bitrotReaders, 0, int64(len(data)), int64(len(data)), nil) + if err != nil { + t.Fatalf("Decode failed: %v", err) + } + if elapsed := time.Since(start); elapsed > 5*time.Second { + t.Fatalf("Decode blocked for %v, slow disk should not stall the read", elapsed) + } + if !bytes.Equal(writer.Bytes(), data) { + t.Fatal("read returns wrong file content") + } + + // The straggler is marked in the caller's reader slice so that + // closeBitrotReaders skips it; its goroutine closes it after the + // read returns. + if bitrotReaders[0] != nil { + t.Fatal("straggler reader should be marked nil for the caller") + } + + close(gated.gate) + <-gated.done + closeBitrotReaders(bitrotReaders) +} diff --git a/docs/changelog/hedged-erasure-reads.md b/docs/changelog/hedged-erasure-reads.md new file mode 100644 index 0000000000000..b9e3a04c8b8da --- /dev/null +++ b/docs/changelog/hedged-erasure-reads.md @@ -0,0 +1,77 @@ +# Changelog: hedged erasure reads (read_fanout_delay) + +## Summary + +Adds an opt-in "hedged read" strategy for erasure-coded object reads +(`GET`/range reads). A single slow drive no longer stalls a read; the +read races `K+1` shards (K = data blocks) and, if that is not enough, +fans out to all remaining shards after a configurable delay. + +## Configuration + +New `drive` subsystem setting, applied dynamically (no restart): + +``` +mc admin config set drive.read_fanout_delay=250ms +``` + +- `0` (default): current behavior, hedging disabled. +- `250ms` (example): enable hedged reads; value is per-block fan-out delay. +- Environment override: `MINIO_DRIVE_READ_FANOUT_DELAY=250ms`. + The environment variable takes precedence over the config value. + +## Behavior when enabled + +Per erasure block (`Erasure.Decode`, normal object reads only): + +1. Launches reads on the first `K+1` online shards (local disks preferred + via the existing `prefer` ordering). One slow or missing shard is + covered instantly at the cost of a single extra shard read. +2. A shard that fails immediately (missing, corrupt, disk error) triggers + an immediate substitute read, as before. +3. If `K` verified shards have not arrived within `read_fanout_delay`, + reads are launched on every remaining shard in parallel. +4. The block completes as soon as `K` bitrot-verified shards (data or + parity) have arrived; the decode step reconstructs missing data + shards from whatever mix arrived. +5. Readers that lose the race ("stragglers") are not waited on. A + straggler that completes late is discarded for its own block and + rejoins for the next block; while slow it is simply skipped. + +Healing (`Erasure.Heal`) and all write paths are unchanged. Read quorum +failure still surfaces as `SlowDownRead` (503). + +## Overheads (when enabled) + +- Steady state: one extra shard read per block (`K+1` instead of `K`). +- Full fan-out reads only on blocks that actually stall for the + configured delay; shards read after the block completed are wasted + reads, bounded by the fan-out. +- RAM: shard buffers are allocated per launched reader instead of + seeded from the shared byte pool (straggler goroutines may outlive + the reader); worst case is `N x shardSize` per in-flight part read. + +## Operational notes + +- Sensible starting point: `250ms`. Must stay well below + `drive.max_timeout` (default 30s). +- Bitrot verification applies to every shard that wins the race, so + the integrity guarantees are unchanged: corrupt shards are replaced + by parity on the fly and the object is queued for async heal. +- Tune with the existing disk metrics (`total-errs-timeout`) and + observe straggler frequency before lowering `drive.max_timeout`; a + slow drive now loses block races instead of blocking them, so the + two settings compound. + +## Files changed + +- `internal/config/drive/drive.go`, `internal/config/drive/help.go`: + new `read_fanout_delay` key, env override, dynamic update, help text. +- `cmd/erasure-decode.go`: hedged read implementation in + `parallelReader` (`readHedged`), in-flight reader tracking, straggler + re-join/abandon lifecycle; `Erasure.Decode` wires the config, + `Erasure.Heal` stays on the plain path. +- `cmd/erasure-decode_test.go`: hedged unit tests (hedge coverage, + fan-out timer, error substitution, quorum error, straggler rejoin), + full-stack slow-disk decode test, and byte-pool self-initialization + for the decode tests (fixes pre-existing panic when run standalone). \ No newline at end of file diff --git a/internal/config/drive/drive.go b/internal/config/drive/drive.go index 862c62ab76b79..7f4186754c8dd 100644 --- a/internal/config/drive/drive.go +++ b/internal/config/drive/drive.go @@ -18,6 +18,7 @@ package drive import ( + "fmt" "sync" "time" @@ -30,6 +31,7 @@ const ( EnvMaxDriveTimeout = "MINIO_DRIVE_MAX_TIMEOUT" EnvMaxDriveTimeoutLegacy = "_MINIO_DRIVE_MAX_TIMEOUT" EnvMaxDiskTimeoutLegacy = "_MINIO_DISK_MAX_TIMEOUT" + EnvReadFanoutDelay = "MINIO_DRIVE_READ_FANOUT_DELAY" ) // DefaultKVS - default KVS for drive @@ -38,6 +40,10 @@ var DefaultKVS = config.KVS{ Key: MaxTimeout, Value: "30s", }, + config.KV{ + Key: ReadFanoutDelay, + Value: "0", + }, } var configLk sync.RWMutex @@ -46,6 +52,10 @@ var configLk sync.RWMutex type Config struct { // MaxTimeout - maximum timeout for a drive operation MaxTimeout time.Duration `json:"maxTimeout"` + + // ReadFanoutDelay - delay after which a hedged erasure read fans out + // to all remaining shards, 0 disables hedged reads. + ReadFanoutDelay time.Duration `json:"readFanoutDelay"` } // Update - updates the config with latest values @@ -53,6 +63,7 @@ func (c *Config) Update(updated Config) error { configLk.Lock() defer configLk.Unlock() c.MaxTimeout = getMaxTimeout(updated.MaxTimeout) + c.ReadFanoutDelay = updated.ReadFanoutDelay return nil } @@ -61,6 +72,14 @@ func (c *Config) GetMaxTimeout() time.Duration { return c.GetOPTimeout() } +// GetReadFanoutDelay - returns the delay after which hedged erasure +// reads fan out to all remaining shards. 0 disables hedged reads. +func (c *Config) GetReadFanoutDelay() time.Duration { + configLk.RLock() + defer configLk.RUnlock() + return c.ReadFanoutDelay +} + // GetOPTimeout - returns the per call drive operation timeout func (c *Config) GetOPTimeout() time.Duration { configLk.RLock() @@ -90,6 +109,16 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) { cfg.MaxTimeout = getMaxTimeout(dur) } } + + fanoutDelay, err := time.ParseDuration(env.Get(EnvReadFanoutDelay, kvs.GetWithDefault(ReadFanoutDelay, DefaultKVS))) + if err != nil { + return cfg, err + } + if fanoutDelay < 0 { + return cfg, fmt.Errorf("invalid value %v for read_fanout_delay", fanoutDelay) + } + cfg.ReadFanoutDelay = fanoutDelay + return cfg, err } diff --git a/internal/config/drive/help.go b/internal/config/drive/help.go index 5964dcce4181f..37b42e313cb0b 100644 --- a/internal/config/drive/help.go +++ b/internal/config/drive/help.go @@ -23,6 +23,9 @@ var ( // MaxTimeout is the max timeout for drive MaxTimeout = "max_timeout" + // ReadFanoutDelay is the delay before hedged erasure reads fan out to all shards + ReadFanoutDelay = "read_fanout_delay" + // HelpDrive is help for drive HelpDrive = config.HelpKVS{ config.HelpKV{ @@ -31,5 +34,11 @@ var ( Description: "set per call max_timeout for the drive, defaults to 30 seconds", Optional: true, }, + config.HelpKV{ + Key: ReadFanoutDelay, + Type: "string", + Description: "hedged erasure read fan-out delay, e.g. '250ms'. Reads start with K+1 shards and fan out to all shards after this delay. '0' (default) disables hedged reads", + Optional: true, + }, } ) From c78680b11ca8f30573096a9a703139e466a55c93 Mon Sep 17 00:00:00 2001 From: Joakim Hindersson Date: Thu, 24 Sep 2026 22:30:10 +0200 Subject: [PATCH 2/3] security: fix CVE-2026-41145, CVE-2026-40344, CVE-2026-34204, CVE-2026-33322 - CVE-2026-41145: only accept Authorization header credentials for STREAMING-UNSIGNED-PAYLOAD-TRAILER auth type. - CVE-2026-40344: add missing authTypeStreamingUnsignedTrailer handling in PutObjectExtractHandler (Snowball auto-extract). - CVE-2026-34204: ignore X-Minio-Replication-* SSE metadata headers on non-replication PutObject/PostPolicy requests. - CVE-2026-33322: restrict OIDC JWT ValidMethods to algorithms advertised by the provider, defaulting to asymmetric only, and only register the client secret as HMAC key when HMAC is advertised. Includes regression tests for all four CVEs. --- cmd/auth-handler.go | 6 +- cmd/bucket-handlers.go | 4 +- cmd/handler-utils.go | 17 ++- cmd/handler-utils_test.go | 29 ++++- cmd/object-handlers.go | 12 ++- cmd/server_test.go | 114 ++++++++++++++++++++ cmd/signature-v4-parser.go | 17 +++ internal/config/identity/openid/jwt.go | 46 +++++--- internal/config/identity/openid/jwt_test.go | 68 ++++++++++++ 9 files changed, 290 insertions(+), 23 deletions(-) diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go index 6e824e3126d46..983195241f0dd 100644 --- a/cmd/auth-handler.go +++ b/cmd/auth-handler.go @@ -731,7 +731,11 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN case authTypeStreamingSigned, authTypePresigned, authTypeSigned, authTypeStreamingSignedTrailer: cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3) case authTypeStreamingUnsignedTrailer: - cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3) + // For unsigned trailer streaming the request must either be fully + // signed (credentials come from the Authorization header) or be + // anonymous. Query-string X-Amz-Credential is not signature-verified + // for this auth type and must not be used for authorization. + cred, owner, s3Err = getReqAccessKeyV4FromHeader(r, region, serviceS3) if s3Err == ErrMissingFields { // Could be anonymous. cred + owner is zero value. s3Err = ErrNone diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index 564572f2a94a6..67f3771bd4943 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -1253,8 +1253,10 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h } // Extract metadata to be saved from received Form. + // Post-policy uploads are never replication requests, so replication + // SSE headers must not be accepted (CVE-2026-34204). metadata := make(map[string]string) - err = extractMetadataFromMime(ctx, textproto.MIMEHeader(formValues), metadata) + err = extractMetadataFromMime(ctx, textproto.MIMEHeader(formValues), metadata, false) if err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return diff --git a/cmd/handler-utils.go b/cmd/handler-utils.go index 7752cfece2db3..e8ef441f8774d 100644 --- a/cmd/handler-utils.go +++ b/cmd/handler-utils.go @@ -29,6 +29,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/bucket/replication" "github.com/minio/minio/internal/handlers" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" @@ -142,15 +143,18 @@ var userMetadataKeyPrefixes = []string{ // extractMetadataFromReq extracts metadata from HTTP header and HTTP queryString. func extractMetadataFromReq(ctx context.Context, r *http.Request) (metadata map[string]string, err error) { - return extractMetadata(ctx, textproto.MIMEHeader(r.Form), textproto.MIMEHeader(r.Header)) + // CVE-2026-34204: only replication traffic is allowed to set internal SSE + // metadata via X-Minio-Replication-* headers. + isReplica := r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() + return extractMetadata(ctx, isReplica, textproto.MIMEHeader(r.Form), textproto.MIMEHeader(r.Header)) } -func extractMetadata(ctx context.Context, mimesHeader ...textproto.MIMEHeader) (metadata map[string]string, err error) { +func extractMetadata(ctx context.Context, isReplica bool, mimesHeader ...textproto.MIMEHeader) (metadata map[string]string, err error) { metadata = make(map[string]string) for _, hdr := range mimesHeader { // Extract all query values. - err = extractMetadataFromMime(ctx, hdr, metadata) + err = extractMetadataFromMime(ctx, hdr, metadata, isReplica) if err != nil { return nil, err } @@ -191,7 +195,9 @@ func extractMetadata(ctx context.Context, mimesHeader ...textproto.MIMEHeader) ( } // extractMetadata extracts metadata from map values. -func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[string]string) error { +// When allowReplicationHeaders is false, X-Minio-Replication-* headers that +// map to internal SSE metadata are ignored (CVE-2026-34204). +func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[string]string, allowReplicationHeaders bool) error { if v == nil { bugLogIf(ctx, errInvalidArgument) return errInvalidArgument @@ -208,6 +214,9 @@ func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[ value, ok := nv[http.CanonicalHeaderKey(supportedHeader)] if ok { if v, ok := replicationToInternalHeaders[supportedHeader]; ok { + if !allowReplicationHeaders { + continue + } m[v] = strings.Join(value, ",") } else { m[supportedHeader] = strings.Join(value, ",") diff --git a/cmd/handler-utils_test.go b/cmd/handler-utils_test.go index 517f93fcc9fbd..caaec920a9e26 100644 --- a/cmd/handler-utils_test.go +++ b/cmd/handler-utils_test.go @@ -158,12 +158,21 @@ func TestExtractMetadataHeaders(t *testing.T) { metadata: nil, shouldFail: true, }, + // CVE-2026-34204: X-Minio-Replication-* SSE headers must not be + // accepted from regular (non-replication) requests. + { + header: http.Header{ + "X-Minio-Replication-Server-Side-Encryption-Sealed-Key": []string{"injected-value"}, + }, + metadata: map[string]string{}, + shouldFail: false, + }, } // Validate if the extracting headers. for i, testCase := range testCases { metadata := make(map[string]string) - err := extractMetadataFromMime(t.Context(), textproto.MIMEHeader(testCase.header), metadata) + err := extractMetadataFromMime(t.Context(), textproto.MIMEHeader(testCase.header), metadata, false) if err != nil && !testCase.shouldFail { t.Fatalf("Test %d failed to extract metadata: %v", i+1, err) } @@ -176,6 +185,24 @@ func TestExtractMetadataHeaders(t *testing.T) { } } +// Tests that replication SSE headers are accepted when explicitly allowed. +func TestExtractMetadataHeadersReplicationAllowed(t *testing.T) { + metadata := make(map[string]string) + header := http.Header{ + "X-Minio-Replication-Server-Side-Encryption-Sealed-Key": []string{"replica-value"}, + } + err := extractMetadataFromMime(t.Context(), textproto.MIMEHeader(header), metadata, true) + if err != nil { + t.Fatalf("failed to extract metadata: %v", err) + } + expected := map[string]string{ + "X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "replica-value", + } + if !reflect.DeepEqual(metadata, expected) { + t.Fatalf("Expected %#v, got %#v", expected, metadata) + } +} + // Test getResource() func TestGetResource(t *testing.T) { testCases := []struct { diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index 6542c46554f89..dfc77b3db2935 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -2242,7 +2242,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h // if Content-Length is unknown/missing, deny the request size := r.ContentLength rAuthType := getRequestAuthType(r) - if rAuthType == authTypeStreamingSigned || rAuthType == authTypeStreamingSignedTrailer { + if rAuthType == authTypeStreamingSigned || rAuthType == authTypeStreamingSignedTrailer || rAuthType == authTypeStreamingUnsignedTrailer { if sizeStr, ok := r.Header[xhttp.AmzDecodedContentLength]; ok { if sizeStr[0] == "" { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL) @@ -2296,6 +2296,13 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) return } + case authTypeStreamingUnsignedTrailer: + // Initialize stream chunked reader with optional trailers. + reader, s3Err = newUnsignedV4ChunkedReader(r, true, r.Header.Get(xhttp.Authorization) != "") + if s3Err != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } case authTypeSignedV2, authTypePresignedV2: s3Err = isReqAuthenticatedV2(r) if s3Err != ErrNone { @@ -2413,7 +2420,8 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h hdrs.Set(k, v) } } - m, err := extractMetadata(ctx, textproto.MIMEHeader(hdrs)) + isReplica := r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() + m, err := extractMetadata(ctx, isReplica, textproto.MIMEHeader(hdrs)) if err != nil { return err } diff --git a/cmd/server_test.go b/cmd/server_test.go index e69117351544b..97a8650b04e41 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -18,6 +18,7 @@ package cmd import ( + "archive/tar" "bytes" "context" "encoding/xml" @@ -128,6 +129,8 @@ func runAllTests(suite *TestSuiteCommon, c *check) { suite.TestBucketSQSNotificationWebHook(c) suite.TestBucketSQSNotificationAMQP(c) suite.TestUnsignedCVE(c) + suite.TestUnsignedTrailerQueryStringCVE(c) + suite.TestSnowballAutoExtractCVE(c) suite.TearDownSuite(c) } @@ -409,6 +412,117 @@ func (s *TestSuiteCommon) TestUnsignedCVE(c *check) { c.Assert(response.StatusCode, http.StatusBadRequest) } +func (s *TestSuiteCommon) TestUnsignedTrailerQueryStringCVE(c *check) { + c.Helper() + + // generate a random bucket Name. + bucketName := getRandomBucketName() + + // HTTP request to create the bucket. + request, err := newTestSignedRequest(http.MethodPut, getMakeBucketURL(s.endPoint, bucketName), + 0, nil, s.accessKey, s.secretKey, s.signer) + c.Assert(err, nil) + + // execute the request. + response, err := s.client.Do(request) + c.Assert(err, nil) + c.Assert(response.StatusCode, http.StatusOK) + + now := UTCNow() + scope := fmt.Sprintf("%s/us-east-1/s3/aws4_request", now.Format(yyyymmdd)) + putURL := getPutObjectURL(s.endPoint, bucketName, "test-cve-object.txt") + // Inject a valid access key via query string, mimicking the + // CVE-2026-41145 attack vector. + putURL = putURL + "?" + xhttp.AmzCredential + "=" + url.QueryEscape(s.accessKey+"/"+scope) + + req, err := http.NewRequest(http.MethodPut, putURL, nil) + c.Assert(err, nil) + + req.Body = io.NopCloser(bytes.NewReader([]byte("foobar!\n"))) + req.Trailer = http.Header{} + req.Trailer.Set("x-amz-checksum-crc32", "rK0DXg==") + + req = signer.StreamingUnsignedV4(req, "", 8, now) + + // Remove the Authorization header; the attacker only provides the + // access key via the query string and does not know the secret key. + req.Header.Del("Authorization") + + // Ensure the headers expected for unsigned trailer streaming remain. + req.Header.Set("X-Amz-Decoded-Content-Length", "8") + req.Header.Set("Content-Encoding", "aws-chunked") + req.Header.Set("X-Amz-Trailer", "x-amz-checksum-crc32") + req.Header.Set("x-amz-content-sha256", unsignedPayloadTrailer) + + // execute the request. + response, err = s.client.Do(req) + c.Assert(err, nil) + + // The request must be rejected because query-string credentials are not + // signature-verified for unsigned trailer streaming. + c.Assert(response.StatusCode, http.StatusForbidden) +} + +func (s *TestSuiteCommon) TestSnowballAutoExtractCVE(c *check) { + c.Helper() + + // generate a random bucket Name. + bucketName := getRandomBucketName() + + // HTTP request to create the bucket. + request, err := newTestSignedRequest(http.MethodPut, getMakeBucketURL(s.endPoint, bucketName), + 0, nil, s.accessKey, s.secretKey, s.signer) + c.Assert(err, nil) + + // execute the request. + response, err := s.client.Do(request) + c.Assert(err, nil) + c.Assert(response.StatusCode, http.StatusOK) + + // Build a minimal valid tar archive. + var tarBuf bytes.Buffer + tw := tar.NewWriter(&tarBuf) + c.Assert(tw.WriteHeader(&tar.Header{ + Name: "test.txt", + Mode: 0644, + Size: int64(len("hello snowball")), + }), nil) + _, err = tw.Write([]byte("hello snowball")) + c.Assert(err, nil) + c.Assert(tw.Close(), nil) + + now := UTCNow() + req, err := http.NewRequest(http.MethodPut, getPutObjectURL(s.endPoint, bucketName, "test-snowball.tar"), nil) + c.Assert(err, nil) + + req.Body = io.NopCloser(bytes.NewReader(tarBuf.Bytes())) + req.Trailer = http.Header{} + req.Trailer.Set("x-amz-checksum-crc32", "rK0DXg==") + + req = signer.StreamingUnsignedV4(req, "", int64(tarBuf.Len()), now) + + // The CVE-2026-40344 attack: provide an Authorization header with a + // valid access key but a completely fabricated signature. The Snowball + // auto-extract handler must still verify the signature. + maliciousAuth := fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=deadbeefdeadbeefdeadbeeddeadbeeddeadbeefdeadbeefdeadbeefdeadbeef", s.accessKey, now.Format(yyyymmdd)) + req.Header.Set("Authorization", maliciousAuth) + + // Ensure the headers expected for unsigned trailer streaming remain. + req.Header.Set("X-Amz-Decoded-Content-Length", fmt.Sprintf("%d", tarBuf.Len())) + req.Header.Set("Content-Encoding", "aws-chunked") + req.Header.Set("X-Amz-Trailer", "x-amz-checksum-crc32") + req.Header.Set("x-amz-content-sha256", unsignedPayloadTrailer) + // Trigger the Snowball auto-extract handler. + req.Header.Set("X-Amz-Meta-Snowball-Auto-Extract", "true") + + // execute the request. + response, err = s.client.Do(req) + c.Assert(err, nil) + + // The request must be rejected because the fabricated signature is not valid. + c.Assert(response.StatusCode, http.StatusForbidden) +} + func (s *TestSuiteCommon) TestBucketSQSNotificationAMQP(c *check) { // Sample bucket notification. bucketNotificationBuf := `s3:ObjectCreated:Putprefiximages/1arn:minio:sqs:us-east-1:444455556666:amqp` diff --git a/cmd/signature-v4-parser.go b/cmd/signature-v4-parser.go index f6866cb858870..9d45590586921 100644 --- a/cmd/signature-v4-parser.go +++ b/cmd/signature-v4-parser.go @@ -66,6 +66,23 @@ func getReqAccessKeyV4(r *http.Request, region string, stype serviceType) (auth. return checkKeyValid(r, ch.accessKey) } +// getReqAccessKeyV4FromHeader extracts the access key only from the +// Authorization header. It is used for auth types (such as unsigned trailer +// streaming) where query-string credentials must not be trusted because no +// signature verification is performed for them. +func getReqAccessKeyV4FromHeader(r *http.Request, region string, stype serviceType) (auth.Credentials, bool, APIErrorCode) { + v4Auth := strings.TrimPrefix(r.Header.Get("Authorization"), signV4Algorithm) + authFields := strings.Split(strings.TrimSpace(v4Auth), ",") + if len(authFields) != 3 { + return auth.Credentials{}, false, ErrMissingFields + } + ch, s3Err := parseCredentialHeader(authFields[0], region, stype) + if s3Err != ErrNone { + return auth.Credentials{}, false, s3Err + } + return checkKeyValid(r, ch.accessKey) +} + // parse credentialHeader string into its structured form. func parseCredentialHeader(credElement string, region string, stype serviceType) (ch credentialHeader, aec APIErrorCode) { creds := strings.SplitN(strings.TrimSpace(credElement), "=", 2) diff --git a/internal/config/identity/openid/jwt.go b/internal/config/identity/openid/jwt.go index 2a422010f842d..001c861235208 100644 --- a/internal/config/identity/openid/jwt.go +++ b/internal/config/identity/openid/jwt.go @@ -79,8 +79,14 @@ func (r *Config) PopulatePublicKey(arn arn.ARN) error { return nil } - // Add client secret for the client ID for HMAC based signature. - r.pubKeys.add(pCfg.ClientID, []byte(pCfg.ClientSecret)) + // Add client secret for the client ID for HMAC based signature, but only + // when the provider advertises HMAC signing algorithms. Otherwise an + // attacker who knows the client secret could perform an algorithm + // confusion attack by sending a token with alg=HS256 even though the + // provider only issues RS256 tokens (CVE-2026-33322). + if hasHMACAlgorithm(pCfg.DiscoveryDoc.IDTokenSigningAlgValuesSupported) { + r.pubKeys.add(pCfg.ClientID, []byte(pCfg.ClientSecret)) + } client := &http.Client{ Transport: r.transport, @@ -103,6 +109,18 @@ var ( ErrTokenExpired = errors.New("token expired") ) +// hasHMACAlgorithm returns true if the provider advertises an HMAC-based +// JWT signing algorithm. +func hasHMACAlgorithm(algs []string) bool { + for _, alg := range algs { + switch alg { + case "HS256", "HS384", "HS512": + return true + } + } + return false +} + func updateClaimsExpiry(dsecs string, claims map[string]any) error { expStr := claims["exp"] if expStr == "" { @@ -134,13 +152,18 @@ const ( // Validate - validates the id_token. func (r *Config) Validate(ctx context.Context, arn arn.ARN, token, accessToken, dsecs string, claims map[string]any) error { + pCfg, ok := r.arnProviderCfgsMap[arn] + if !ok { + return fmt.Errorf("Role %s does not exist", arn) + } + jp := new(jwtgo.Parser) - jp.ValidMethods = []string{ - "RS256", "RS384", "RS512", - "ES256", "ES384", "ES512", - "HS256", "HS384", "HS512", - "RS3256", "RS3384", "RS3512", - "ES3256", "ES3384", "ES3512", + // Only accept the signing algorithms advertised by the provider. If the + // provider does not advertise any, fall back to asymmetric algorithms to + // prevent algorithm confusion attacks (CVE-2026-33322). + jp.ValidMethods = pCfg.DiscoveryDoc.IDTokenSigningAlgValuesSupported + if len(jp.ValidMethods) == 0 { + jp.ValidMethods = []string{"RS256", "RS384", "RS512", "ES256", "ES384", "ES512"} } keyFuncCallback := func(jwtToken *jwtgo.Token) (any, error) { @@ -155,11 +178,6 @@ func (r *Config) Validate(ctx context.Context, arn arn.ARN, token, accessToken, return pubkey, nil } - pCfg, ok := r.arnProviderCfgsMap[arn] - if !ok { - return fmt.Errorf("Role %s does not exist", arn) - } - mclaims := jwtgo.MapClaims(claims) jwtToken, err := jp.ParseWithClaims(token, &mclaims, keyFuncCallback) if err != nil { @@ -168,7 +186,7 @@ func (r *Config) Validate(ctx context.Context, arn arn.ARN, token, accessToken, if err = r.PopulatePublicKey(arn); err != nil { return err } - jwtToken, err = jwtgo.ParseWithClaims(token, &mclaims, keyFuncCallback) + jwtToken, err = jp.ParseWithClaims(token, &mclaims, keyFuncCallback) if err != nil { return err } diff --git a/internal/config/identity/openid/jwt_test.go b/internal/config/identity/openid/jwt_test.go index cb54faff95741..7871614441bca 100644 --- a/internal/config/identity/openid/jwt_test.go +++ b/internal/config/identity/openid/jwt_test.go @@ -129,6 +129,9 @@ func TestJWTHMACType(t *testing.T) { provider := providerCfg{ ClientID: "76b95ae5-33ef-4283-97b7-d2a85dc2d8f4", ClientSecret: "WNGvKVyyNmXq0TraSvjaDN9CtpFgx35IXtGEffMCPR0", + DiscoveryDoc: DiscoveryDoc{ + IDTokenSigningAlgValuesSupported: []string{"HS256"}, + }, } provider.JWKS.URL = u1 cfg := Config{ @@ -151,6 +154,71 @@ func TestJWTHMACType(t *testing.T) { } } +// TestJWTAlgorithmConfusion verifies CVE-2026-33322: an attacker with the +// client secret must not be able to forge an HS256 token when the provider +// only advertises RS256. +func TestJWTAlgorithmConfusion(t *testing.T) { + server := initJWKSServer() + defer server.Close() + + jwt := &jwtgo.Token{ + Method: jwtgo.SigningMethodHS256, + Claims: jwtgo.StandardClaims{ + ExpiresAt: 253428928061, + Audience: "76b95ae5-33ef-4283-97b7-d2a85dc2d8f4", + }, + Header: map[string]any{ + "typ": "JWT", + "alg": jwtgo.SigningMethodHS256.Alg(), + "kid": "76b95ae5-33ef-4283-97b7-d2a85dc2d8f4", + }, + } + + token, err := jwt.SignedString([]byte("WNGvKVyyNmXq0TraSvjaDN9CtpFgx35IXtGEffMCPR0")) + if err != nil { + t.Fatal(err) + } + + u1, err := xnet.ParseHTTPURL(server.URL) + if err != nil { + t.Fatal(err) + } + + pubKeys := publicKeys{ + RWMutex: &sync.RWMutex{}, + pkMap: map[string]any{}, + } + pubKeys.add("76b95ae5-33ef-4283-97b7-d2a85dc2d8f4", []byte("WNGvKVyyNmXq0TraSvjaDN9CtpFgx35IXtGEffMCPR0")) + + provider := providerCfg{ + ClientID: "76b95ae5-33ef-4283-97b7-d2a85dc2d8f4", + ClientSecret: "WNGvKVyyNmXq0TraSvjaDN9CtpFgx35IXtGEffMCPR0", + DiscoveryDoc: DiscoveryDoc{ + // Provider only advertises RS256; HS256 must be rejected. + IDTokenSigningAlgValuesSupported: []string{"RS256"}, + }, + } + provider.JWKS.URL = u1 + cfg := Config{ + Enabled: true, + pubKeys: pubKeys, + arnProviderCfgsMap: map[arn.ARN]*providerCfg{ + DummyRoleARN: &provider, + }, + ProviderCfgs: map[string]*providerCfg{ + "1": &provider, + }, + closeRespFn: func(rc io.ReadCloser) { + rc.Close() + }, + } + + var claims jwtgo.MapClaims + if err = cfg.Validate(t.Context(), DummyRoleARN, token, "", "", claims); err == nil { + t.Fatal("expected HS256 token to be rejected when provider advertises only RS256") + } +} + func TestJWT(t *testing.T) { const jsonkey = `{"keys": [ From f9baf3c66bde08fe92c381d08b12492f82773a10 Mon Sep 17 00:00:00 2001 From: Joakim Hindersson Date: Thu, 24 Sep 2026 22:37:38 +0200 Subject: [PATCH 3/3] Remove all uneccessary github workflows --- .github/workflows/depsreview.yaml | 14 -- .github/workflows/go-cross.yml | 39 ----- .github/workflows/go-healing.yml | 44 ----- .github/workflows/go-lint.yml | 42 ----- .github/workflows/go-resiliency.yml | 39 ----- .github/workflows/helm-lint.yml | 30 ---- .github/workflows/iam-integrations.yaml | 161 ------------------ .github/workflows/issues.yaml | 18 -- .github/workflows/lock.yml | 24 --- .github/workflows/mint.yml | 81 --------- .../mint/minio-compress-encrypt.yaml | 80 --------- .github/workflows/mint/minio-erasure.yaml | 51 ------ .github/workflows/mint/minio-pools.yaml | 117 ------------- .github/workflows/mint/minio-resiliency.yaml | 78 --------- .github/workflows/mint/nginx-1-node.conf | 100 ----------- .github/workflows/mint/nginx-4-node.conf | 105 ------------ .github/workflows/mint/nginx-8-node.conf | 114 ------------- .github/workflows/mint/nginx.conf | 106 ------------ .../multipart/docker-compose-site1.yaml | 66 ------- .../multipart/docker-compose-site2.yaml | 66 ------- .github/workflows/multipart/migrate.sh | 147 ---------------- .github/workflows/multipart/nginx-site1.conf | 61 ------- .github/workflows/multipart/nginx-site2.conf | 61 ------- .github/workflows/replication.yaml | 79 --------- .github/workflows/root-disable.yml | 34 ---- .github/workflows/root.cert | 9 - .github/workflows/root.key | 3 - .github/workflows/run-mint.sh | 64 ------- .github/workflows/shfmt.yml | 22 --- .github/workflows/typos.yml | 15 -- .github/workflows/upgrade-ci-cd.yaml | 34 ---- .github/workflows/vulncheck.yml | 31 ---- 32 files changed, 1935 deletions(-) delete mode 100644 .github/workflows/depsreview.yaml delete mode 100644 .github/workflows/go-cross.yml delete mode 100644 .github/workflows/go-healing.yml delete mode 100644 .github/workflows/go-lint.yml delete mode 100644 .github/workflows/go-resiliency.yml delete mode 100644 .github/workflows/helm-lint.yml delete mode 100644 .github/workflows/iam-integrations.yaml delete mode 100644 .github/workflows/issues.yaml delete mode 100644 .github/workflows/lock.yml delete mode 100644 .github/workflows/mint.yml delete mode 100644 .github/workflows/mint/minio-compress-encrypt.yaml delete mode 100644 .github/workflows/mint/minio-erasure.yaml delete mode 100644 .github/workflows/mint/minio-pools.yaml delete mode 100644 .github/workflows/mint/minio-resiliency.yaml delete mode 100644 .github/workflows/mint/nginx-1-node.conf delete mode 100644 .github/workflows/mint/nginx-4-node.conf delete mode 100644 .github/workflows/mint/nginx-8-node.conf delete mode 100644 .github/workflows/mint/nginx.conf delete mode 100644 .github/workflows/multipart/docker-compose-site1.yaml delete mode 100644 .github/workflows/multipart/docker-compose-site2.yaml delete mode 100755 .github/workflows/multipart/migrate.sh delete mode 100644 .github/workflows/multipart/nginx-site1.conf delete mode 100644 .github/workflows/multipart/nginx-site2.conf delete mode 100644 .github/workflows/replication.yaml delete mode 100644 .github/workflows/root-disable.yml delete mode 100644 .github/workflows/root.cert delete mode 100644 .github/workflows/root.key delete mode 100755 .github/workflows/run-mint.sh delete mode 100644 .github/workflows/shfmt.yml delete mode 100644 .github/workflows/typos.yml delete mode 100644 .github/workflows/upgrade-ci-cd.yaml delete mode 100644 .github/workflows/vulncheck.yml diff --git a/.github/workflows/depsreview.yaml b/.github/workflows/depsreview.yaml deleted file mode 100644 index b9d6d20fff4e7..0000000000000 --- a/.github/workflows/depsreview.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: 'Dependency Review' -on: [pull_request] - -permissions: - contents: read - -jobs: - dependency-review: - runs-on: ubuntu-latest - steps: - - name: 'Checkout Repository' - uses: actions/checkout@v4 - - name: 'Dependency Review' - uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/go-cross.yml b/.github/workflows/go-cross.yml deleted file mode 100644 index 324af3e956eba..0000000000000 --- a/.github/workflows/go-cross.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Crosscompile - -on: - pull_request: - branches: - - master - -# This ensures that previous jobs for the PR are canceled when the PR is -# updated. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build: - name: Build Tests with Go ${{ matrix.go-version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - go-version: [1.24.x] - os: [ubuntu-latest] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - - name: Build on ${{ matrix.os }} - if: matrix.os == 'ubuntu-latest' - env: - CGO_ENABLED: 0 - GO111MODULE: on - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make crosscompile diff --git a/.github/workflows/go-healing.yml b/.github/workflows/go-healing.yml deleted file mode 100644 index 0ea2505128ca4..0000000000000 --- a/.github/workflows/go-healing.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Healing Functional Tests - -on: - pull_request: - branches: - - master - -# This ensures that previous jobs for the PR are canceled when the PR is -# updated. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build: - name: Go ${{ matrix.go-version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - go-version: [1.24.x] - os: [ubuntu-latest] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - - name: Build on ${{ matrix.os }} - if: matrix.os == 'ubuntu-latest' - env: - CGO_ENABLED: 0 - GO111MODULE: on - MINIO_KMS_SECRET_KEY: "my-minio-key:oyArl7zlPECEduNbB1KXgdzDn2Bdpvvw0l8VO51HQnY=" - MINIO_KMS_AUTO_ENCRYPTION: on - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make verify-healing - make verify-healing-inconsistent-versions - make verify-healing-with-root-disks - make verify-healing-with-rewrite diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml deleted file mode 100644 index 6b43086a7a14f..0000000000000 --- a/.github/workflows/go-lint.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Linters and Tests - -on: - pull_request: - branches: - - master - -# This ensures that previous jobs for the PR are canceled when the PR is -# updated. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build: - name: Go ${{ matrix.go-version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - go-version: [1.24.x] - os: [ubuntu-latest] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - - name: Build on ${{ matrix.os }} - if: matrix.os == 'ubuntu-latest' - env: - CGO_ENABLED: 0 - GO111MODULE: on - run: | - sudo apt install jq -y - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make - make test - make test-race diff --git a/.github/workflows/go-resiliency.yml b/.github/workflows/go-resiliency.yml deleted file mode 100644 index 4eb2bbb90ba5c..0000000000000 --- a/.github/workflows/go-resiliency.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Resiliency Functional Tests - -on: - pull_request: - branches: - - master - -# This ensures that previous jobs for the PR are canceled when the PR is -# updated. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build: - name: Go ${{ matrix.go-version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - go-version: [1.24.x] - os: [ubuntu-latest] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - - name: Build on ${{ matrix.os }} - if: matrix.os == 'ubuntu-latest' - env: - CGO_ENABLED: 0 - GO111MODULE: on - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make test-resiliency diff --git a/.github/workflows/helm-lint.yml b/.github/workflows/helm-lint.yml deleted file mode 100644 index f444106f1cdf0..0000000000000 --- a/.github/workflows/helm-lint.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Helm Chart linting - -on: - pull_request: - branches: - - master - -# This ensures that previous jobs for the PR are canceled when the PR is -# updated. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - release: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Helm - uses: azure/setup-helm@v4 - - - name: Run helm lint - run: | - cd helm/minio - helm lint . diff --git a/.github/workflows/iam-integrations.yaml b/.github/workflows/iam-integrations.yaml deleted file mode 100644 index cc57a77aa70d8..0000000000000 --- a/.github/workflows/iam-integrations.yaml +++ /dev/null @@ -1,161 +0,0 @@ -name: IAM integration - -on: - pull_request: - branches: - - master - -# This ensures that previous jobs for the PR are canceled when the PR is -# updated. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - iam-matrix-test: - name: "[Go=${{ matrix.go-version }}|ldap=${{ matrix.ldap }}|etcd=${{ matrix.etcd }}|openid=${{ matrix.openid }}]" - runs-on: ubuntu-latest - - services: - openldap: - image: quay.io/minio/openldap - ports: - - "389:389" - - "636:636" - env: - LDAP_ORGANIZATION: "MinIO Inc" - LDAP_DOMAIN: "min.io" - LDAP_ADMIN_PASSWORD: "admin" - etcd: - image: "quay.io/coreos/etcd:v3.5.1" - env: - ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379" - ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379" - ports: - - "2379:2379" - options: >- - --health-cmd "etcdctl endpoint health" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - openid: - image: quay.io/minio/dex - ports: - - "5556:5556" - env: - DEX_LDAP_SERVER: "openldap:389" - openid2: - image: quay.io/minio/dex - ports: - - "5557:5557" - env: - DEX_LDAP_SERVER: "openldap:389" - DEX_ISSUER: "http://127.0.0.1:5557/dex" - DEX_WEB_HTTP: "0.0.0.0:5557" - - strategy: - # When ldap, etcd or openid vars are empty below, those external servers - # are turned off - i.e. if ldap="", then ldap server is not enabled for - # the tests. - matrix: - go-version: [1.24.x] - ldap: ["", "localhost:389"] - etcd: ["", "http://localhost:2379"] - openid: ["", "http://127.0.0.1:5556/dex"] - exclude: - # exclude combos where all are empty. - - ldap: "" - etcd: "" - openid: "" - # exclude combos where both ldap and openid IDPs are specified. - - ldap: "localhost:389" - openid: "http://127.0.0.1:5556/dex" - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - - name: Test LDAP/OpenID/Etcd combo - env: - _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }} - _MINIO_ETCD_TEST_SERVER: ${{ matrix.etcd }} - _MINIO_OPENID_TEST_SERVER: ${{ matrix.openid }} - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make test-iam - - name: Test with multiple OpenID providers - if: matrix.openid == 'http://127.0.0.1:5556/dex' - env: - _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }} - _MINIO_ETCD_TEST_SERVER: ${{ matrix.etcd }} - _MINIO_OPENID_TEST_SERVER: ${{ matrix.openid }} - _MINIO_OPENID_TEST_SERVER_2: "http://127.0.0.1:5557/dex" - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make test-iam - - name: Test with Access Management Plugin enabled - env: - _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }} - _MINIO_ETCD_TEST_SERVER: ${{ matrix.etcd }} - _MINIO_OPENID_TEST_SERVER: ${{ matrix.openid }} - _MINIO_POLICY_PLUGIN_TEST_ENDPOINT: "http://127.0.0.1:8080" - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - go run docs/iam/access-manager-plugin.go & - make test-iam - - name: Test MinIO Old Version data to IAM import current version - if: matrix.ldap == 'ldaphost:389' - env: - _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }} - run: | - make test-iam-ldap-upgrade-import - - name: Test LDAP for automatic site replication - if: matrix.ldap == 'localhost:389' - run: | - make test-site-replication-ldap - - name: Test OIDC for automatic site replication - if: matrix.openid == 'http://127.0.0.1:5556/dex' - run: | - make test-site-replication-oidc - iam-import-with-missing-entities: - name: Test IAM import in new cluster with missing entities - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - - name: Checkout minio-iam-testing - uses: actions/checkout@v4 - with: - repository: minio/minio-iam-testing - path: minio-iam-testing - - name: Test import of IAM artifacts when in fresh cluster there are missing groups etc - run: | - make test-iam-import-with-missing-entities - iam-import-with-openid: - name: Test IAM import in new cluster with opendid configurations - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - - name: Checkout minio-iam-testing - uses: actions/checkout@v4 - with: - repository: minio/minio-iam-testing - path: minio-iam-testing - - name: Test import of IAM artifacts when in fresh cluster with openid configurations - run: | - make test-iam-import-with-openid diff --git a/.github/workflows/issues.yaml b/.github/workflows/issues.yaml deleted file mode 100644 index d91950aeb2b77..0000000000000 --- a/.github/workflows/issues.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# @format - -name: Issue Workflow - -on: - issues: - types: - - opened - -jobs: - add-to-project: - name: Add issue to project - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@v0.5.0 - with: - project-url: https://github.com/orgs/miniohq/projects/2 - github-token: ${{ secrets.BOT_PAT }} diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml deleted file mode 100644 index da3afe3c4627e..0000000000000 --- a/.github/workflows/lock.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: 'Lock Threads' - -on: - schedule: - - cron: '0 0 * * *' - workflow_dispatch: - -permissions: - issues: write - -concurrency: - group: lock - -jobs: - action: - runs-on: ubuntu-latest - steps: - - uses: dessant/lock-threads@v3 - with: - github-token: ${{ github.token }} - issue-inactive-days: '365' - exclude-any-issue-labels: 'do-not-close' - issue-lock-reason: 'resolved' - log-output: true diff --git a/.github/workflows/mint.yml b/.github/workflows/mint.yml deleted file mode 100644 index 77dc498cec39e..0000000000000 --- a/.github/workflows/mint.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Mint Tests - -on: - pull_request: - branches: - - master - -# This ensures that previous jobs for the PR are canceled when the PR is -# updated. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - mint-test: - runs-on: mint - timeout-minutes: 120 - steps: - - name: cleanup #https://github.com/actions/checkout/issues/273 - run: | - sudo -S rm -rf ${GITHUB_WORKSPACE} - mkdir ${GITHUB_WORKSPACE} - - name: checkout-step - uses: actions/checkout@v4 - - - name: setup-go-step - uses: actions/setup-go@v5 - with: - go-version: 1.24.x - - - name: github sha short - id: vars - run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - - name: build-minio - run: | - TAG="quay.io/minio/minio:${{ steps.vars.outputs.sha_short }}" make docker - - - name: multipart uploads test - run: | - ${GITHUB_WORKSPACE}/.github/workflows/multipart/migrate.sh "${{ steps.vars.outputs.sha_short }}" - - - name: compress and encrypt - run: | - ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "compress-encrypt" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}" - - - name: multiple pools - run: | - ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "pools" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}" - - - name: standalone erasure - run: | - ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "erasure" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}" - - # FIXME: renable this back when we have a valid way to add deadlines for PUT()s (internode CreateFile) - # - name: resiliency - # run: | - # ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "resiliency" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}" - - - name: The job must cleanup - if: ${{ always() }} - run: | - export JOB_NAME=${{ steps.vars.outputs.sha_short }} - for mode in $(echo compress-encrypt pools erasure); do - docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/mint/minio-${mode}.yaml down || true - docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/mint/minio-${mode}.yaml rm || true - done - - docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/multipart/docker-compose-site1.yaml rm -s -f || true - docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/multipart/docker-compose-site2.yaml rm -s -f || true - for volume in $(docker volume ls -q | grep minio); do - docker volume rm ${volume} || true - done - - docker rmi -f quay.io/minio/minio:${{ steps.vars.outputs.sha_short }} - docker system prune -f || true - docker volume prune -f || true - docker volume rm $(docker volume ls -q -f dangling=true) || true diff --git a/.github/workflows/mint/minio-compress-encrypt.yaml b/.github/workflows/mint/minio-compress-encrypt.yaml deleted file mode 100644 index fe1238e80d3d8..0000000000000 --- a/.github/workflows/mint/minio-compress-encrypt.yaml +++ /dev/null @@ -1,80 +0,0 @@ -version: '3.7' - -# Settings and configurations that are common for all containers -x-minio-common: &minio-common - image: quay.io/minio/minio:${JOB_NAME} - command: server --console-address ":9001" http://minio{1...4}/cdata{1...2} - expose: - - "9000" - - "9001" - environment: - MINIO_CI_CD: "on" - MINIO_ROOT_USER: "minio" - MINIO_ROOT_PASSWORD: "minio123" - MINIO_COMPRESSION_ENABLE: "on" - MINIO_COMPRESSION_MIME_TYPES: "*" - MINIO_COMPRESSION_ALLOW_ENCRYPTION: "on" - MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw=" - healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 5s - timeout: 5s - retries: 5 - -# starts 4 docker containers running minio server instances. -# using nginx reverse proxy, load balancing, you can access -# it through port 9000. -services: - minio1: - <<: *minio-common - hostname: minio1 - volumes: - - cdata1-1:/cdata1 - - cdata1-2:/cdata2 - - minio2: - <<: *minio-common - hostname: minio2 - volumes: - - cdata2-1:/cdata1 - - cdata2-2:/cdata2 - - minio3: - <<: *minio-common - hostname: minio3 - volumes: - - cdata3-1:/cdata1 - - cdata3-2:/cdata2 - - minio4: - <<: *minio-common - hostname: minio4 - volumes: - - cdata4-1:/cdata1 - - cdata4-2:/cdata2 - - nginx: - image: nginx:1.19.2-alpine - hostname: nginx - volumes: - - ./nginx-4-node.conf:/etc/nginx/nginx.conf:ro - ports: - - "9000:9000" - - "9001:9001" - depends_on: - - minio1 - - minio2 - - minio3 - - minio4 - -## By default this config uses default local driver, -## For custom volumes replace with volume driver configuration. -volumes: - cdata1-1: - cdata1-2: - cdata2-1: - cdata2-2: - cdata3-1: - cdata3-2: - cdata4-1: - cdata4-2: diff --git a/.github/workflows/mint/minio-erasure.yaml b/.github/workflows/mint/minio-erasure.yaml deleted file mode 100644 index a3fd6dbc0de21..0000000000000 --- a/.github/workflows/mint/minio-erasure.yaml +++ /dev/null @@ -1,51 +0,0 @@ -version: '3.7' - -# Settings and configurations that are common for all containers -x-minio-common: &minio-common - image: quay.io/minio/minio:${JOB_NAME} - command: server --console-address ":9001" edata{1...4} - expose: - - "9000" - - "9001" - environment: - MINIO_CI_CD: "on" - MINIO_ROOT_USER: "minio" - MINIO_ROOT_PASSWORD: "minio123" - MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw=" - healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 5s - timeout: 5s - retries: 5 - -# starts 4 docker containers running minio server instances. -# using nginx reverse proxy, load balancing, you can access -# it through port 9000. -services: - minio1: - <<: *minio-common - hostname: minio1 - volumes: - - edata1-1:/edata1 - - edata1-2:/edata2 - - edata1-3:/edata3 - - edata1-4:/edata4 - - nginx: - image: nginx:1.19.2-alpine - hostname: nginx - volumes: - - ./nginx-1-node.conf:/etc/nginx/nginx.conf:ro - ports: - - "9000:9000" - - "9001:9001" - depends_on: - - minio1 - -## By default this config uses default local driver, -## For custom volumes replace with volume driver configuration. -volumes: - edata1-1: - edata1-2: - edata1-3: - edata1-4: diff --git a/.github/workflows/mint/minio-pools.yaml b/.github/workflows/mint/minio-pools.yaml deleted file mode 100644 index bd79fdae048e6..0000000000000 --- a/.github/workflows/mint/minio-pools.yaml +++ /dev/null @@ -1,117 +0,0 @@ -version: '3.7' - -# Settings and configurations that are common for all containers -x-minio-common: &minio-common - image: quay.io/minio/minio:${JOB_NAME} - command: server --console-address ":9001" http://minio{1...4}/pdata{1...2} http://minio{5...8}/pdata{1...2} - expose: - - "9000" - - "9001" - environment: - MINIO_CI_CD: "on" - MINIO_ROOT_USER: "minio" - MINIO_ROOT_PASSWORD: "minio123" - MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw=" - healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 5s - timeout: 5s - retries: 5 - -# starts 4 docker containers running minio server instances. -# using nginx reverse proxy, load balancing, you can access -# it through port 9000. -services: - minio1: - <<: *minio-common - hostname: minio1 - volumes: - - pdata1-1:/pdata1 - - pdata1-2:/pdata2 - - minio2: - <<: *minio-common - hostname: minio2 - volumes: - - pdata2-1:/pdata1 - - pdata2-2:/pdata2 - - minio3: - <<: *minio-common - hostname: minio3 - volumes: - - pdata3-1:/pdata1 - - pdata3-2:/pdata2 - - minio4: - <<: *minio-common - hostname: minio4 - volumes: - - pdata4-1:/pdata1 - - pdata4-2:/pdata2 - - minio5: - <<: *minio-common - hostname: minio5 - volumes: - - pdata5-1:/pdata1 - - pdata5-2:/pdata2 - - minio6: - <<: *minio-common - hostname: minio6 - volumes: - - pdata6-1:/pdata1 - - pdata6-2:/pdata2 - - minio7: - <<: *minio-common - hostname: minio7 - volumes: - - pdata7-1:/pdata1 - - pdata7-2:/pdata2 - - minio8: - <<: *minio-common - hostname: minio8 - volumes: - - pdata8-1:/pdata1 - - pdata8-2:/pdata2 - - nginx: - image: nginx:1.19.2-alpine - hostname: nginx - volumes: - - ./nginx-8-node.conf:/etc/nginx/nginx.conf:ro - ports: - - "9000:9000" - - "9001:9001" - depends_on: - - minio1 - - minio2 - - minio3 - - minio4 - - minio5 - - minio6 - - minio7 - - minio8 - -## By default this config uses default local driver, -## For custom volumes replace with volume driver configuration. -volumes: - pdata1-1: - pdata1-2: - pdata2-1: - pdata2-2: - pdata3-1: - pdata3-2: - pdata4-1: - pdata4-2: - pdata5-1: - pdata5-2: - pdata6-1: - pdata6-2: - pdata7-1: - pdata7-2: - pdata8-1: - pdata8-2: diff --git a/.github/workflows/mint/minio-resiliency.yaml b/.github/workflows/mint/minio-resiliency.yaml deleted file mode 100644 index 9d569c59e4cff..0000000000000 --- a/.github/workflows/mint/minio-resiliency.yaml +++ /dev/null @@ -1,78 +0,0 @@ -version: '3.7' - -# Settings and configurations that are common for all containers -x-minio-common: &minio-common - image: quay.io/minio/minio:${JOB_NAME} - command: server --console-address ":9001" http://minio{1...4}/rdata{1...2} - expose: - - "9000" - - "9001" - environment: - MINIO_CI_CD: "on" - MINIO_ROOT_USER: "minio" - MINIO_ROOT_PASSWORD: "minio123" - MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw=" - MINIO_DRIVE_MAX_TIMEOUT: "5s" - healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 5s - timeout: 5s - retries: 5 - -# starts 4 docker containers running minio server instances. -# using nginx reverse proxy, load balancing, you can access -# it through port 9000. -services: - minio1: - <<: *minio-common - hostname: minio1 - volumes: - - rdata1-1:/rdata1 - - rdata1-2:/rdata2 - - minio2: - <<: *minio-common - hostname: minio2 - volumes: - - rdata2-1:/rdata1 - - rdata2-2:/rdata2 - - minio3: - <<: *minio-common - hostname: minio3 - volumes: - - rdata3-1:/rdata1 - - rdata3-2:/rdata2 - - minio4: - <<: *minio-common - hostname: minio4 - volumes: - - rdata4-1:/rdata1 - - rdata4-2:/rdata2 - - nginx: - image: nginx:1.19.2-alpine - hostname: nginx - volumes: - - ./nginx-4-node.conf:/etc/nginx/nginx.conf:ro - ports: - - "9000:9000" - - "9001:9001" - depends_on: - - minio1 - - minio2 - - minio3 - - minio4 - -## By default this config uses default local driver, -## For custom volumes replace with volume driver configuration. -volumes: - rdata1-1: - rdata1-2: - rdata2-1: - rdata2-2: - rdata3-1: - rdata3-2: - rdata4-1: - rdata4-2: diff --git a/.github/workflows/mint/nginx-1-node.conf b/.github/workflows/mint/nginx-1-node.conf deleted file mode 100644 index bb13bde24d71c..0000000000000 --- a/.github/workflows/mint/nginx-1-node.conf +++ /dev/null @@ -1,100 +0,0 @@ -user nginx; -worker_processes auto; - -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 4096; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - sendfile on; - keepalive_timeout 65; - - # include /etc/nginx/conf.d/*.conf; - - upstream minio { - server minio1:9000; - } - - upstream console { - ip_hash; - server minio1:9001; - } - - server { - listen 9000; - listen [::]:9000; - server_name localhost; - - # To allow special characters in headers - ignore_invalid_headers off; - # Allow any size file to be uploaded. - # Set to a value such as 1000m; to restrict file size to a specific value - client_max_body_size 0; - # To disable buffering - proxy_buffering off; - proxy_request_buffering off; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_connect_timeout 300; - # Default is HTTP/1, keepalive is only enabled in HTTP/1.1 - proxy_http_version 1.1; - proxy_set_header Connection ""; - chunked_transfer_encoding off; - - proxy_pass http://minio; - } - } - - server { - listen 9001; - listen [::]:9001; - server_name localhost; - - # To allow special characters in headers - ignore_invalid_headers off; - # Allow any size file to be uploaded. - # Set to a value such as 1000m; to restrict file size to a specific value - client_max_body_size 0; - # To disable buffering - proxy_buffering off; - proxy_request_buffering off; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-NginX-Proxy true; - - # This is necessary to pass the correct IP to be hashed - real_ip_header X-Real-IP; - - proxy_connect_timeout 300; - - # To support websocket - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - chunked_transfer_encoding off; - - proxy_pass http://console; - } - } -} diff --git a/.github/workflows/mint/nginx-4-node.conf b/.github/workflows/mint/nginx-4-node.conf deleted file mode 100644 index b849940d972a9..0000000000000 --- a/.github/workflows/mint/nginx-4-node.conf +++ /dev/null @@ -1,105 +0,0 @@ -user nginx; -worker_processes auto; - -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 4096; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - sendfile on; - keepalive_timeout 65; - - # include /etc/nginx/conf.d/*.conf; - - upstream minio { - server minio1:9000 max_fails=1 fail_timeout=10s; - server minio2:9000 max_fails=1 fail_timeout=10s; - server minio3:9000 max_fails=1 fail_timeout=10s; - } - - upstream console { - ip_hash; - server minio1:9001; - server minio2:9001; - server minio3:9001; - server minio4:9001; - } - - server { - listen 9000; - listen [::]:9000; - server_name localhost; - - # To allow special characters in headers - ignore_invalid_headers off; - # Allow any size file to be uploaded. - # Set to a value such as 1000m; to restrict file size to a specific value - client_max_body_size 0; - # To disable buffering - proxy_buffering off; - proxy_request_buffering off; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_connect_timeout 300; - # Default is HTTP/1, keepalive is only enabled in HTTP/1.1 - proxy_http_version 1.1; - proxy_set_header Connection ""; - chunked_transfer_encoding off; - - proxy_pass http://minio; - } - } - - server { - listen 9001; - listen [::]:9001; - server_name localhost; - - # To allow special characters in headers - ignore_invalid_headers off; - # Allow any size file to be uploaded. - # Set to a value such as 1000m; to restrict file size to a specific value - client_max_body_size 0; - # To disable buffering - proxy_buffering off; - proxy_request_buffering off; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-NginX-Proxy true; - - # This is necessary to pass the correct IP to be hashed - real_ip_header X-Real-IP; - - proxy_connect_timeout 300; - - # To support websocket - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - chunked_transfer_encoding off; - - proxy_pass http://console; - } - } -} diff --git a/.github/workflows/mint/nginx-8-node.conf b/.github/workflows/mint/nginx-8-node.conf deleted file mode 100644 index 278b5ae76e6c2..0000000000000 --- a/.github/workflows/mint/nginx-8-node.conf +++ /dev/null @@ -1,114 +0,0 @@ -user nginx; -worker_processes auto; - -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 4096; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - sendfile on; - keepalive_timeout 65; - - # include /etc/nginx/conf.d/*.conf; - - upstream minio { - server minio1:9000 max_fails=1 fail_timeout=10s; - server minio2:9000 max_fails=1 fail_timeout=10s; - server minio3:9000 max_fails=1 fail_timeout=10s; - server minio4:9000 max_fails=1 fail_timeout=10s; - server minio5:9000 max_fails=1 fail_timeout=10s; - server minio6:9000 max_fails=1 fail_timeout=10s; - server minio7:9000 max_fails=1 fail_timeout=10s; - server minio8:9000 max_fails=1 fail_timeout=10s; - } - - upstream console { - ip_hash; - server minio1:9001; - server minio2:9001; - server minio3:9001; - server minio4:9001; - server minio5:9001; - server minio6:9001; - server minio7:9001; - server minio8:9001; - } - - server { - listen 9000; - listen [::]:9000; - server_name localhost; - - # To allow special characters in headers - ignore_invalid_headers off; - # Allow any size file to be uploaded. - # Set to a value such as 1000m; to restrict file size to a specific value - client_max_body_size 0; - # To disable buffering - proxy_buffering off; - proxy_request_buffering off; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_connect_timeout 300; - # Default is HTTP/1, keepalive is only enabled in HTTP/1.1 - proxy_http_version 1.1; - proxy_set_header Connection ""; - chunked_transfer_encoding off; - - proxy_pass http://minio; - } - } - - server { - listen 9001; - listen [::]:9001; - server_name localhost; - - # To allow special characters in headers - ignore_invalid_headers off; - # Allow any size file to be uploaded. - # Set to a value such as 1000m; to restrict file size to a specific value - client_max_body_size 0; - # To disable buffering - proxy_buffering off; - proxy_request_buffering off; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-NginX-Proxy true; - - # This is necessary to pass the correct IP to be hashed - real_ip_header X-Real-IP; - - proxy_connect_timeout 300; - - # To support websocket - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - chunked_transfer_encoding off; - - proxy_pass http://console; - } - } -} diff --git a/.github/workflows/mint/nginx.conf b/.github/workflows/mint/nginx.conf deleted file mode 100644 index 1455a3e2f13b3..0000000000000 --- a/.github/workflows/mint/nginx.conf +++ /dev/null @@ -1,106 +0,0 @@ -user nginx; -worker_processes auto; - -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 4096; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - sendfile on; - keepalive_timeout 65; - - # include /etc/nginx/conf.d/*.conf; - - upstream minio { - server minio1:9000 max_fails=1 fail_timeout=10s; - server minio2:9000 max_fails=1 fail_timeout=10s; - server minio3:9000 max_fails=1 fail_timeout=10s; - server minio4:9000 max_fails=1 fail_timeout=10s; - } - - upstream console { - ip_hash; - server minio1:9001; - server minio2:9001; - server minio3:9001; - server minio4:9001; - } - - server { - listen 9000; - listen [::]:9000; - server_name localhost; - - # To allow special characters in headers - ignore_invalid_headers off; - # Allow any size file to be uploaded. - # Set to a value such as 1000m; to restrict file size to a specific value - client_max_body_size 0; - # To disable buffering - proxy_buffering off; - proxy_request_buffering off; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_connect_timeout 300; - # Default is HTTP/1, keepalive is only enabled in HTTP/1.1 - proxy_http_version 1.1; - proxy_set_header Connection ""; - chunked_transfer_encoding off; - - proxy_pass http://minio; - } - } - - server { - listen 9001; - listen [::]:9001; - server_name localhost; - - # To allow special characters in headers - ignore_invalid_headers off; - # Allow any size file to be uploaded. - # Set to a value such as 1000m; to restrict file size to a specific value - client_max_body_size 0; - # To disable buffering - proxy_buffering off; - proxy_request_buffering off; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-NginX-Proxy true; - - # This is necessary to pass the correct IP to be hashed - real_ip_header X-Real-IP; - - proxy_connect_timeout 300; - - # To support websocket - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - chunked_transfer_encoding off; - - proxy_pass http://console; - } - } -} diff --git a/.github/workflows/multipart/docker-compose-site1.yaml b/.github/workflows/multipart/docker-compose-site1.yaml deleted file mode 100644 index b4a665ef068b1..0000000000000 --- a/.github/workflows/multipart/docker-compose-site1.yaml +++ /dev/null @@ -1,66 +0,0 @@ -version: '3.7' - -# Settings and configurations that are common for all containers -x-minio-common: &minio-common - image: quay.io/minio/minio:${RELEASE} - command: server http://site1-minio{1...4}/data{1...2} - environment: - - MINIO_PROMETHEUS_AUTH_TYPE=public - - CI=true - -# starts 4 docker containers running minio server instances. -# using nginx reverse proxy, load balancing, you can access -# it through port 9000. -services: - site1-minio1: - <<: *minio-common - hostname: site1-minio1 - volumes: - - site1-data1-1:/data1 - - site1-data1-2:/data2 - - site1-minio2: - <<: *minio-common - hostname: site1-minio2 - volumes: - - site1-data2-1:/data1 - - site1-data2-2:/data2 - - site1-minio3: - <<: *minio-common - hostname: site1-minio3 - volumes: - - site1-data3-1:/data1 - - site1-data3-2:/data2 - - site1-minio4: - <<: *minio-common - hostname: site1-minio4 - volumes: - - site1-data4-1:/data1 - - site1-data4-2:/data2 - - site1-nginx: - image: nginx:1.19.2-alpine - hostname: site1-nginx - volumes: - - ./nginx-site1.conf:/etc/nginx/nginx.conf:ro - ports: - - "9001:9001" - depends_on: - - site1-minio1 - - site1-minio2 - - site1-minio3 - - site1-minio4 - -## By default this config uses default local driver, -## For custom volumes replace with volume driver configuration. -volumes: - site1-data1-1: - site1-data1-2: - site1-data2-1: - site1-data2-2: - site1-data3-1: - site1-data3-2: - site1-data4-1: - site1-data4-2: diff --git a/.github/workflows/multipart/docker-compose-site2.yaml b/.github/workflows/multipart/docker-compose-site2.yaml deleted file mode 100644 index 31a1f7a8c9e41..0000000000000 --- a/.github/workflows/multipart/docker-compose-site2.yaml +++ /dev/null @@ -1,66 +0,0 @@ -version: '3.7' - -# Settings and configurations that are common for all containers -x-minio-common: &minio-common - image: quay.io/minio/minio:${RELEASE} - command: server http://site2-minio{1...4}/data{1...2} - environment: - - MINIO_PROMETHEUS_AUTH_TYPE=public - - CI=true - -# starts 4 docker containers running minio server instances. -# using nginx reverse proxy, load balancing, you can access -# it through port 9000. -services: - site2-minio1: - <<: *minio-common - hostname: site2-minio1 - volumes: - - site2-data1-1:/data1 - - site2-data1-2:/data2 - - site2-minio2: - <<: *minio-common - hostname: site2-minio2 - volumes: - - site2-data2-1:/data1 - - site2-data2-2:/data2 - - site2-minio3: - <<: *minio-common - hostname: site2-minio3 - volumes: - - site2-data3-1:/data1 - - site2-data3-2:/data2 - - site2-minio4: - <<: *minio-common - hostname: site2-minio4 - volumes: - - site2-data4-1:/data1 - - site2-data4-2:/data2 - - site2-nginx: - image: nginx:1.19.2-alpine - hostname: site2-nginx - volumes: - - ./nginx-site2.conf:/etc/nginx/nginx.conf:ro - ports: - - "9002:9002" - depends_on: - - site2-minio1 - - site2-minio2 - - site2-minio3 - - site2-minio4 - -## By default this config uses default local driver, -## For custom volumes replace with volume driver configuration. -volumes: - site2-data1-1: - site2-data1-2: - site2-data2-1: - site2-data2-2: - site2-data3-1: - site2-data3-2: - site2-data4-1: - site2-data4-2: diff --git a/.github/workflows/multipart/migrate.sh b/.github/workflows/multipart/migrate.sh deleted file mode 100755 index 6058b008c8836..0000000000000 --- a/.github/workflows/multipart/migrate.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/bin/bash - -set -x - -## change working directory -cd .github/workflows/multipart/ - -function cleanup() { - docker-compose -f docker-compose-site1.yaml rm -s -f || true - docker-compose -f docker-compose-site2.yaml rm -s -f || true - for volume in $(docker volume ls -q | grep minio); do - docker volume rm ${volume} || true - done - - docker system prune -f || true - docker volume prune -f || true - docker volume rm $(docker volume ls -q -f dangling=true) || true -} - -cleanup - -if [ ! -f ./mc ]; then - wget --quiet -O mc https://dl.minio.io/client/mc/release/linux-amd64/mc && - chmod +x mc -fi - -export RELEASE=RELEASE.2023-08-29T23-07-35Z - -docker-compose -f docker-compose-site1.yaml up -d -docker-compose -f docker-compose-site2.yaml up -d - -sleep 30s - -./mc alias set site1 http://site1-nginx:9001 minioadmin minioadmin --api s3v4 -./mc alias set site2 http://site2-nginx:9002 minioadmin minioadmin --api s3v4 - -./mc ready site1/ -./mc ready site2/ - -./mc admin replicate add site1 site2 -./mc mb site1/testbucket/ -./mc cp -r --quiet /usr/bin site1/testbucket/ - -sleep 5 - -./s3-check-md5 -h - -failed_count_site1=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l) -failed_count_site2=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l) - -if [ $failed_count_site1 -ne 0 ]; then - echo "failed with multipart on site1 uploads" - exit 1 -fi - -if [ $failed_count_site2 -ne 0 ]; then - echo "failed with multipart on site2 uploads" - exit 1 -fi - -./mc cp -r --quiet /usr/bin site1/testbucket/ - -sleep 5 - -failed_count_site1=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l) -failed_count_site2=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l) - -## we do not need to fail here, since we are going to test -## upgrading to master, healing and being able to recover -## the last version. -if [ $failed_count_site1 -ne 0 ]; then - echo "failed with multipart on site1 uploads ${failed_count_site1}" -fi - -if [ $failed_count_site2 -ne 0 ]; then - echo "failed with multipart on site2 uploads ${failed_count_site2}" -fi - -export RELEASE=${1} - -docker-compose -f docker-compose-site1.yaml up -d -docker-compose -f docker-compose-site2.yaml up -d - -./mc ready site1/ -./mc ready site2/ - -for i in $(seq 1 10); do - # mc admin heal -r --remove when used against a LB endpoint - # behaves flaky, let this run 10 times before giving up - ./mc admin heal -r --remove --json site1/ 2>&1 >/dev/null - ./mc admin heal -r --remove --json site2/ 2>&1 >/dev/null -done - -failed_count_site1=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l) -failed_count_site2=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l) - -if [ $failed_count_site1 -ne 0 ]; then - echo "failed with multipart on site1 uploads" - exit 1 -fi - -if [ $failed_count_site2 -ne 0 ]; then - echo "failed with multipart on site2 uploads" - exit 1 -fi - -# Add user group test -./mc admin user add site1 site-replication-issue-user site-replication-issue-password -./mc admin group add site1 site-replication-issue-group site-replication-issue-user - -max_wait_attempts=30 -wait_interval=5 - -attempt=1 -while true; do - diff <(./mc admin group info site1 site-replication-issue-group) <(./mc admin group info site2 site-replication-issue-group) - - if [[ $? -eq 0 ]]; then - echo "Outputs are consistent." - break - fi - - remaining_attempts=$((max_wait_attempts - attempt)) - if ((attempt >= max_wait_attempts)); then - echo "Outputs remain inconsistent after $max_wait_attempts attempts. Exiting with error." - exit 1 - else - echo "Outputs are inconsistent. Waiting for $wait_interval seconds (attempt $attempt/$max_wait_attempts)." - sleep $wait_interval - fi - - ((attempt++)) -done - -status=$(./mc admin group info site1 site-replication-issue-group --json | jq .groupStatus | tr -d '"') - -if [[ $status == "enabled" ]]; then - echo "Success" -else - echo "Expected status: enabled, actual status: $status" - exit 1 -fi - -cleanup - -## change working directory -cd ../../../ diff --git a/.github/workflows/multipart/nginx-site1.conf b/.github/workflows/multipart/nginx-site1.conf deleted file mode 100644 index 1a37b9d2bc964..0000000000000 --- a/.github/workflows/multipart/nginx-site1.conf +++ /dev/null @@ -1,61 +0,0 @@ -user nginx; -worker_processes auto; - -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 4096; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - sendfile on; - keepalive_timeout 65; - - # include /etc/nginx/conf.d/*.conf; - - upstream minio { - server site1-minio1:9000; - server site1-minio2:9000; - server site1-minio3:9000; - server site1-minio4:9000; - } - - server { - listen 9001; - listen [::]:9001; - server_name localhost; - - # To allow special characters in headers - ignore_invalid_headers off; - # Allow any size file to be uploaded. - # Set to a value such as 1000m; to restrict file size to a specific value - client_max_body_size 0; - # To disable buffering - proxy_buffering off; - proxy_request_buffering off; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_connect_timeout 300; - # Default is HTTP/1, keepalive is only enabled in HTTP/1.1 - proxy_http_version 1.1; - proxy_set_header Connection ""; - chunked_transfer_encoding off; - - proxy_pass http://minio; - } - } -} diff --git a/.github/workflows/multipart/nginx-site2.conf b/.github/workflows/multipart/nginx-site2.conf deleted file mode 100644 index b93bba24d6135..0000000000000 --- a/.github/workflows/multipart/nginx-site2.conf +++ /dev/null @@ -1,61 +0,0 @@ -user nginx; -worker_processes auto; - -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 4096; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - sendfile on; - keepalive_timeout 65; - - # include /etc/nginx/conf.d/*.conf; - - upstream minio { - server site2-minio1:9000; - server site2-minio2:9000; - server site2-minio3:9000; - server site2-minio4:9000; - } - - server { - listen 9002; - listen [::]:9002; - server_name localhost; - - # To allow special characters in headers - ignore_invalid_headers off; - # Allow any size file to be uploaded. - # Set to a value such as 1000m; to restrict file size to a specific value - client_max_body_size 0; - # To disable buffering - proxy_buffering off; - proxy_request_buffering off; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_connect_timeout 300; - # Default is HTTP/1, keepalive is only enabled in HTTP/1.1 - proxy_http_version 1.1; - proxy_set_header Connection ""; - chunked_transfer_encoding off; - - proxy_pass http://minio; - } - } -} diff --git a/.github/workflows/replication.yaml b/.github/workflows/replication.yaml deleted file mode 100644 index 18c3277496b7b..0000000000000 --- a/.github/workflows/replication.yaml +++ /dev/null @@ -1,79 +0,0 @@ -name: MinIO advanced tests - -on: - pull_request: - branches: - - master - -# This ensures that previous jobs for the PR are canceled when the PR is -# updated. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - replication-test: - name: Advanced Tests with Go ${{ matrix.go-version }} - runs-on: ubuntu-latest - - strategy: - matrix: - go-version: [1.24.x] - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - - name: Test Decom - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make test-decom - - - name: Test ILM - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make test-ilm - make test-ilm-transition - - - name: Test PBAC - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make test-pbac - - - name: Test Config File - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make test-configfile - - - name: Test Replication - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make test-replication - - - name: Test MinIO IDP for automatic site replication - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make test-site-replication-minio - - - name: Test Versioning - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make test-versioning - - - name: Test Multipart upload with failures - run: | - sudo sysctl net.ipv6.conf.all.disable_ipv6=0 - sudo sysctl net.ipv6.conf.default.disable_ipv6=0 - make test-multipart diff --git a/.github/workflows/root-disable.yml b/.github/workflows/root-disable.yml deleted file mode 100644 index c08fb8b1fbd9e..0000000000000 --- a/.github/workflows/root-disable.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Root lockdown tests - -on: - pull_request: - branches: - - master - -# This ensures that previous jobs for the PR are canceled when the PR is -# updated. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build: - name: Go ${{ matrix.go-version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - go-version: [1.24.x] - os: [ubuntu-latest] - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - - name: Start root lockdown tests - run: | - make test-root-disable diff --git a/.github/workflows/root.cert b/.github/workflows/root.cert deleted file mode 100644 index 5f220f79bd429..0000000000000 --- a/.github/workflows/root.cert +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBKDCB26ADAgECAhB6vebGMUfKnmBKyqoApRSOMAUGAytlcDAbMRkwFwYDVQQD -DBByb290QHBsYXkubWluLmlvMB4XDTIwMDQzMDE1MjIyNVoXDTI1MDQyOTE1MjIy -NVowGzEZMBcGA1UEAwwQcm9vdEBwbGF5Lm1pbi5pbzAqMAUGAytlcAMhALzn735W -fmSH/ghKs+4iPWziZMmWdiWr/sqvqeW+WwSxozUwMzAOBgNVHQ8BAf8EBAMCB4Aw -EwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAFBgMrZXADQQDZOrGK -b2ATkDlu2pTcP3LyhSBDpYh7V4TvjRkBTRgjkacCzwFLm+mh+7US8V4dBpIDsJ4u -uWoF0y6vbLVGIlkG ------END CERTIFICATE----- diff --git a/.github/workflows/root.key b/.github/workflows/root.key deleted file mode 100644 index 53a47e25da515..0000000000000 --- a/.github/workflows/root.key +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEID9E7FSYWrMD+VjhI6q545cYT9YOyFxZb7UnjEepYDRc ------END PRIVATE KEY----- diff --git a/.github/workflows/run-mint.sh b/.github/workflows/run-mint.sh deleted file mode 100755 index 0bbc1cbaf6150..0000000000000 --- a/.github/workflows/run-mint.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash - -set -ex - -export MODE="$1" -export ACCESS_KEY="$2" -export SECRET_KEY="$3" -export JOB_NAME="$4" -export MINT_MODE="full" - -docker system prune -f || true -docker volume prune -f || true -docker volume rm $(docker volume ls -f dangling=true) || true - -## change working directory -cd .github/workflows/mint - -## always pull latest -docker pull docker.io/minio/mint:edge - -docker-compose -f minio-${MODE}.yaml up -d -sleep 1m - -docker system prune -f || true -docker volume prune -f || true -docker volume rm $(docker volume ls -q -f dangling=true) || true - -# Stop two nodes, one of each pool, to check that all S3 calls work while quorum is still there -[ "${MODE}" == "pools" ] && docker-compose -f minio-${MODE}.yaml stop minio2 -[ "${MODE}" == "pools" ] && docker-compose -f minio-${MODE}.yaml stop minio6 - -# Pause one node, to check that all S3 calls work while one node goes wrong -[ "${MODE}" == "resiliency" ] && docker-compose -f minio-${MODE}.yaml pause minio4 - -docker run --rm --net=mint_default \ - --name="mint-${MODE}-${JOB_NAME}" \ - -e SERVER_ENDPOINT="nginx:9000" \ - -e ACCESS_KEY="${ACCESS_KEY}" \ - -e SECRET_KEY="${SECRET_KEY}" \ - -e ENABLE_HTTPS=0 \ - -e MINT_MODE="${MINT_MODE}" \ - docker.io/minio/mint:edge - -# FIXME: enable this after fixing aws-sdk-java-v2 tests -# # unpause the node, to check that all S3 calls work while one node goes wrong -# [ "${MODE}" == "resiliency" ] && docker-compose -f minio-${MODE}.yaml unpause minio4 -# [ "${MODE}" == "resiliency" ] && docker run --rm --net=mint_default \ -# --name="mint-${MODE}-${JOB_NAME}" \ -# -e SERVER_ENDPOINT="nginx:9000" \ -# -e ACCESS_KEY="${ACCESS_KEY}" \ -# -e SECRET_KEY="${SECRET_KEY}" \ -# -e ENABLE_HTTPS=0 \ -# -e MINT_MODE="${MINT_MODE}" \ -# docker.io/minio/mint:edge - -docker-compose -f minio-${MODE}.yaml down || true -sleep 10s - -docker system prune -f || true -docker volume prune -f || true -docker volume rm $(docker volume ls -q -f dangling=true) || true - -## change working directory -cd ../../../ diff --git a/.github/workflows/shfmt.yml b/.github/workflows/shfmt.yml deleted file mode 100644 index 3e446306bdd55..0000000000000 --- a/.github/workflows/shfmt.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Shell formatting checks - -on: - pull_request: - branches: - - master - -permissions: - contents: read - -jobs: - build: - name: runner / shfmt - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: luizm/action-sh-checker@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SHFMT_OPTS: "-s" - with: - sh_checker_shellcheck_disable: true # disable for now diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml deleted file mode 100644 index 7addb3143b61c..0000000000000 --- a/.github/workflows/typos.yml +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: Spelling -on: [pull_request] - -jobs: - run: - name: Spell Check with Typos - runs-on: ubuntu-latest - steps: - - name: Checkout Actions Repository - uses: actions/checkout@v4 - - - name: Check spelling of repo - uses: crate-ci/typos@master - diff --git a/.github/workflows/upgrade-ci-cd.yaml b/.github/workflows/upgrade-ci-cd.yaml deleted file mode 100644 index d3e71ff593641..0000000000000 --- a/.github/workflows/upgrade-ci-cd.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: Upgrade old version tests - -on: - pull_request: - branches: - - master - -# This ensures that previous jobs for the PR are canceled when the PR is -# updated. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build: - name: Go ${{ matrix.go-version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - go-version: [1.24.x] - os: [ubuntu-latest] - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - - name: Start upgrade tests - run: | - make test-upgrade diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml deleted file mode 100644 index 5dfcde04bde5e..0000000000000 --- a/.github/workflows/vulncheck.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: VulnCheck -on: - pull_request: - branches: - - master - - push: - branches: - - master - -permissions: - contents: read # to fetch code (actions/checkout) - -jobs: - vulncheck: - name: Analysis - runs-on: ubuntu-latest - steps: - - name: Check out code into the Go module directory - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 1.24.x - cached: false - - name: Get official govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@latest - shell: bash - - name: Run govulncheck - run: govulncheck -show verbose ./... - shell: bash