-
Notifications
You must be signed in to change notification settings - Fork 38
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
enable saving config between steps in a single feature #103
enable saving config between steps in a single feature #103
Conversation
pkg/feature/feature.go
Outdated
@@ -52,6 +55,22 @@ func (s *Step) TestName() string { | |||
} | |||
} | |||
|
|||
// Save a value for use in a later feature step, or error on issue. | |||
func (f *Feature) Save(ctx context.Context, t *testing.T, key string, value interface{}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious what the reasoning is for Save/Load vs. Set/Get? I found it a bit odd that we translate Save/Load into Get/Set underneath.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess my thinking is that to me Save/Load semantically means save everything or load everything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to make a helper method for getting and setting the keys tied up with failing the test, so maybe you have a better name for the method? Maybe I also call it Get and Set, note it is on the top level object, not the Store object.
import "context" | ||
|
||
// This is a subset of knative.dev/pkg/kvstore.Interface | ||
type Store interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of defining a separate interface, would it be bad if we did not define this new interface, but instead in the InMemory implementation, we return NonImplemented error instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was attempting to allow to use one of those impls, but not vendor it in directly because the Load and Init and Save did not make sense for Features
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the approach overrall, but i don't like that i need to use feature
to Load
and Store
. This approach with feature is not really modular, because you can't provide a stepfn that wants to push things inside the store without without knowing in advance the feature, breaking the encapsulation between feature and stepfn (for example, think about one of the recorder features pushing useful data inside the kv store when installing for later usage)
What I had in mind was something using the context, where the kv store lives in the context and this context is passed through the various phases of the execution:
f.Setup("install sender", func(ctx context.Context, t *testing.T) {
t.Helper()
var to string
var event cloudevents.Event
storepkg.Load(ctx, t, "to", &to)
storepkg.Load(ctx, t, "event", &event)
from := feature.MakeRandomK8sName("sender")
eventshub.Install(from, eventshub.StartSender(to), eventshub.InputEvent(event))
})
(storepkg
is the module where kvstore impl lives)
Also you should provide a Get
method that returns interface{}
too, so user can choose between the two "styles"
having the store be context based will not work because of how passing and threading work, it would have to be injected into the context higher up and then used as a pointer to another kvstore, which we can do as a followup that is feature based. So I say we start with this when I fix it up with the comments and we can look at fancy hooks to access the kvstore later? or maybe ... meh YOLO, brb. |
126e09c
to
2a10dfe
Compare
ok please take another look, I took the request from @vaikas and @slinkydeveloper and made the feature integration look like this: setup,
using,
but nothing is stopping you from loading the KVStore directly:
|
/hold |
if !ok { | ||
return fmt.Errorf("key %s does not exist", key) | ||
} | ||
err := json.Unmarshal([]byte(v), value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah wait i didn't saw it, why storing json? Can't we store just void pointers to things and then cast them when retrieving them? The state to store doesn't necessarily define conversion back and forth to json, and for some reason the user might even not be able to define such conversion (e.g. when you're using a struct from an external library as state)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I avoided this way to prevent mutation of the thing that is stored in the kvstore. Set, Get, mutate get'ed object, Get updated object?? is unexpected to me. cheap and cheerful way is json.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point, but maybe what I want to store inside the kvstore is exactly a mutable object, like a list.
In any case, my problem with json is that you might end up storing objects which are not very json compatible but where marshalling doesn't fail, and you will lose some information, and together with it debugging time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the impl can evolve over time, this is more about the interface and the current problemset is really "how do I pass that url to the next phase"
if you start working out ways of passing callbacks I will burn the building down.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
net.Url
is a too simplistic example (and even with it, sometimes encoding and decoding it back and forth to json has weird behaviours). Any more complex state where json encoding/decoding is not defined won't work with this implementation.
if you start working out ways of passing callbacks I will burn the building down.
That's state of the test too 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is a kv store, if you can't save it to a config map, it will not work. That is the bar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But why are we using the kv store model for something which is always in memory by design? It's an in memory map, it's the state of the test, it's not something that has to be read/write to a config map.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it might let us do some really interesting things later, like retries or test startup in the middle, or capture test state.
Ready. /unhold |
func ContextWith(ctx context.Context, s Store) context.Context { | ||
return context.WithValue(ctx, envKey{}, s) | ||
} | ||
|
||
// FromContext returns the Store from Context, if not found FromContext will | ||
// panic. | ||
// TODO: revisit if we really want to panic here... likely not. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO panic is absolutely fine here, the Store
must always exists in the context passed to StepFn
. If it's not there, it's a reconciler-test bug
if !ok { | ||
return fmt.Errorf("key %s does not exist", key) | ||
} | ||
err := json.Unmarshal([]byte(v), value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point, but maybe what I want to store inside the kvstore is exactly a mutable object, like a list.
In any case, my problem with json is that you might end up storing objects which are not very json compatible but where marshalling doesn't fail, and you will lose some information, and together with it debugging time.
} | ||
|
||
// Set marshals and sets the value given under specified key. | ||
func (cs *KVStore) Set(_ context.Context, key string, value interface{}) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we expect users to check the error
everytime and fail the StepFn
if the Set
/Get
returns an error? Isn't that too much and makes the usage unnecessary complicated? Can't we just panic in case of an error? These Get
/Set
errors are not recoverable anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am matching a subset of this interface: https://github.com/knative/pkg/blob/master/kvstore/kvstore.go#L23
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's an implementation, i guess you can always have the GetOrFail
and SetOrFail
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't do that because KVStore is not used, it is Store, and Store is the pkg kvstore interface.
I have added methods like this to the context.
|
||
import "context" | ||
|
||
// This is a subset of knative.dev/pkg/kvstore.Interface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: @slinkydeveloper ^^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason why we're trying to match that particular interface about config maps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So that we can swap out this impl for the same that is used in pkg.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are trying to make synergy here, not reboil the world. I made the interface a subset because some of those things did not make sense in the context of features, but I want to use that impl in pkg at some point later.
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: vaikas The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
Changes
Looking for feedback on a method to pass data between steps in a feature.
Note that the interface introduced here is aligned with the implementation in pkg: https://github.com/knative/pkg/tree/master/kvstore
/kind enhancement
Relates to #66