mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
6.8 KiB
218 lines
6.8 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: BUSL-1.1 |
|
|
|
package decode |
|
|
|
import ( |
|
"encoding/json" |
|
"flag" |
|
"fmt" |
|
"io" |
|
"os" |
|
"path" |
|
"strings" |
|
|
|
"github.com/hashicorp/consul-net-rpc/go-msgpack/codec" |
|
"github.com/hashicorp/consul/agent/consul/fsm" |
|
"github.com/hashicorp/consul/agent/consul/state" |
|
"github.com/hashicorp/consul/agent/structs" |
|
"github.com/hashicorp/consul/command/flags" |
|
"github.com/hashicorp/consul/proto-public/pbresource" |
|
"github.com/hashicorp/consul/proto/private/pbpeering" |
|
"github.com/hashicorp/consul/snapshot" |
|
"github.com/hashicorp/go-hclog" |
|
"github.com/hashicorp/go-raftchunking" |
|
"github.com/hashicorp/raft" |
|
"github.com/mitchellh/cli" |
|
) |
|
|
|
var requestTypeZeroValues = map[structs.MessageType]func() any{ |
|
structs.RegisterRequestType: func() any { return new(structs.RegisterRequest) }, |
|
structs.KVSRequestType: func() any { return new(structs.KVSRequest) }, |
|
structs.SessionRequestType: func() any { return new(structs.SessionRequest) }, |
|
structs.TombstoneRequestType: func() any { return new(structs.TombstoneRequest) }, |
|
structs.CoordinateBatchUpdateType: func() any { return new(structs.Coordinates) }, |
|
structs.PreparedQueryRequestType: func() any { return new(structs.PreparedQueryRequest) }, |
|
structs.AutopilotRequestType: func() any { return new(structs.AutopilotSetConfigRequest) }, |
|
structs.IntentionRequestType: func() any { return new(structs.IntentionRequest) }, |
|
structs.ConnectCARequestType: func() any { return new(structs.CARequest) }, |
|
structs.ConnectCAProviderStateType: func() any { return new(structs.CAConsulProviderState) }, |
|
structs.ConnectCAConfigType: func() any { return new(structs.CAConfiguration) }, |
|
structs.IndexRequestType: func() any { return new(state.IndexEntry) }, |
|
structs.ACLTokenSetRequestType: func() any { return new(structs.ACLToken) }, |
|
structs.ACLPolicySetRequestType: func() any { return new(structs.ACLPolicy) }, |
|
structs.ConfigEntryRequestType: func() any { return new(structs.ConfigEntryRequest) }, |
|
structs.ACLRoleSetRequestType: func() any { return new(structs.ACLRole) }, |
|
structs.ACLBindingRuleSetRequestType: func() any { return new(structs.ACLBindingRule) }, |
|
structs.ACLAuthMethodSetRequestType: func() any { return new(structs.ACLAuthMethod) }, |
|
structs.ChunkingStateType: func() any { return new(raftchunking.State) }, |
|
structs.FederationStateRequestType: func() any { return new(structs.FederationStateRequest) }, |
|
structs.SystemMetadataRequestType: func() any { return new(structs.SystemMetadataEntry) }, |
|
structs.ServiceVirtualIPRequestType: func() any { return new(state.ServiceVirtualIP) }, |
|
structs.FreeVirtualIPRequestType: func() any { return new(state.FreeVirtualIP) }, |
|
structs.PeeringWriteType: func() any { return new(pbpeering.Peering) }, |
|
structs.PeeringTrustBundleWriteType: func() any { return new(pbpeering.PeeringTrustBundle) }, |
|
structs.PeeringSecretsWriteType: func() any { return new(pbpeering.PeeringSecrets) }, |
|
structs.ResourceOperationType: func() any { return new(pbresource.Resource) }, |
|
} |
|
|
|
func New(ui cli.Ui) *cmd { |
|
c := &cmd{UI: ui} |
|
c.init() |
|
return c |
|
} |
|
|
|
type cmd struct { |
|
UI cli.Ui |
|
flags *flag.FlagSet |
|
help string |
|
format string |
|
|
|
encoder *json.Encoder |
|
} |
|
|
|
func (c *cmd) Write(p []byte) (n int, err error) { |
|
s := string(p) |
|
c.UI.Output(strings.TrimRight(s, "\n")) |
|
return len(s), nil |
|
} |
|
|
|
func (c *cmd) init() { |
|
c.flags = flag.NewFlagSet("", flag.ContinueOnError) |
|
c.help = flags.Usage(help, c.flags) |
|
c.encoder = json.NewEncoder(c) |
|
} |
|
|
|
func (c *cmd) Run(args []string) int { |
|
if err := c.flags.Parse(args); err != nil { |
|
c.UI.Error(err.Error()) |
|
return 1 |
|
} |
|
|
|
var file string |
|
args = c.flags.Args() |
|
|
|
switch len(args) { |
|
case 0: |
|
c.UI.Error("Missing FILE argument") |
|
return 1 |
|
case 1: |
|
file = args[0] |
|
default: |
|
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) |
|
return 1 |
|
} |
|
|
|
// Open the file. |
|
f, err := os.Open(file) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Error opening snapshot file: %s", err)) |
|
return 1 |
|
} |
|
defer f.Close() |
|
|
|
var readFile *os.File |
|
var meta *raft.SnapshotMeta |
|
|
|
if strings.ToLower(path.Base(file)) == "state.bin" { |
|
// This is an internal raw raft snapshot not a gzipped archive one |
|
// downloaded from the API, we can read it directly |
|
readFile = f |
|
|
|
// Assume the meta is colocated and error if not. |
|
metaRaw, err := os.ReadFile(path.Join(path.Dir(file), "meta.json")) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Error reading meta.json from internal snapshot dir: %s", err)) |
|
return 1 |
|
} |
|
var metaDecoded raft.SnapshotMeta |
|
err = json.Unmarshal(metaRaw, &metaDecoded) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Error parsing meta.json from internal snapshot dir: %s", err)) |
|
return 1 |
|
} |
|
meta = &metaDecoded |
|
} else { |
|
readFile, meta, err = snapshot.Read(hclog.New(nil), f) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Error reading snapshot: %s", err)) |
|
return 1 |
|
} |
|
defer func() { |
|
if err := readFile.Close(); err != nil { |
|
c.UI.Error(fmt.Sprintf("Failed to close temp snapshot: %v", err)) |
|
} |
|
if err := os.Remove(readFile.Name()); err != nil { |
|
c.UI.Error(fmt.Sprintf("Failed to clean up temp snapshot: %v", err)) |
|
} |
|
}() |
|
} |
|
|
|
err = c.encoder.Encode(map[string]interface{}{ |
|
"Type": "SnapshotHeader", |
|
"Data": meta, |
|
}) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Error encoding snapshot header to the output stream: %v", err)) |
|
return 1 |
|
} |
|
|
|
err = c.decodeStream(readFile) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Error extracting snapshot data: %s", err)) |
|
return 1 |
|
} |
|
|
|
return 0 |
|
} |
|
|
|
// enhance utilizes ReadSnapshot to populate the struct with |
|
// all of the snapshot's itemized data |
|
func (c *cmd) decodeStream(file io.Reader) error { |
|
handler := func(header *fsm.SnapshotHeader, msg structs.MessageType, dec *codec.Decoder) error { |
|
name := structs.MessageType.String(msg) |
|
var val interface{} |
|
|
|
if zeroVal, ok := requestTypeZeroValues[msg]; ok { |
|
val = zeroVal() |
|
} |
|
|
|
err := dec.Decode(&val) |
|
if err != nil { |
|
return fmt.Errorf("failed to decode msg type %v, error %v", name, err) |
|
} |
|
|
|
err = c.encoder.Encode(map[string]interface{}{ |
|
"Type": name, |
|
"Data": val, |
|
}) |
|
|
|
if err != nil { |
|
return fmt.Errorf("failed to encode data into the object stream: %w", err) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
return fsm.ReadSnapshot(file, handler) |
|
} |
|
|
|
func (c *cmd) Synopsis() string { |
|
return synopsis |
|
} |
|
|
|
func (c *cmd) Help() string { |
|
return c.help |
|
} |
|
|
|
const synopsis = "Decodes the binary" |
|
const help = ` |
|
Usage: consul snapshot decode [options] FILE |
|
|
|
Decodes snapshot data and outputs a stream of line delimited JSON objects of the form: |
|
|
|
{"Type": "SnapshotHeader", "Data": {"<json encoded snapshot header>"}} |
|
{"Type": "<type name>","Data": {<json encoded data>}} |
|
{"Type": "<type name>","Data": {<json encoded data>}} |
|
... |
|
`
|
|
|