-
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
Changes from all commits
e632ecd
90b8bb3
488e224
34b6c82
55d330f
432aab4
469acac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/* | ||
Copyright 2021 The Knative Authors | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package state | ||
|
||
import ( | ||
"context" | ||
) | ||
|
||
type envKey struct{} | ||
|
||
// ContextWith decorates the given context with the provided Store, and returns | ||
// the resulting context. | ||
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. | ||
func FromContext(ctx context.Context) Store { | ||
slinkydeveloper marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if e, ok := ctx.Value(envKey{}).(Store); ok { | ||
return e | ||
} | ||
panic("no Store found in context") | ||
} | ||
|
||
// Fail is defined to avoid circular dependency with the feature package. | ||
type fail interface { | ||
Error(args ...interface{}) | ||
} | ||
|
||
// Get the string value from the kvstore from key. | ||
func GetStringOrFail(ctx context.Context, t fail, key string) string { | ||
value := "" | ||
state := FromContext(ctx) | ||
if err := state.Get(ctx, key, &value); err != nil { | ||
t.Error(err) | ||
} | ||
return value | ||
} | ||
|
||
// Get gets the key from the Store into the provided value | ||
func GetOrFail(ctx context.Context, t fail, key string, value interface{}) { | ||
state := FromContext(ctx) | ||
if err := state.Get(ctx, key, value); err != nil { | ||
t.Error(err) | ||
} | ||
} | ||
|
||
// Set sets the key into the Store from the provided value | ||
func SetOrFail(ctx context.Context, t fail, key string, value interface{}) { | ||
state := FromContext(ctx) | ||
if err := state.Set(ctx, key, value); err != nil { | ||
t.Error(err) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
Copyright 2021 The Knative Authors | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package state | ||
|
||
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 commentThe 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 commentThe 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 commentThe 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 commentThe 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. |
||
type Store interface { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 |
||
// Get gets the key from the Store into the provided value | ||
Get(ctx context.Context, key string, value interface{}) error | ||
// Set sets the key into the Store from the provided value | ||
Set(ctx context.Context, key string, value interface{}) error | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
Copyright 2021 The Knative Authors | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package state | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"sync" | ||
) | ||
|
||
type KVStore struct { | ||
store map[string]string | ||
mux sync.Mutex | ||
} | ||
|
||
// Get retrieves and unmarshals the value from the map. | ||
func (cs *KVStore) Get(_ context.Context, key string, value interface{}) error { | ||
cs.mux.Lock() | ||
defer cs.mux.Unlock() | ||
|
||
if cs.store == nil { | ||
cs.store = make(map[string]string) | ||
} | ||
|
||
v, ok := cs.store[key] | ||
|
||
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 commentThe 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 commentThe 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 commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more.
That's state of the test too 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe 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. |
||
if err != nil { | ||
return fmt.Errorf("failed to Unmarshal %q: %v", v, err) | ||
} | ||
return nil | ||
} | ||
|
||
// 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 commentThe reason will be displayed to describe this comment to others. Learn more. Do we expect users to check the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Since it's an implementation, i guess you can always have the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
cs.mux.Lock() | ||
defer cs.mux.Unlock() | ||
|
||
if cs.store == nil { | ||
cs.store = make(map[string]string) | ||
} | ||
|
||
bytes, err := json.Marshal(value) | ||
if err != nil { | ||
return fmt.Errorf("failed to Marshal: %v", err) | ||
} | ||
cs.store[key] = string(bytes) | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
Copyright 2021 The Knative Authors | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package state | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
) | ||
|
||
func TestKVStore_Get(t *testing.T) { | ||
type test1 struct { | ||
Foo string | ||
Bar string | ||
} | ||
|
||
tests := []struct { | ||
name string | ||
store map[string]string | ||
key string | ||
target interface{} | ||
want interface{} | ||
wantErr bool | ||
}{{ | ||
name: "nil store", | ||
store: nil, | ||
key: "foo", | ||
target: func() interface{} { | ||
return "" | ||
}(), | ||
want: "", | ||
wantErr: true, | ||
}, { | ||
name: "found", | ||
store: map[string]string{ | ||
"foo": `"bar"`, | ||
}, | ||
key: "foo", | ||
target: func() interface{} { | ||
return "" | ||
}(), | ||
want: "bar", | ||
wantErr: false, | ||
}, { | ||
name: "found struct", | ||
store: map[string]string{ | ||
"key": `{"Foo": "hello","Bar": "world"}`, | ||
}, | ||
key: "key", | ||
target: func() interface{} { | ||
return test1{} | ||
}(), | ||
want: test1{ | ||
Foo: "hello", | ||
Bar: "world", | ||
}, | ||
wantErr: false, | ||
}} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
s := KVStore{ | ||
store: tt.store, | ||
} | ||
err := s.Get(context.Background(), tt.key, &tt.target) | ||
if (err != nil) != tt.wantErr { | ||
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} | ||
if err != nil { | ||
if diff := cmp.Diff(tt.want, tt.target); diff != "" { | ||
t.Error("Unexpected difference on return value (-want, +got):", diff) | ||
} | ||
} | ||
}) | ||
} | ||
} |
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 toStepFn
. If it's not there, it's a reconciler-test bug