Files
Jamie LiuandgVisor bot 0c7f4cfa6a Implement GCS save/restore.
When `runsc checkpoint`, `runsc restore`, `runsc fscheckpoint`, or `runsc
create --fs-restore-image-path` is used with an image-path directory containing
a `gcs_opts.json` file, whose format is specified by
`runsc/checkpointgofer.GCSOptions`, save checkpoints to / restore checkpoints
from Google Cloud Storage instead of local disk.

PiperOrigin-RevId: 915653601
2026-05-14 15:43:12 -07:00

174 lines
4.7 KiB
Go

// Copyright 2025 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gcs
import (
"context"
"fmt"
"cloud.google.com/go/storage"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/memmap"
"gvisor.dev/gvisor/pkg/sentry/state/stateio"
"gvisor.dev/gvisor/pkg/sync"
)
// Writer implements stateio.AsyncWriter for a GCS object.
type Writer struct {
stateio.NoRegisterClientFD
maxWriteBytes uint64
maxRanges int
obj *storage.ObjectHandle
writer *storage.Writer
subs chan writeSubmission
cmps chan stateio.Completion
cancel context.CancelCauseFunc
worker sync.WaitGroup
}
type writeSubmission struct {
id int
src stateio.LocalClientRanges
}
// NewWriter returns a Writer that writes to the given GCS object.
func NewWriter(ctx context.Context, obj *storage.ObjectHandle, maxWriteBytes uint64, maxRanges, maxParallel int) *Writer {
ctx, cancel := context.WithCancelCause(ctx)
w := &Writer{
maxWriteBytes: maxWriteBytes,
maxRanges: maxRanges,
obj: obj,
writer: obj.NewWriter(ctx),
subs: make(chan writeSubmission, maxParallel),
cmps: make(chan stateio.Completion, maxParallel),
cancel: cancel,
}
w.writer.ChunkSize = int(w.maxWriteBytes)
// Set Content-Type explicitly to avoid wasting time on Content-Type
// autodetection.
w.writer.ContentType = contentType
w.worker.Add(1)
go w.workerMain(ctx)
return w
}
// Close implements stateio.AsyncWriter.Close.
func (w *Writer) Close() error {
w.cancel(fmt.Errorf("context canceled by Writer.Close"))
w.worker.Wait()
if w.writer != nil {
// We don't need to convert this to an errno for two reasons:
//
// - Finalize was never called, so the error returned by
// w.writer.Close() is ultimately irrelevant.
//
// - stateipc implements stateio.AsyncReader/Writer.Close() via URPC;
// URPC preserves error strings, but loses error types.
return w.writer.Close()
}
// The error from w.writer.Close() was already returned by w.Finalize(); we
// don't need to return it again.
return nil
}
// MaxWriteBytes implements stateio.AsyncWriter.MaxWriteBytes.
func (w *Writer) MaxWriteBytes() uint64 {
return w.maxWriteBytes
}
// MaxRanges implements stateio.AsyncWriter.MaxRanges.
func (w *Writer) MaxRanges() int {
return w.maxRanges
}
// MaxParallel implements stateio.AsyncWriter.MaxParallel.
func (w *Writer) MaxParallel() int {
return cap(w.subs)
}
// AddWrite implements stateio.AsyncWriter.AddWrite.
func (w *Writer) AddWrite(id int, _ stateio.SourceFile, _ memmap.FileRange, srcMap []byte) {
w.subs <- writeSubmission{
id: id,
src: stateio.LocalClientMapping(srcMap),
}
}
// AddWritev implements stateio.AsyncWriter.AddWritev.
func (w *Writer) AddWritev(id int, total uint64, _ stateio.SourceFile, _ []memmap.FileRange, srcMaps []unix.Iovec) {
w.subs <- writeSubmission{
id: id,
src: stateio.LocalClientMappings(srcMaps),
}
}
// Wait implements stateio.AsyncWriter.Wait.
func (w *Writer) Wait(cs []stateio.Completion, minCompletions int) ([]stateio.Completion, error) {
return stateio.CompletionChanWait(w.cmps, cs, minCompletions)
}
// Reserve implements stateio.AsyncWriter.Reserve.
func (w *Writer) Reserve(n uint64) {
// no-op
}
// Finalize implements stateio.AsyncWriter.Finalize.
func (w *Writer) Finalize() error {
sw := w.writer
w.writer = nil
if err := sw.Close(); err != nil {
if code, ok := httpCodeFromError(err); ok && isPermissionDeniedCode(code) {
log.Infof("gcs.Writer returning EACCES for close error: %v", err)
return unix.EACCES
}
return err
}
return nil
}
func (w *Writer) workerMain(ctx context.Context) {
defer w.worker.Done()
for {
select {
case <-ctx.Done():
return
case sub := <-w.subs:
var done uint64
var doneErr error
for _, src := range sub.src.Mappings {
n, err := w.writer.Write(src)
done += uint64(n)
if err != nil {
doneErr = err
break
}
}
if doneErr != nil {
if code, ok := httpCodeFromError(doneErr); ok && isPermissionDeniedCode(code) {
log.Infof("gcs.Writer returning EACCES for write error: %v", doneErr)
doneErr = unix.EACCES
}
}
w.cmps <- stateio.Completion{
ID: sub.id,
N: done,
Err: doneErr,
}
}
}
}