Extend kv metadata to get, put, and patch (#12907)

* go get vault-plugin-secrets-kv@extend-kv-metadata-to-get-and-put

* test for custom_metadata in kv get, put, patch command output

* remove flagFormat-specific check from TestKVMetadataGetCommand

* rewrite custom metadata changelog entry

* go get vault-plugin-secrets-kv@master

* go mod tidy
This commit is contained in:
Chris Capurso 2021-10-26 15:38:56 -04:00 committed by GitHub
parent b9b7f5a9a3
commit a6b1cbad12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 135 additions and 71 deletions

View File

@ -1,3 +0,0 @@
```release-note:feature
secrets/kv: Add ability to specify version-agnostic custom key metadata
```

5
changelog/12907.txt Normal file
View File

@ -0,0 +1,5 @@
```release-note:feature
**KV Custom Metadata**: Add ability in kv-v2 to specify version-agnostic custom key metadata via the
metadata endpoint. The data will be present in responses made to the data endpoint independent of the
calling token's `read` access to the metadata endpoint.
```

View File

@ -84,52 +84,54 @@ func kvPatchWithRetry(t *testing.T, client *api.Client, args []string, stdin *io
func TestKVPutCommand(t *testing.T) { func TestKVPutCommand(t *testing.T) {
t.Parallel() t.Parallel()
v2ExpectedFields := []string{"created_time", "custom_metadata", "deletion_time", "deletion_time", "version"}
cases := []struct { cases := []struct {
name string name string
args []string args []string
out string outStrings []string
code int code int
}{ }{
{ {
"not_enough_args", "not_enough_args",
[]string{}, []string{},
"Not enough arguments", []string{"Not enough arguments"},
1, 1,
}, },
{ {
"empty_kvs", "empty_kvs",
[]string{"secret/write/foo"}, []string{"secret/write/foo"},
"Must supply data", []string{"Must supply data"},
1, 1,
}, },
{ {
"kvs_no_value", "kvs_no_value",
[]string{"secret/write/foo", "foo"}, []string{"secret/write/foo", "foo"},
"Failed to parse K=V data", []string{"Failed to parse K=V data"},
1, 1,
}, },
{ {
"single_value", "single_value",
[]string{"secret/write/foo", "foo=bar"}, []string{"secret/write/foo", "foo=bar"},
"Success!", []string{"Success!"},
0, 0,
}, },
{ {
"multi_value", "multi_value",
[]string{"secret/write/foo", "foo=bar", "zip=zap"}, []string{"secret/write/foo", "foo=bar", "zip=zap"},
"Success!", []string{"Success!"},
0, 0,
}, },
{ {
"v2_single_value", "v2_single_value",
[]string{"kv/write/foo", "foo=bar"}, []string{"kv/write/foo", "foo=bar"},
"created_time", v2ExpectedFields,
0, 0,
}, },
{ {
"v2_multi_value", "v2_multi_value",
[]string{"kv/write/foo", "foo=bar", "zip=zap"}, []string{"kv/write/foo", "foo=bar", "zip=zap"},
"created_time", v2ExpectedFields,
0, 0,
}, },
} }
@ -153,8 +155,11 @@ func TestKVPutCommand(t *testing.T) {
if code != tc.code { if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code) t.Errorf("expected %d to be %d", code, tc.code)
} }
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out) for _, str := range tc.outStrings {
if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
}
} }
}) })
} }
@ -178,8 +183,11 @@ func TestKVPutCommand(t *testing.T) {
if code != 0 { if code != 0 {
t.Fatalf("expected 0 to be %d", code) t.Fatalf("expected 0 to be %d", code)
} }
if !strings.Contains(combined, "created_time") {
t.Errorf("expected %q to contain %q", combined, "created_time") for _, str := range v2ExpectedFields {
if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
}
} }
ui, cmd := testKVPutCommand(t) ui, cmd := testKVPutCommand(t)
@ -191,8 +199,11 @@ func TestKVPutCommand(t *testing.T) {
t.Fatalf("expected 0 to be %d", code) t.Fatalf("expected 0 to be %d", code)
} }
combined = ui.OutputWriter.String() + ui.ErrorWriter.String() combined = ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, "created_time") {
t.Errorf("expected %q to contain %q", combined, "created_time") for _, str := range v2ExpectedFields {
if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
}
} }
ui, cmd = testKVPutCommand(t) ui, cmd = testKVPutCommand(t)
@ -366,72 +377,68 @@ func testKVGetCommand(tb testing.TB) (*cli.MockUi, *KVGetCommand) {
func TestKVGetCommand(t *testing.T) { func TestKVGetCommand(t *testing.T) {
t.Parallel() t.Parallel()
baseV2ExpectedFields := []string{"created_time", "custom_metadata", "deletion_time", "deletion_time", "version"}
cases := []struct { cases := []struct {
name string name string
args []string args []string
out string outStrings []string
code int code int
}{ }{
{ {
"not_enough_args", "not_enough_args",
[]string{}, []string{},
"Not enough arguments", []string{"Not enough arguments"},
1, 1,
}, },
{ {
"too_many_args", "too_many_args",
[]string{"foo", "bar"}, []string{"foo", "bar"},
"Too many arguments", []string{"Too many arguments"},
1, 1,
}, },
{ {
"not_found", "not_found",
[]string{"secret/nope/not/once/never"}, []string{"secret/nope/not/once/never"},
"", []string{"No value found at secret/nope/not/once/never"},
2, 2,
}, },
{ {
"default", "default",
[]string{"secret/read/foo"}, []string{"secret/read/foo"},
"foo", []string{"foo"},
0, 0,
}, },
{ {
"v1_field", "v1_field",
[]string{"-field", "foo", "secret/read/foo"}, []string{"-field", "foo", "secret/read/foo"},
"bar", []string{"bar"},
0, 0,
}, },
{ {
"v2_field", "v2_field",
[]string{"-field", "foo", "kv/read/foo"}, []string{"-field", "foo", "kv/read/foo"},
"bar", []string{"bar"},
0, 0,
}, },
{ {
"v2_not_found", "v2_not_found",
[]string{"kv/nope/not/once/never"}, []string{"kv/nope/not/once/never"},
"", []string{"No value found at kv/data/nope/not/once/never"},
2, 2,
}, },
{ {
"v2_read", "v2_read",
[]string{"kv/read/foo"}, []string{"kv/read/foo"},
"foo", append(baseV2ExpectedFields, "foo"),
0,
},
{
"v2_read",
[]string{"kv/read/foo"},
"version",
0, 0,
}, },
{ {
"v2_read_version", "v2_read_version",
[]string{"--version", "1", "kv/read/foo"}, []string{"--version", "1", "kv/read/foo"},
"foo", append(baseV2ExpectedFields, "foo"),
0, 0,
}, },
} }
@ -479,8 +486,11 @@ func TestKVGetCommand(t *testing.T) {
} }
combined := ui.OutputWriter.String() + ui.ErrorWriter.String() combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out) for _, str := range tc.outStrings {
if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
}
} }
}) })
} }
@ -508,28 +518,46 @@ func testKVMetadataGetCommand(tb testing.TB) (*cli.MockUi, *KVMetadataGetCommand
func TestKVMetadataGetCommand(t *testing.T) { func TestKVMetadataGetCommand(t *testing.T) {
t.Parallel() t.Parallel()
expectedTopLevelFields := []string{
"cas_required",
"created_time",
"current_version",
"custom_metadata",
"delete_version_after",
"max_versions",
"oldest_version",
"updated_time",
}
expectedVersionFields := []string{
"created_time", // field is redundant
"deletion_time",
"destroyed",
}
cases := []struct { cases := []struct {
name string name string
args []string args []string
out string outStrings []string
code int code int
}{ }{
{ {
"v1", "v1",
[]string{"secret/foo"}, []string{"secret/foo"},
"Metadata not supported on KV Version 1", []string{"Metadata not supported on KV Version 1"},
1, 1,
}, },
{ {
"metadata_exists", "metadata_exists",
[]string{"kv/foo"}, []string{"kv/foo"},
"current_version", expectedTopLevelFields,
0, 0,
}, },
// ensure that all top-level and version-level fields are output along with version num
{ {
"versions_exist", "versions_exist",
[]string{"kv/foo"}, []string{"kv/foo"},
"deletion_time", append(expectedTopLevelFields, expectedVersionFields[:]...),
0, 0,
}, },
} }
@ -571,8 +599,10 @@ func TestKVMetadataGetCommand(t *testing.T) {
} }
combined := ui.OutputWriter.String() + ui.ErrorWriter.String() combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) { for _, str := range tc.outStrings {
t.Errorf("expected %q to contain %q", combined, tc.out) if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
}
} }
}) })
} }
@ -652,6 +682,19 @@ func TestKVPatchCommand_ArgValidation(t *testing.T) {
} }
} }
// expectedPatchFields produces a deterministic slice of
// expected fields for patch command output since const
// slices are not supported
func expectedPatchFields() []string {
return []string{
"created_time",
"custom_metadata",
"deletion_time",
"destroyed",
"version",
}
}
func TestKvPatchCommand_StdinFull(t *testing.T) { func TestKvPatchCommand_StdinFull(t *testing.T) {
client, closer := testVaultServer(t) client, closer := testVaultServer(t)
defer closer() defer closer()
@ -677,7 +720,14 @@ func TestKvPatchCommand_StdinFull(t *testing.T) {
}() }()
args := []string{"kv/patch/foo", "-"} args := []string{"kv/patch/foo", "-"}
code, _ := kvPatchWithRetry(t, client, args, stdinR) code, combined := kvPatchWithRetry(t, client, args, stdinR)
for _, str := range expectedPatchFields() {
if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
}
}
if code != 0 { if code != 0 {
t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args) t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args)
} }
@ -733,11 +783,17 @@ func TestKvPatchCommand_StdinValue(t *testing.T) {
}() }()
args := []string{"kv/patch/foo", "foo=-"} args := []string{"kv/patch/foo", "foo=-"}
code, _ := kvPatchWithRetry(t, client, args, stdinR) code, combined := kvPatchWithRetry(t, client, args, stdinR)
if code != 0 { if code != 0 {
t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args) t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args)
} }
for _, str := range expectedPatchFields() {
if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
}
}
secret, err := client.Logical().Read("kv/data/patch/foo") secret, err := client.Logical().Read("kv/data/patch/foo")
if err != nil { if err != nil {
t.Fatalf("read failed, err: %#v\n", err) t.Fatalf("read failed, err: %#v\n", err)
@ -810,9 +866,10 @@ func TestKVPatchCommand_RWMethodSucceeds(t *testing.T) {
t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args) t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args)
} }
expectedOutputSubstr := "created_time" for _, str := range expectedPatchFields() {
if !strings.Contains(combined, expectedOutputSubstr) { if !strings.Contains(combined, str) {
t.Fatalf("expected output %q to contain %q for patch cmd with args %#v\n", combined, expectedOutputSubstr, args) t.Errorf("expected %q to contain %q", combined, str)
}
} }
// Test multi value // Test multi value
@ -823,31 +880,33 @@ func TestKVPatchCommand_RWMethodSucceeds(t *testing.T) {
t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args) t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args)
} }
if !strings.Contains(combined, expectedOutputSubstr) { for _, str := range expectedPatchFields() {
t.Fatalf("expected output %q to contain %q for patch cmd with args %#v\n", combined, expectedOutputSubstr, args) if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
}
} }
} }
func TestKVPatchCommand_CAS(t *testing.T) { func TestKVPatchCommand_CAS(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
args []string args []string
expected string expected string
out string outStrings []string
code int code int
}{ }{
{ {
"right version", "right version",
[]string{"-cas", "1", "kv/foo", "bar=quux"}, []string{"-cas", "1", "kv/foo", "bar=quux"},
"quux", "quux",
"", expectedPatchFields(),
0, 0,
}, },
{ {
"wrong version", "wrong version",
[]string{"-cas", "2", "kv/foo", "bar=wibble"}, []string{"-cas", "2", "kv/foo", "bar=wibble"},
"baz", "baz",
"check-and-set parameter did not match the current version", []string{"check-and-set parameter did not match the current version"},
2, 2,
}, },
} }
@ -892,9 +951,9 @@ func TestKVPatchCommand_CAS(t *testing.T) {
t.Fatalf("expected code to be %d but was %d", tc.code, code) t.Fatalf("expected code to be %d but was %d", tc.code, code)
} }
if tc.out != "" { for _, str := range tc.outStrings {
if !strings.Contains(combined, tc.out) { if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, tc.out) t.Errorf("expected %q to contain %q", combined, str)
} }
} }

4
go.mod
View File

@ -104,13 +104,13 @@ require (
github.com/hashicorp/vault-plugin-secrets-azure v0.6.3-0.20210924190759-58a034528e35 github.com/hashicorp/vault-plugin-secrets-azure v0.6.3-0.20210924190759-58a034528e35
github.com/hashicorp/vault-plugin-secrets-gcp v0.10.2 github.com/hashicorp/vault-plugin-secrets-gcp v0.10.2
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.9.0 github.com/hashicorp/vault-plugin-secrets-gcpkms v0.9.0
github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211013154503-eec8a1c892fb github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211026132900-bc1c42ddb53c
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.4.0 github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.4.0
github.com/hashicorp/vault-plugin-secrets-openldap v0.4.1-0.20210921171411-e86105e4986d github.com/hashicorp/vault-plugin-secrets-openldap v0.4.1-0.20210921171411-e86105e4986d
github.com/hashicorp/vault-plugin-secrets-terraform v0.3.0 github.com/hashicorp/vault-plugin-secrets-terraform v0.3.0
github.com/hashicorp/vault-testing-stepwise v0.1.1 github.com/hashicorp/vault-testing-stepwise v0.1.1
github.com/hashicorp/vault/api v1.2.0 github.com/hashicorp/vault/api v1.2.0
github.com/hashicorp/vault/sdk v0.2.2-0.20211004171540-a8c7e135dd6a github.com/hashicorp/vault/sdk v0.2.2-0.20211014165207-28bd5c3a0311
github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4 github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4
github.com/jcmturner/gokrb5/v8 v8.0.0 github.com/jcmturner/gokrb5/v8 v8.0.0
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f

7
go.sum
View File

@ -401,6 +401,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-ldap/ldap v3.0.2+incompatible h1:kD5HQcAzlQ7yrhfn+h+MSABeAy/jAJhvIJ/QDllP44g=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8= github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
github.com/go-ldap/ldap/v3 v3.1.7/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-ldap/ldap/v3 v3.1.7/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
@ -776,8 +778,8 @@ github.com/hashicorp/vault-plugin-secrets-gcp v0.10.2 h1:+DtlYJTsrFRInQpAo09KkYN
github.com/hashicorp/vault-plugin-secrets-gcp v0.10.2/go.mod h1:psRQ/dm5XatoUKLDUeWrpP9icMJNtu/jmscUr37YGK4= github.com/hashicorp/vault-plugin-secrets-gcp v0.10.2/go.mod h1:psRQ/dm5XatoUKLDUeWrpP9icMJNtu/jmscUr37YGK4=
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.9.0 h1:7a0iWuFA/YNinQ1xXogyZHStolxMVtLV+sy1LpEHaZs= github.com/hashicorp/vault-plugin-secrets-gcpkms v0.9.0 h1:7a0iWuFA/YNinQ1xXogyZHStolxMVtLV+sy1LpEHaZs=
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.9.0/go.mod h1:hhwps56f2ATeC4Smgghrc5JH9dXR31b4ehSf1HblP5Q= github.com/hashicorp/vault-plugin-secrets-gcpkms v0.9.0/go.mod h1:hhwps56f2ATeC4Smgghrc5JH9dXR31b4ehSf1HblP5Q=
github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211013154503-eec8a1c892fb h1:nZ2a4a1G0ALLAzKOWQbLzD5oljKo+pjMarbq3BwU0pM= github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211026132900-bc1c42ddb53c h1:m6aJO2SrAf8bCLjyAtQJNiSuV0nM4TBKqrJpImrDtSY=
github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211013154503-eec8a1c892fb/go.mod h1:D/FQJ7zU5pD6FNJVUwaVtxr75ZsxIIqaG/Nh6RHt/xo= github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211026132900-bc1c42ddb53c/go.mod h1:Luu1GqDOMnuJ2iqn6mFf38Dz8DQ8mgtyQRXrS7Bp8Xc=
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.4.0 h1:6ve+7hZmGn7OpML81iZUxYj2AaJptwys323S5XsvVas= github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.4.0 h1:6ve+7hZmGn7OpML81iZUxYj2AaJptwys323S5XsvVas=
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.4.0/go.mod h1:4mdgPqlkO+vfFX1cFAWcxkeqz6JAtZgKxL/67q/58Oo= github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.4.0/go.mod h1:4mdgPqlkO+vfFX1cFAWcxkeqz6JAtZgKxL/67q/58Oo=
github.com/hashicorp/vault-plugin-secrets-openldap v0.4.1-0.20210921171411-e86105e4986d h1:o5Z9B1FztTYSnTQNzFr+iZJHPM8ZD23uV5A8gMxm2g0= github.com/hashicorp/vault-plugin-secrets-openldap v0.4.1-0.20210921171411-e86105e4986d h1:o5Z9B1FztTYSnTQNzFr+iZJHPM8ZD23uV5A8gMxm2g0=
@ -1578,6 +1580,7 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=