mirror of https://github.com/k3s-io/k3s
297 lines
7.2 KiB
Go
297 lines
7.2 KiB
Go
package etcdsnapshot
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"sort"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
"github.com/erikdubbelboer/gspt"
|
|
k3s "github.com/k3s-io/k3s/pkg/apis/k3s.cattle.io/v1"
|
|
"github.com/k3s-io/k3s/pkg/cli/cmds"
|
|
"github.com/k3s-io/k3s/pkg/clientaccess"
|
|
"github.com/k3s-io/k3s/pkg/cluster/managed"
|
|
"github.com/k3s-io/k3s/pkg/etcd"
|
|
"github.com/k3s-io/k3s/pkg/server"
|
|
util2 "github.com/k3s-io/k3s/pkg/util"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/urfave/cli"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/cli-runtime/pkg/printers"
|
|
)
|
|
|
|
var timeout = 2 * time.Minute
|
|
|
|
// commandSetup setups up common things needed
|
|
// for each etcd command.
|
|
func commandSetup(app *cli.Context, cfg *cmds.Server) (*etcd.SnapshotRequest, *clientaccess.Info, error) {
|
|
// hide process arguments from ps output, since they may contain
|
|
// database credentials or other secrets.
|
|
gspt.SetProcTitle(os.Args[0] + " etcd-snapshot")
|
|
|
|
sr := &etcd.SnapshotRequest{}
|
|
// Operation and name are set by the command handler.
|
|
// Compression, dir, and retention take the server defaults if not overridden on the CLI.
|
|
if app.IsSet("etcd-snapshot-compress") {
|
|
sr.Compress = &cfg.EtcdSnapshotCompress
|
|
}
|
|
if app.IsSet("etcd-snapshot-dir") {
|
|
sr.Dir = &cfg.EtcdSnapshotDir
|
|
}
|
|
if app.IsSet("etcd-snapshot-retention") {
|
|
sr.Retention = &cfg.EtcdSnapshotRetention
|
|
}
|
|
|
|
if cfg.EtcdS3 {
|
|
sr.S3 = &etcd.SnapshotRequestS3{}
|
|
sr.S3.AccessKey = cfg.EtcdS3AccessKey
|
|
sr.S3.Bucket = cfg.EtcdS3BucketName
|
|
sr.S3.Endpoint = cfg.EtcdS3Endpoint
|
|
sr.S3.EndpointCA = cfg.EtcdS3EndpointCA
|
|
sr.S3.Folder = cfg.EtcdS3Folder
|
|
sr.S3.Insecure = cfg.EtcdS3Insecure
|
|
sr.S3.Region = cfg.EtcdS3Region
|
|
sr.S3.SecretKey = cfg.EtcdS3SecretKey
|
|
sr.S3.SkipSSLVerify = cfg.EtcdS3SkipSSLVerify
|
|
sr.S3.Timeout = metav1.Duration{Duration: cfg.EtcdS3Timeout}
|
|
// extend request timeout to allow the S3 operation to complete
|
|
timeout += cfg.EtcdS3Timeout
|
|
}
|
|
|
|
dataDir, err := server.ResolveDataDir(cfg.DataDir)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if cfg.Token == "" {
|
|
fp := filepath.Join(dataDir, "token")
|
|
tokenByte, err := os.ReadFile(fp)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
cfg.Token = string(bytes.TrimRight(tokenByte, "\n"))
|
|
}
|
|
info, err := clientaccess.ParseAndValidateToken(cmds.ServerConfig.ServerURL, cfg.Token, clientaccess.WithUser("server"))
|
|
return sr, info, err
|
|
}
|
|
|
|
func wrapServerError(err error) error {
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
// if the request timed out the server log likely won't contain anything useful,
|
|
// since the operation may have actualy succeeded despite the client timing out the request.
|
|
return err
|
|
}
|
|
return errors.Wrap(err, "see server log for details")
|
|
}
|
|
|
|
// Save triggers an on-demand etcd snapshot operation
|
|
func Save(app *cli.Context) error {
|
|
if err := cmds.InitLogging(); err != nil {
|
|
return err
|
|
}
|
|
return save(app, &cmds.ServerConfig)
|
|
}
|
|
|
|
func save(app *cli.Context, cfg *cmds.Server) error {
|
|
if len(app.Args()) > 0 {
|
|
return util2.ErrCommandNoArgs
|
|
}
|
|
|
|
// Save always sets retention to 0 to disable automatic pruning.
|
|
// Prune can be run manually after save, if desired.
|
|
app.Set("etcd-snapshot-retention", "0")
|
|
|
|
sr, info, err := commandSetup(app, cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sr.Operation = etcd.SnapshotOperationSave
|
|
sr.Name = []string{cfg.EtcdSnapshotName}
|
|
|
|
b, err := json.Marshal(sr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r, err := info.Post("/db/snapshot", b, clientaccess.WithTimeout(timeout))
|
|
if err != nil {
|
|
return wrapServerError(err)
|
|
}
|
|
resp := &managed.SnapshotResult{}
|
|
if err := json.Unmarshal(r, resp); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, name := range resp.Created {
|
|
logrus.Infof("Snapshot %s saved.", name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func Delete(app *cli.Context) error {
|
|
if err := cmds.InitLogging(); err != nil {
|
|
return err
|
|
}
|
|
return delete(app, &cmds.ServerConfig)
|
|
}
|
|
|
|
func delete(app *cli.Context, cfg *cmds.Server) error {
|
|
snapshots := app.Args()
|
|
if len(snapshots) == 0 {
|
|
return errors.New("no snapshots given for removal")
|
|
}
|
|
|
|
sr, info, err := commandSetup(app, cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sr.Operation = etcd.SnapshotOperationDelete
|
|
sr.Name = snapshots
|
|
|
|
b, err := json.Marshal(sr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r, err := info.Post("/db/snapshot", b, clientaccess.WithTimeout(timeout))
|
|
if err != nil {
|
|
return wrapServerError(err)
|
|
}
|
|
resp := &managed.SnapshotResult{}
|
|
if err := json.Unmarshal(r, resp); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, name := range resp.Deleted {
|
|
logrus.Infof("Snapshot %s deleted.", name)
|
|
}
|
|
for _, name := range snapshots {
|
|
if !slices.Contains(resp.Deleted, name) {
|
|
logrus.Warnf("Snapshot %s not found.", name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func List(app *cli.Context) error {
|
|
if err := cmds.InitLogging(); err != nil {
|
|
return err
|
|
}
|
|
return list(app, &cmds.ServerConfig)
|
|
}
|
|
|
|
var etcdListFormats = []string{"json", "yaml", "table"}
|
|
|
|
func validEtcdListFormat(format string) bool {
|
|
for _, supportedFormat := range etcdListFormats {
|
|
if format == supportedFormat {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func list(app *cli.Context, cfg *cmds.Server) error {
|
|
if cfg.EtcdListFormat != "" && !validEtcdListFormat(cfg.EtcdListFormat) {
|
|
return errors.New("invalid output format: " + cfg.EtcdListFormat)
|
|
}
|
|
|
|
sr, info, err := commandSetup(app, cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sr.Operation = etcd.SnapshotOperationList
|
|
|
|
b, err := json.Marshal(sr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r, err := info.Post("/db/snapshot", b, clientaccess.WithTimeout(timeout))
|
|
if err != nil {
|
|
return wrapServerError(err)
|
|
}
|
|
|
|
sf := &k3s.ETCDSnapshotFileList{}
|
|
if err := json.Unmarshal(r, sf); err != nil {
|
|
return err
|
|
}
|
|
|
|
sort.Slice(sf.Items, func(i, j int) bool {
|
|
if sf.Items[i].Status.CreationTime.Equal(sf.Items[j].Status.CreationTime) {
|
|
return sf.Items[i].Spec.SnapshotName < sf.Items[j].Spec.SnapshotName
|
|
}
|
|
return sf.Items[i].Status.CreationTime.Before(sf.Items[j].Status.CreationTime)
|
|
})
|
|
|
|
switch cfg.EtcdListFormat {
|
|
case "json":
|
|
json := printers.JSONPrinter{}
|
|
if err := json.PrintObj(sf, os.Stdout); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
case "yaml":
|
|
yaml := printers.YAMLPrinter{}
|
|
if err := yaml.PrintObj(sf, os.Stdout); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
default:
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
|
defer w.Flush()
|
|
|
|
fmt.Fprint(w, "Name\tLocation\tSize\tCreated\n")
|
|
for _, esf := range sf.Items {
|
|
fmt.Fprintf(w, "%s\t%s\t%d\t%s\n", esf.Spec.SnapshotName, esf.Spec.Location, esf.Status.Size.Value(), esf.Status.CreationTime.Format(time.RFC3339))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func Prune(app *cli.Context) error {
|
|
if err := cmds.InitLogging(); err != nil {
|
|
return err
|
|
}
|
|
return prune(app, &cmds.ServerConfig)
|
|
}
|
|
|
|
func prune(app *cli.Context, cfg *cmds.Server) error {
|
|
sr, info, err := commandSetup(app, cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sr.Operation = etcd.SnapshotOperationPrune
|
|
sr.Name = []string{cfg.EtcdSnapshotName}
|
|
|
|
b, err := json.Marshal(sr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r, err := info.Post("/db/snapshot", b, clientaccess.WithTimeout(timeout))
|
|
if err != nil {
|
|
return wrapServerError(err)
|
|
}
|
|
resp := &managed.SnapshotResult{}
|
|
if err := json.Unmarshal(r, resp); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, name := range resp.Deleted {
|
|
logrus.Infof("Snapshot %s deleted.", name)
|
|
}
|
|
|
|
return nil
|
|
}
|