Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KV helper methods for api package #15305

Merged
merged 22 commits into from
May 25, 2022
Merged

KV helper methods for api package #15305

merged 22 commits into from
May 25, 2022

Conversation

digivava
Copy link
Contributor

@digivava digivava commented May 5, 2022

Purpose

The Developer Experience team has observed many users struggling to figure out how to perform simple KV secret reads and writes in their application code. Not only are the Logical() methods hard to discover because of their naming, they also require users to know about the internal naming discrepancy between KV v1 and KV v2 (secret/foo vs secret/data/foo).

Adding to this difficulty is the fact that in the case of KV v2, an additional layer called "data" must be parsed from the response, resulting in code that looks like secret.Data["data"], which many users have had trouble discovering.

These helper methods seek to eliminate those difficulties by providing an intuitive method that handles both the /data in the URL and the "data" object unwrapping for the user.

Usage

client.KVv1("secret").Read(ctx, "foo") and client.KVv2("secret").Read(ctx, "foo"). No need for /data, hooray!
Both will return a *KVSecret, the KVv2 one will take care of the "data" unwrapping for you.

Design Notes

Thanks @averche for the initial POC on this! One of the ideas that has stuck around from that POC is how the original *api.Secret is returned as part of the KVSecret struct, so that users can easily pass it on to LifetimeWatcher.

There was some discussion over whether to provide a separate method for each KV version (e.g. client.KVv2("secret")) , or to specify the KV secrets engine version as a parameter (e.g. client.KV("secret", 2) or the enum version, client.KV("secret", api.KVv2Version)).

After considering the various options, I decided to go with the client.KVv2("secret") approach, mostly because it's simple. Having to specify an int every time looks awkward, and the enum might also be hard to discover. We could instead specify a NewKVClient struct and initialize the mount path and version as part of that, but that wouldn't be very consistent with the client.ActionType().Action() style that we've been using in the rest of this package (client.Logical().Read, client.Sys().Renew, etc.) Some discussion was also had about automatically detecting the version, but this would require a hidden extra call to the sys/internal/ui/mounts endpoint every time the function is called, which doesn't feel like a great pattern.

Other methods

Other KV-related methods like Destroy, DeleteMetadata, Rollback, etc. can come in subsequent PRs as needed. This is meant to be more of a stop-gap helper for just the basic CRUD functionality until the next major iteration of our client library.

api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated
var metadata *KVMetadata

// deletion_time usually comes in as an empty string which can't be
// processed as time.RFC3339, so we reset it to a convertible value
Copy link
Contributor Author

@digivava digivava May 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anyone knows how to get the DecodeHook to shut up and accept the empty string as time.RFC3339, lemme know. I tried the ,omitempty and ,string struct tags and still no good. So I'm just kinda hacking around this manually.

@digivava digivava requested review from a team, ncabatoff and briankassouf May 5, 2022 18:17
Copy link
Contributor

@VinnyHC VinnyHC left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome; the UX is going to be much nicer! I left a couple comments around the design.

api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated
OldestVersion int `mapstructure:"oldest_version"`
UpdatedTime time.Time `mapstructure:"updated_time"`
// Keys are stringified ints, e.g. "3"
Versions map[string]VersionMetadata `mapstructure:"versions"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nicer UX to have this as either map[int]VersionMetadata or []VersionMetadata (sorted by version, since Version is already in the struct)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find a way to make mapstructure happy if I had it as an int (since it comes in as a string), maybe there's some decode hook I could use but couldn't find it... 🤔 I did add a GetVersionsAsList method though so that folks have access to a sorted slice as well.

api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
Copy link
Contributor

@averche averche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty awesome, definitely will be a big improvement to the user experience.

api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv_test.go Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
api/kv.go Show resolved Hide resolved
api/kv_v1.go Outdated Show resolved Hide resolved
api/kv_v2.go Outdated Show resolved Hide resolved
api/kv.go Outdated Show resolved Hide resolved
@digivava digivava requested a review from VinnyHC May 23, 2022 16:55
Copy link
Contributor

@VinnyHC VinnyHC left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fantastic addition, excellent work.

Copy link
Contributor

@AnPucel AnPucel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

Copy link
Contributor

@raskchanky raskchanky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me. I left a few minor comments, feel free to take or leave as you see fit.

api/kv_v2.go Outdated Show resolved Hide resolved
api/kv_v2.go Outdated Show resolved Hide resolved
api/kv_v2.go Outdated Show resolved Hide resolved
vault/external_tests/api/kv_helpers_test.go Outdated Show resolved Hide resolved
@digivava digivava merged commit 242a6f9 into main May 25, 2022
@digivava digivava deleted the VAULT-5973_api-kv-helpers branch May 25, 2022 18:17
Gabrielopesantos pushed a commit to Gabrielopesantos/vault that referenced this pull request Jun 6, 2022
* Add Read methods for KVClient

* KV write helper

* Add changelog

* Add Delete method

* Use extractVersionMetadata inside extractDataAndVersionMetadata

* Return nil, nil for v1 writes

* Add test for extracting version metadata

* Split kv client into v1 and v2-specific clients

* Add ability to set options on Put

* Add test for KV helpers

* Add custom metadata to top level and allow for getting versions as sorted slice

* Update tests

* Separate KV v1 and v2 into different files

* Add test for GetVersionsAsList, rename Metadata key to VersionMetadata for clarity

* Move structs and godoc comments to more appropriate files

* Add more tests for extract methods

* Rework custom metadata helper to be more consistent with other helpers

* Remove KVSecret from custom metadata test now that we don't append to it as part of helper method

* Return early for readability and make test value name less confusing
@universam1
Copy link

@digivava Great idea for this convenience functions - I'm struggling to find an equivalent for .List() methods - is this planned?

@digivava
Copy link
Contributor Author

digivava commented Nov 2, 2022

@universam1 Good question! At least at the time I implemented this, I chose not to do List just because it behaves pretty differently from the other KV commands and thus didn't really match the KVv2 method signature here.

With the other KV commands, when you say KVv2("secret"), "secret" is always the mount path. But for List, that parameter would sometimes refer to the mount path, sometimes the full secret path, sometimes an arbitrary "folder" within the path. It just felt a little inconsistent UX-wise to make KVv2() take any of those things.

That said, I'm sure its absence from these helpers sometimes trips people up! I'll let @hashicorp/vault-devex speak to whether they have plans for a List helper in the future, but in the meantime, you can of course always fall back on the old client.Logical().List(path) method. (This works for any API path that accepts a List call!)

@universam1
Copy link

Thanks for the answer @digivava
I found the client reference implementation in
https://github.com/hashicorp/vault/blob/main/command/kv_list.go#L86-L100

There the condition for v1 vs v2 is clearly handled. I'm not really understanding what you mean how List path is different than a Put/Get and so on, it is always the full path from root, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants