This patch changes the behaviour of `tailscale lock log --json` to make
it more useful for users. It also introduces versioning of our JSON output.
## Changes to `tailscale lock log --json`
Previously this command would print the hash and base64-encoded bytes of
each AUM, and users would need their own CBOR decoder to interpret it in
a useful way:
```json
[
{
"Hash": [
80,
136,
151,
…
],
"Change": "checkpoint",
"Raw": "pAEFAvYFpQH2AopYIAkPN+8V3cJpkoC5ZY2+RI2Bcg2q5G7tRAQQd67W3YpnWCDPOo4KGeQBd8hdGsjoEQpSXyiPdlm+NXAlJ5dS1qEbFlggylNJDQM5ZQ2ULNsXxg2ZBFkPl/D93I1M56/rowU+UIlYIPZ/SxT9EA2Idy9kaCbsFzjX/s3Ms7584wWGbWd/f/QAWCBHYZzYiAPpQ+NXN+1Wn2fopQYk4yl7kNQcMXUKNAdt1lggcfjcuVACOH0J9pRNvYZQFOkbiBmLOW1hPKJsbC1D1GdYIKrJ38XMgpVMuTuBxM4YwoLmrK/RgXQw1uVEL3cywl3QWCA0FilVVv8uys8BNhS62cfNvCew1Pw5wIgSe3Prv8d8pFggQrwIt6ldYtyFPQcC5V18qrCnt7VpThACaz5RYzpx7RNYIKskOA7UoNiVtMkOrV2QoXv6EvDpbO26a01lVeh8UCeEA4KjAQECAQNYIORIdNHqSOzz1trIygnP5w3JWK2DtlY5NDIBbD7SKcjWowEBAgEDWCD27LpxiZNiA19k0QZhOWmJRvBdK2mz+dHu7rf0iGTPFwQb69Gt42fKNn0FGwRUiav/k6dDF4GiAVgg5Eh00epI7PPW2sjKCc/nDclYrYO2Vjk0MgFsPtIpyNYCWEDzIAooc+m45ay5PB/OB4AA9Fdki4KJq9Ll+PF6IJHYlOVhpTbc3E0KF7ODu1WURd0f7PXnW72dr89CSfGxIHAF"
}
]
```
Now we print the AUM in an expanded form that can be easily read by scripts,
although we include the raw bytes for verification and auditing.
```json
{
"SchemaVersion": "1",
"Messages": [
{
"Hash": "KCEJPRKNSXJG2TPH3EHQRLJNLIIK2DV53FUNPADWA7BZJWBDRXZQ",
"AUM": {
"MessageKind": "checkpoint",
"PrevAUMHash": null,
"Key": null,
"KeyID": null,
"State": {
…
},
"Votes": null,
"Meta": null,
"Signatures": [
{
"KeyID": "tlpub:e44874d1ea48ecf3d6dac8ca09cfe70dc958ad83b656393432016c3ed229c8d6",
"Signature": "8yAKKHPpuOWsuTwfzgeAAPRXZIuCiavS5fjxeiCR2JTlYaU23NxNChezg7tVlEXdH+z151u9na/PQknxsSBwBQ=="
}
]
},
"Raw": "pAEFAvYFpQH2AopYIAkPN-8V3cJpkoC5ZY2-RI2Bcg2q5G7tRAQQd67W3YpnWCDPOo4KGeQBd8hdGsjoEQpSXyiPdlm-NXAlJ5dS1qEbFlggylNJDQM5ZQ2ULNsXxg2ZBFkPl_D93I1M56_rowU-UIlYIPZ_SxT9EA2Idy9kaCbsFzjX_s3Ms7584wWGbWd_f_QAWCBHYZzYiAPpQ-NXN-1Wn2fopQYk4yl7kNQcMXUKNAdt1lggcfjcuVACOH0J9pRNvYZQFOkbiBmLOW1hPKJsbC1D1GdYIKrJ38XMgpVMuTuBxM4YwoLmrK_RgXQw1uVEL3cywl3QWCA0FilVVv8uys8BNhS62cfNvCew1Pw5wIgSe3Prv8d8pFggQrwIt6ldYtyFPQcC5V18qrCnt7VpThACaz5RYzpx7RNYIKskOA7UoNiVtMkOrV2QoXv6EvDpbO26a01lVeh8UCeEA4KjAQECAQNYIORIdNHqSOzz1trIygnP5w3JWK2DtlY5NDIBbD7SKcjWowEBAgEDWCD27LpxiZNiA19k0QZhOWmJRvBdK2mz-dHu7rf0iGTPFwQb69Gt42fKNn0FGwRUiav_k6dDF4GiAVgg5Eh00epI7PPW2sjKCc_nDclYrYO2Vjk0MgFsPtIpyNYCWEDzIAooc-m45ay5PB_OB4AA9Fdki4KJq9Ll-PF6IJHYlOVhpTbc3E0KF7ODu1WURd0f7PXnW72dr89CSfGxIHAF"
}
]
}
```
This output was previously marked as unstable, and it wasn't very useful,
so changing it should be fine.
## Versioning our JSON output
This patch introduces a way to version our JSON output on the CLI, so we
can make backwards-incompatible changes in future without breaking existing
scripts or integrations.
You can run this command in two ways:
```
tailscale lock log --json
tailscale lock log --json=1
```
Passing an explicit version number allows you to pick a specific JSON schema.
If we ever want to change the schema, we increment the version number and
users must opt-in to the new output.
A bare `--json` flag will always return schema version 1, for compatibility
with existing scripts.
Updates https://github.com/tailscale/tailscale/issues/17613
Updates https://github.com/tailscale/corp/issues/23258
Signed-off-by: Alex Chan <alexc@tailscale.com>
Change-Id: I897f78521cc1a81651f5476228c0882d7b723606
main
parent
99b06eac49
commit
139c395d7d
@ -0,0 +1,84 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package jsonoutput provides stable and versioned JSON serialisation for CLI output.
|
||||
// This allows us to provide stable output to scripts/clients, but also make
|
||||
// breaking changes to the output when it's useful.
|
||||
//
|
||||
// Historically we only used `--json` as a boolean flag, so changing the output
|
||||
// could break scripts that rely on the existing format.
|
||||
//
|
||||
// This package allows callers to pass a version number to `--json` and get
|
||||
// a consistent output. We'll bump the version when we make a breaking change
|
||||
// that's likely to break scripts that rely on the existing output, e.g. if
|
||||
// we remove a field or change the type/format.
|
||||
//
|
||||
// Passing just the boolean flag `--json` will always return v1, to preserve
|
||||
// compatibility with scripts written before we versioned our output.
|
||||
package jsonoutput |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"strconv" |
||||
) |
||||
|
||||
// JSONSchemaVersion implements flag.Value, and tracks whether the CLI has
|
||||
// been called with `--json`, and if so, with what value.
|
||||
type JSONSchemaVersion struct { |
||||
// IsSet tracks if the flag was provided at all.
|
||||
IsSet bool |
||||
|
||||
// Value tracks the desired schema version, which defaults to 1 if
|
||||
// the user passes `--json` without an argument.
|
||||
Value int |
||||
} |
||||
|
||||
// String returns the default value which is printed in the CLI help text.
|
||||
func (v *JSONSchemaVersion) String() string { |
||||
if v.IsSet { |
||||
return strconv.Itoa(v.Value) |
||||
} else { |
||||
return "(not set)" |
||||
} |
||||
} |
||||
|
||||
// Set is called when the user passes the flag as a command-line argument.
|
||||
func (v *JSONSchemaVersion) Set(s string) error { |
||||
if v.IsSet { |
||||
return errors.New("received multiple instances of --json; only pass it once") |
||||
} |
||||
|
||||
v.IsSet = true |
||||
|
||||
// If the user doesn't supply a schema version, default to 1.
|
||||
// This ensures that any existing scripts will continue to get their
|
||||
// current output.
|
||||
if s == "true" { |
||||
v.Value = 1 |
||||
return nil |
||||
} |
||||
|
||||
version, err := strconv.Atoi(s) |
||||
if err != nil { |
||||
return fmt.Errorf("invalid integer value passed to --json: %q", s) |
||||
} |
||||
v.Value = version |
||||
return nil |
||||
} |
||||
|
||||
// IsBoolFlag tells the flag package that JSONSchemaVersion can be set
|
||||
// without an argument.
|
||||
func (v *JSONSchemaVersion) IsBoolFlag() bool { |
||||
return true |
||||
} |
||||
|
||||
// ResponseEnvelope is a set of fields common to all versioned JSON output.
|
||||
type ResponseEnvelope struct { |
||||
// SchemaVersion is the version of the JSON output, e.g. "1", "2", "3"
|
||||
SchemaVersion string |
||||
|
||||
// ResponseWarning tells a user if a newer version of the JSON output
|
||||
// is available.
|
||||
ResponseWarning string `json:"_WARNING,omitzero"` |
||||
} |
||||
@ -0,0 +1,203 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package jsonoutput |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/base64" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
|
||||
"tailscale.com/ipn/ipnstate" |
||||
"tailscale.com/tka" |
||||
) |
||||
|
||||
// PrintNetworkLockJSONV1 prints the stored TKA state as a JSON object to the CLI,
|
||||
// in a stable "v1" format.
|
||||
//
|
||||
// This format includes:
|
||||
//
|
||||
// - the AUM hash as a base32-encoded string
|
||||
// - the raw AUM as base64-encoded bytes
|
||||
// - the expanded AUM, which prints named fields for consumption by other tools
|
||||
func PrintNetworkLockJSONV1(out io.Writer, updates []ipnstate.NetworkLockUpdate) error { |
||||
messages := make([]logMessageV1, len(updates)) |
||||
|
||||
for i, update := range updates { |
||||
var aum tka.AUM |
||||
if err := aum.Unserialize(update.Raw); err != nil { |
||||
return fmt.Errorf("decoding: %w", err) |
||||
} |
||||
|
||||
h := aum.Hash() |
||||
|
||||
if !bytes.Equal(h[:], update.Hash[:]) { |
||||
return fmt.Errorf("incorrect AUM hash: got %v, want %v", h, update) |
||||
} |
||||
|
||||
messages[i] = toLogMessageV1(aum, update) |
||||
} |
||||
|
||||
result := struct { |
||||
ResponseEnvelope |
||||
Messages []logMessageV1 |
||||
}{ |
||||
ResponseEnvelope: ResponseEnvelope{ |
||||
SchemaVersion: "1", |
||||
}, |
||||
Messages: messages, |
||||
} |
||||
|
||||
enc := json.NewEncoder(out) |
||||
enc.SetIndent("", " ") |
||||
return enc.Encode(result) |
||||
} |
||||
|
||||
// toLogMessageV1 converts a [tka.AUM] and [ipnstate.NetworkLockUpdate] to the
|
||||
// JSON output returned by the CLI.
|
||||
func toLogMessageV1(aum tka.AUM, update ipnstate.NetworkLockUpdate) logMessageV1 { |
||||
expandedAUM := expandedAUMV1{} |
||||
expandedAUM.MessageKind = aum.MessageKind.String() |
||||
if len(aum.PrevAUMHash) > 0 { |
||||
expandedAUM.PrevAUMHash = aum.PrevAUMHash.String() |
||||
} |
||||
if key := aum.Key; key != nil { |
||||
expandedAUM.Key = toExpandedKeyV1(key) |
||||
} |
||||
if keyID := aum.KeyID; keyID != nil { |
||||
expandedAUM.KeyID = fmt.Sprintf("tlpub:%x", keyID) |
||||
} |
||||
if state := aum.State; state != nil { |
||||
expandedState := expandedStateV1{} |
||||
if h := state.LastAUMHash; h != nil { |
||||
expandedState.LastAUMHash = h.String() |
||||
} |
||||
for _, secret := range state.DisablementSecrets { |
||||
expandedState.DisablementSecrets = append(expandedState.DisablementSecrets, fmt.Sprintf("%x", secret)) |
||||
} |
||||
for _, key := range state.Keys { |
||||
expandedState.Keys = append(expandedState.Keys, toExpandedKeyV1(&key)) |
||||
} |
||||
expandedState.StateID1 = state.StateID1 |
||||
expandedState.StateID2 = state.StateID2 |
||||
expandedAUM.State = expandedState |
||||
} |
||||
if votes := aum.Votes; votes != nil { |
||||
expandedAUM.Votes = *votes |
||||
} |
||||
expandedAUM.Meta = aum.Meta |
||||
for _, signature := range aum.Signatures { |
||||
expandedAUM.Signatures = append(expandedAUM.Signatures, expandedSignatureV1{ |
||||
KeyID: fmt.Sprintf("tlpub:%x", signature.KeyID), |
||||
Signature: base64.URLEncoding.EncodeToString(signature.Signature), |
||||
}) |
||||
} |
||||
|
||||
return logMessageV1{ |
||||
Hash: aum.Hash().String(), |
||||
AUM: expandedAUM, |
||||
Raw: base64.URLEncoding.EncodeToString(update.Raw), |
||||
} |
||||
} |
||||
|
||||
// toExpandedKeyV1 converts a [tka.Key] to the JSON output returned
|
||||
// by the CLI.
|
||||
func toExpandedKeyV1(key *tka.Key) expandedKeyV1 { |
||||
return expandedKeyV1{ |
||||
Kind: key.Kind.String(), |
||||
Votes: key.Votes, |
||||
Public: fmt.Sprintf("tlpub:%x", key.Public), |
||||
Meta: key.Meta, |
||||
} |
||||
} |
||||
|
||||
// logMessageV1 is the JSON representation of an AUM as both raw bytes and
|
||||
// in its expanded form, and the CLI output is a list of these entries.
|
||||
type logMessageV1 struct { |
||||
// The BLAKE2s digest of the CBOR-encoded AUM. This is printed as a
|
||||
// base32-encoded string, e.g. KCE…XZQ
|
||||
Hash string |
||||
|
||||
// The expanded form of the AUM, which presents the fields in a more
|
||||
// accessible format than doing a CBOR decoding.
|
||||
AUM expandedAUMV1 |
||||
|
||||
// The raw bytes of the CBOR-encoded AUM, encoded as base64.
|
||||
// This is useful for verifying the AUM hash.
|
||||
Raw string |
||||
} |
||||
|
||||
// expandedAUMV1 is the expanded version of a [tka.AUM], designed so external tools
|
||||
// can read the AUM without knowing our CBOR definitions.
|
||||
type expandedAUMV1 struct { |
||||
MessageKind string |
||||
PrevAUMHash string `json:"PrevAUMHash,omitzero"` |
||||
|
||||
// Key encodes a public key to be added to the key authority.
|
||||
// This field is used for AddKey AUMs.
|
||||
Key expandedKeyV1 `json:"Key,omitzero"` |
||||
|
||||
// KeyID references a public key which is part of the key authority.
|
||||
// This field is used for RemoveKey and UpdateKey AUMs.
|
||||
KeyID string `json:"KeyID,omitzero"` |
||||
|
||||
// State describes the full state of the key authority.
|
||||
// This field is used for Checkpoint AUMs.
|
||||
State expandedStateV1 `json:"State,omitzero"` |
||||
|
||||
// Votes and Meta describe properties of a key in the key authority.
|
||||
// These fields are used for UpdateKey AUMs.
|
||||
Votes uint `json:"Votes,omitzero"` |
||||
Meta map[string]string `json:"Meta,omitzero"` |
||||
|
||||
// Signatures lists the signatures over this AUM.
|
||||
Signatures []expandedSignatureV1 `json:"Signatures,omitzero"` |
||||
} |
||||
|
||||
// expandedAUMV1 is the expanded version of a [tka.Key], which describes
|
||||
// the public components of a key known to network-lock.
|
||||
type expandedKeyV1 struct { |
||||
Kind string |
||||
|
||||
// Votes describes the weight applied to signatures using this key.
|
||||
Votes uint |
||||
|
||||
// Public encodes the public key of the key as a hex string.
|
||||
Public string |
||||
|
||||
// Meta describes arbitrary metadata about the key. This could be
|
||||
// used to store the name of the key, for instance.
|
||||
Meta map[string]string `json:"Meta,omitzero"` |
||||
} |
||||
|
||||
// expandedStateV1 is the expanded version of a [tka.State], which describes
|
||||
// Tailnet Key Authority state at an instant in time.
|
||||
type expandedStateV1 struct { |
||||
// LastAUMHash is the blake2s digest of the last-applied AUM.
|
||||
LastAUMHash string `json:"LastAUMHash,omitzero"` |
||||
|
||||
// DisablementSecrets are KDF-derived values which can be used
|
||||
// to turn off the TKA in the event of a consensus-breaking bug.
|
||||
DisablementSecrets []string |
||||
|
||||
// Keys are the public keys of either:
|
||||
//
|
||||
// 1. The signing nodes currently trusted by the TKA.
|
||||
// 2. Ephemeral keys that were used to generate pre-signed auth keys.
|
||||
Keys []expandedKeyV1 |
||||
|
||||
// StateID's are nonce's, generated on enablement and fixed for
|
||||
// the lifetime of the Tailnet Key Authority.
|
||||
StateID1 uint64 |
||||
StateID2 uint64 |
||||
} |
||||
|
||||
// expandedSignatureV1 is the expanded form of a [tka.Signature], which
|
||||
// describes a signature over an AUM. This signature can be verified
|
||||
// using the key referenced by KeyID.
|
||||
type expandedSignatureV1 struct { |
||||
KeyID string |
||||
Signature string |
||||
} |
||||
@ -0,0 +1,204 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli |
||||
|
||||
import ( |
||||
"bytes" |
||||
"testing" |
||||
|
||||
"github.com/google/go-cmp/cmp" |
||||
"tailscale.com/cmd/tailscale/cli/jsonoutput" |
||||
"tailscale.com/ipn/ipnstate" |
||||
"tailscale.com/tka" |
||||
"tailscale.com/types/tkatype" |
||||
) |
||||
|
||||
func TestNetworkLockLogOutput(t *testing.T) { |
||||
votes := uint(1) |
||||
aum1 := tka.AUM{ |
||||
MessageKind: tka.AUMAddKey, |
||||
Key: &tka.Key{ |
||||
Kind: tka.Key25519, |
||||
Votes: 1, |
||||
Public: []byte{2, 2}, |
||||
}, |
||||
} |
||||
h1 := aum1.Hash() |
||||
aum2 := tka.AUM{ |
||||
MessageKind: tka.AUMRemoveKey, |
||||
KeyID: []byte{3, 3}, |
||||
PrevAUMHash: h1[:], |
||||
Signatures: []tkatype.Signature{ |
||||
{ |
||||
KeyID: []byte{3, 4}, |
||||
Signature: []byte{4, 5}, |
||||
}, |
||||
}, |
||||
Meta: map[string]string{"en": "three", "de": "drei", "es": "tres"}, |
||||
} |
||||
h2 := aum2.Hash() |
||||
aum3 := tka.AUM{ |
||||
MessageKind: tka.AUMCheckpoint, |
||||
PrevAUMHash: h2[:], |
||||
State: &tka.State{ |
||||
Keys: []tka.Key{ |
||||
{ |
||||
Kind: tka.Key25519, |
||||
Votes: 1, |
||||
Public: []byte{1, 1}, |
||||
Meta: map[string]string{"en": "one", "de": "eins", "es": "uno"}, |
||||
}, |
||||
}, |
||||
DisablementSecrets: [][]byte{ |
||||
{1, 2, 3}, |
||||
{4, 5, 6}, |
||||
{7, 8, 9}, |
||||
}, |
||||
}, |
||||
Votes: &votes, |
||||
} |
||||
|
||||
updates := []ipnstate.NetworkLockUpdate{ |
||||
{ |
||||
Hash: aum3.Hash(), |
||||
Change: aum3.MessageKind.String(), |
||||
Raw: aum3.Serialize(), |
||||
}, |
||||
{ |
||||
Hash: aum2.Hash(), |
||||
Change: aum2.MessageKind.String(), |
||||
Raw: aum2.Serialize(), |
||||
}, |
||||
{ |
||||
Hash: aum1.Hash(), |
||||
Change: aum1.MessageKind.String(), |
||||
Raw: aum1.Serialize(), |
||||
}, |
||||
} |
||||
|
||||
t.Run("human-readable", func(t *testing.T) { |
||||
t.Parallel() |
||||
|
||||
var outBuf bytes.Buffer |
||||
json := jsonoutput.JSONSchemaVersion{} |
||||
useColor := false |
||||
|
||||
printNetworkLockLog(updates, &outBuf, json, useColor) |
||||
|
||||
t.Logf("%s", outBuf.String()) |
||||
|
||||
want := `update 4M4Q3IXBARPQMFVXHJBDCYQMWU5H5FBKD7MFF75HE4O5JMIWR2UA (checkpoint) |
||||
Disablement values: |
||||
- 010203 |
||||
- 040506 |
||||
- 070809 |
||||
Keys: |
||||
Type: 25519 |
||||
KeyID: tlpub:0101 |
||||
Metadata: map[de:eins en:one es:uno] |
||||
|
||||
update BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ (remove-key) |
||||
KeyID: tlpub:0303 |
||||
|
||||
update UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA (add-key) |
||||
Type: 25519 |
||||
KeyID: tlpub:0202 |
||||
|
||||
` |
||||
|
||||
if diff := cmp.Diff(outBuf.String(), want); diff != "" { |
||||
t.Fatalf("wrong output (-got, +want):\n%s", diff) |
||||
} |
||||
}) |
||||
|
||||
jsonV1 := `{ |
||||
"SchemaVersion": "1", |
||||
"Messages": [ |
||||
{ |
||||
"Hash": "4M4Q3IXBARPQMFVXHJBDCYQMWU5H5FBKD7MFF75HE4O5JMIWR2UA", |
||||
"AUM": { |
||||
"MessageKind": "checkpoint", |
||||
"PrevAUMHash": "BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ", |
||||
"State": { |
||||
"DisablementSecrets": [ |
||||
"010203", |
||||
"040506", |
||||
"070809" |
||||
], |
||||
"Keys": [ |
||||
{ |
||||
"Kind": "25519", |
||||
"Votes": 1, |
||||
"Public": "tlpub:0101", |
||||
"Meta": { |
||||
"de": "eins", |
||||
"en": "one", |
||||
"es": "uno" |
||||
} |
||||
} |
||||
], |
||||
"StateID1": 0, |
||||
"StateID2": 0 |
||||
}, |
||||
"Votes": 1 |
||||
}, |
||||
"Raw": "pAEFAlggCqtbndUNv4_i-JrrVbGywbw5dNWNZYysEm02CCgf3q8FowH2AoNDAQIDQwQFBkMHCAkDgaQBAQIBA0IBAQyjYmRlZGVpbnNiZW5jb25lYmVzY3VubwYB" |
||||
}, |
||||
{ |
||||
"Hash": "BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ", |
||||
"AUM": { |
||||
"MessageKind": "remove-key", |
||||
"PrevAUMHash": "UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA", |
||||
"KeyID": "tlpub:0303", |
||||
"Meta": { |
||||
"de": "drei", |
||||
"en": "three", |
||||
"es": "tres" |
||||
}, |
||||
"Signatures": [ |
||||
{ |
||||
"KeyID": "tlpub:0304", |
||||
"Signature": "BAU=" |
||||
} |
||||
] |
||||
}, |
||||
"Raw": "pQECAlggopKFFOhcPaARv2QQU90-kWozQFAG3Hqja7Vez-_EZIAEQgMDB6NiZGVkZHJlaWJlbmV0aHJlZWJlc2R0cmVzF4GiAUIDBAJCBAU=" |
||||
}, |
||||
{ |
||||
"Hash": "UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA", |
||||
"AUM": { |
||||
"MessageKind": "add-key", |
||||
"Key": { |
||||
"Kind": "25519", |
||||
"Votes": 1, |
||||
"Public": "tlpub:0202" |
||||
} |
||||
}, |
||||
"Raw": "owEBAvYDowEBAgEDQgIC" |
||||
} |
||||
] |
||||
} |
||||
` |
||||
|
||||
t.Run("json-1", func(t *testing.T) { |
||||
t.Parallel() |
||||
t.Logf("BOOM") |
||||
|
||||
var outBuf bytes.Buffer |
||||
json := jsonoutput.JSONSchemaVersion{ |
||||
IsSet: true, |
||||
Value: 1, |
||||
} |
||||
useColor := false |
||||
|
||||
printNetworkLockLog(updates, &outBuf, json, useColor) |
||||
|
||||
want := jsonV1 |
||||
t.Logf("%s", outBuf.String()) |
||||
|
||||
if diff := cmp.Diff(outBuf.String(), want); diff != "" { |
||||
t.Fatalf("wrong output (-got, +want):\n%s", diff) |
||||
} |
||||
}) |
||||
} |
||||
Loading…
Reference in new issue