-
Notifications
You must be signed in to change notification settings - Fork 103
Conversation
dynamic/informer/informer.go
Outdated
type dynamicResourceClientListFunc func(metav1.ListOptions) (*unstructured.UnstructuredList, error) | ||
|
||
func castToCacheListFunc(fn dynamicResourceClientListFunc) cache.ListFunc { | ||
options := metav1.ListOptions{} |
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.
Is an empty ListOptions
acceptable?
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.
In practice, I've only set the LabelSelector of a ListOption before. It might be good hygiene to set timeout but that will likely cause more problems on slow clusters that it would prevent.
So, I think an empty LabelSelector is alright.
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.
This is meant to just be an adapter function, so you should pass along the ListOptions from the caller. See my other comment below, which should make this clearer.
Gopkg.toml
Outdated
|
||
[[override]] | ||
name = "github.com/json-iterator/go" | ||
version = "1.1.4" # as used by apimachinery@kubernetes-1.9.9 | ||
version = "1.1.5" # as used by apimachinery@kubernetes-1.11.0 | ||
|
||
[prune] |
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 take it this is working for you? I only say that because I was running into all sorts of grouble getting code generation to work. See #79. I was able to get it to work adding the following to my GoPkg.toml
:
[prune]
go-tests = true
unused-packages = true
[[prune.project]]
name = "k8s.io/code-generator"
unused-packages = false
I was getting errors that the generator couldn't find the boilerplate.go.txt
that comes with the code-generator repository.
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.
Me too - There is no boilerplate.go.txt
within the vendor code-generator repository, so I added it manually (which is wrong) to unblock myself. I forgot to point this out, thanks for the reminder!
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.
Let me sync with @enisoc, and get back to you later. I believe your way of getting around this is better.
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.
@crimsonfaith91 I agree with the solution @rlguarino suggested. Can you try adding that?
@rlguarino Thanks for tracking that 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.
@enisoc Sure! I will include the [prune.project]
section.
dynamic/informer/informer.go
Outdated
type dynamicResourceClientListFunc func(metav1.ListOptions) (*unstructured.UnstructuredList, error) | ||
|
||
func castToCacheListFunc(fn dynamicResourceClientListFunc) cache.ListFunc { | ||
options := metav1.ListOptions{} |
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.
In practice, I've only set the LabelSelector of a ListOption before. It might be good hygiene to set timeout but that will likely cause more problems on slow clusters that it would prevent.
So, I think an empty LabelSelector is alright.
controller/composite/controller.go
Outdated
@@ -471,7 +471,7 @@ func (pc *parentController) makeSelector(parent *unstructured.Unstructured, extr | |||
} | |||
|
|||
func (pc *parentController) canAdoptFunc(parent *unstructured.Unstructured) func() error { | |||
nsParentClient := pc.parentClient.WithNamespace(parent.GetNamespace()) | |||
nsParentClient := pc.parentClient |
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 actually need to reassign this variable here? Could we just directly call ps.parentClient.Get(...)
below?
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.
Agreed.
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 agree too. In addition, make sure pc.parentClient
is a namespace-agnostic client since a single parentController
instance manages objects in multiple namespaces. In other words, the line below should ultimately read pc.parentClient.Namespace(parent.GetNamespace()).Get(parent.GetName(), ...)
.
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.
Noted, thanks for pointing this out. :)
controller/composite/controller.go
Outdated
@@ -536,7 +536,7 @@ func (pc *parentController) updateParentStatus(parent *unstructured.Unstructured | |||
// We can't use Patch() because we need to ensure that the UID matches. | |||
// TODO(enisoc): Use /status subresource when that exists. | |||
// TODO(enisoc): Update status.observedGeneration when spec.generation starts working. | |||
nsParentClient := pc.parentClient.WithNamespace(parent.GetNamespace()) | |||
nsParentClient := pc.parentClient |
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.
Simmarly to above about calling pc.parentClient.UpdateWIthRetries()...
directly instead of defining a new variable. We could use:
return pc.parentClient.UpdateWithRetries(...
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.
+1, you are right. :)
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.
Also like above, this may need to change to account for pc.parentClient
becoming namespace-agnostic. To make something like pc.parentClient.Namespace(...).UpdateWithRetries(...)
work, we'll need to restructure how we add extra methods to ResourceInterface in our dynamic/clientset
wrapper.
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.
Thanks for the detailed explanation. Did you mean ResourceClient
? NamespaceableResourceInterface
is a vendor API.
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 original purpose of ResourceClient was to extend ResourceInterface with extra methods that are useful for Metacontroller, which works with many different resources from many different angles.
Now that the new dynamic client has been split into ResourceInterface and NamespaceableResourceInterface, we need to restructure ResourceClient to correspond to NamespaceableResourceInterface, and add another level of wrapper to extend ResourceInterface as well.
Gopkg.toml
Outdated
@@ -30,19 +30,19 @@ required = [ | |||
|
|||
[[constraint]] | |||
name = "k8s.io/client-go" | |||
version = "~6.0" | |||
revision = "kubernetes-1.11.0" |
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 think we should stick to version
and use 8.0.0
(the current recommended tag from the client-go README). The client-go versions are meant to be somewhat independent of Kubernetes versions.
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.
Good point. I thought the requirement is to use the client-go version of 1.11 kubernetes version (misunderstood issue description).
Gopkg.toml
Outdated
|
||
[[override]] | ||
name = "github.com/json-iterator/go" | ||
version = "1.1.4" # as used by apimachinery@kubernetes-1.9.9 | ||
version = "1.1.5" # as used by apimachinery@kubernetes-1.11.0 |
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.
How did you determine that apimachinery uses 1.1.5 at the kubernetes-1.11.0 tag? From the Godeps file (below) it looks like it still uses a commit that's contained in 1.1.4:
That said, I'm fine updating to 1.1.5 if it works. We should just reword the comment to say # same minor version track as used by apimachinery@kubernetes-1.11.0
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.
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'm not sure what the bolding means, but I believe that list is all the tags that contain this commit in their histories. If we wanted to find the closest release tag at or after that commit, we should take the earliest tag that contains it. That's how I got 1.1.4 before.
However, as mentioned above, I think we should update to 1.1.5 as long as it's compatible, since it contains additional bug fixes.
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 - we should take the earliest (not latest) tag. Thank you!
I verify that 1.1.5 works.
Gopkg.toml
Outdated
|
||
[[override]] | ||
name = "github.com/json-iterator/go" | ||
version = "1.1.4" # as used by apimachinery@kubernetes-1.9.9 | ||
version = "1.1.5" # as used by apimachinery@kubernetes-1.11.0 | ||
|
||
[prune] |
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.
@crimsonfaith91 I agree with the solution @rlguarino suggested. Can you try adding that?
@rlguarino Thanks for tracking that down!
controller/composite/controller.go
Outdated
@@ -471,7 +471,7 @@ func (pc *parentController) makeSelector(parent *unstructured.Unstructured, extr | |||
} | |||
|
|||
func (pc *parentController) canAdoptFunc(parent *unstructured.Unstructured) func() error { | |||
nsParentClient := pc.parentClient.WithNamespace(parent.GetNamespace()) | |||
nsParentClient := pc.parentClient |
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 agree too. In addition, make sure pc.parentClient
is a namespace-agnostic client since a single parentController
instance manages objects in multiple namespaces. In other words, the line below should ultimately read pc.parentClient.Namespace(parent.GetNamespace()).Get(parent.GetName(), ...)
.
controller/composite/controller.go
Outdated
@@ -536,7 +536,7 @@ func (pc *parentController) updateParentStatus(parent *unstructured.Unstructured | |||
// We can't use Patch() because we need to ensure that the UID matches. | |||
// TODO(enisoc): Use /status subresource when that exists. | |||
// TODO(enisoc): Update status.observedGeneration when spec.generation starts working. | |||
nsParentClient := pc.parentClient.WithNamespace(parent.GetNamespace()) | |||
nsParentClient := pc.parentClient |
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.
Also like above, this may need to change to account for pc.parentClient
becoming namespace-agnostic. To make something like pc.parentClient.Namespace(...).UpdateWithRetries(...)
work, we'll need to restructure how we add extra methods to ResourceInterface in our dynamic/clientset
wrapper.
Resource: resource.Name, | ||
} | ||
} | ||
|
||
func (cs *Clientset) resource(apiResource *dynamicdiscovery.APIResource, namespace string) (*ResourceClient, 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.
One of the improvements in the new dynamic client is that it decouples specification of the namespace from the creation of the client. We should mirror that by removing the namespace
argument here and in Clientset.Resource()
.
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.
And also Clientset.Kind()
?
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.
Oh yeah, that one too. Good catch. :)
|
||
dc *dynamic.Client | ||
dc dynamic.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.
We might not need to store dc
here anymore, since we no longer need to make a new client just to change the namespace.
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.
Removal of dc
means we have to find another way of initializing ResourceClient.NamespaceableResourceInterface
. I will see what is the possible workaround. This will take me some time because I am basically have zero knowledge on API machinery.
gv schema.GroupVersion | ||
resource *dynamicdiscovery.APIResource | ||
} | ||
|
||
func (rc *ResourceClient) WithNamespace(namespace string) *ResourceClient { |
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.
In order to add UpdateWithRetries()
for namespaced clients, we probably need to override Namespace()
here and make it return a new wrapper struct that anonymously embeds a dynamic.ResourceInterface
as well as implementing UpdateWithRetries()
.
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.
Hasn't UpdateWithRetries()
already implemented here?
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 need to implement UpdateWithRetries() twice, although they can both just call the same private function that does the real work. My goal was to match the pattern used in the new dynamic client for handling the fact that some API calls are namespaced and others are not.
Notice that NamespaceableResourceInterface contains both a Namespace()
method and a full embedding of ResourceInterface:
type NamespaceableResourceInterface interface {
Namespace(string) ResourceInterface
ResourceInterface
}
That means, for example, you can do both dc.Resource().List()
and dc.Resource().Namespace().List()
. Which one you use depends on whether you want to send a namespaced request or a cluster-scoped request. For example, you can list objects for one namespace, or for all namespaces.
To match this for the extra functions we add like UpdateWithRetries(), we should make sure both rc.UpdateWithRetries()
and rc.Namespace().UpdateWithRetries()
work, because we want to be able to update objects that are either cluster-scoped or namespaced.
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.
Oh, i see! I have to support rc.UpdateWithRetries()
too for cluster-scoped operations. I thought we need to support only namespaced operations.
dynamic/informer/informer.go
Outdated
type dynamicResourceClientListFunc func(metav1.ListOptions) (*unstructured.UnstructuredList, error) | ||
|
||
func castToCacheListFunc(fn dynamicResourceClientListFunc) cache.ListFunc { | ||
options := metav1.ListOptions{} |
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.
This is meant to just be an adapter function, so you should pass along the ListOptions from the caller. See my other comment below, which should make this clearer.
dynamic/informer/informer.go
Outdated
func newSharedResourceInformer(client *dynamicclientset.ResourceClient, defaultResyncPeriod time.Duration, close func()) *sharedResourceInformer { | ||
informer := cache.NewSharedIndexInformer( | ||
&cache.ListWatch{ | ||
ListFunc: client.List, | ||
ListFunc: castToCacheListFunc(client.List), |
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 we only need to do this once, I don't think it warrants all the stuff broken out above. It should be possible to write this concisely as something like this:
ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
return client.List(opts)
}
Note that you don't need to cast the return value because a type that implements an interface is already assignable to a variable of that interface type.
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.
This is a much better way to do it! Thanks for specifying that the casting is not necessary.
} | ||
|
||
func New(config *rest.Config, resources *dynamicdiscovery.ResourceMap) *Clientset { | ||
func New(config *rest.Config, resources *dynamicdiscovery.ResourceMap) (*Clientset, error) { | ||
// TODO: find a way to obtain APIResource for getting GroupVersion. |
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.
@enisoc I shifted dc
field from ResourceClient
to Clientset
. Not sure whether this is what you suggested. Feel free to sync by person if I get you wrong.
When calling NewForConfig
within New
, I couldn't find any way to obtain apiVersion
and resource
info, so I couldn't call resources.Get(apiVersion, resource)
to obtain the GroupVersion
for populating config
.
I most probably at the wrong track, but decided to show my current progress for facilitating discussion (the codes compiled, but the smoke tests did not pass).
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.
My hope was that the new dynamic client is designed to let us use one dc
for many different GroupVersions and Resources. If that's so, we should be able to make a new dc
here without filling in any GroupVersion or Resource, since we will provide that later when calling dc.Resource()
.
This code looks like what I would expect to work if that were the case. However, if the tests don't pass it might be that we still need to make a new dc
for different GroupVersions. Are you seeing errors in the Metacontroller logs?
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.
Thanks for verifying that I am on the right track. I will post log errors later if I cannot resolve it. :)
Yes, the new dynamic client should allow us to use one dc
across multiple GVRs.
return &ResourceClient{ | ||
NamespaceableResourceInterface: dc.Resource(resourceGVR), | ||
dc: dc, | ||
NamespaceableResourceInterface: cs.dc.Resource(gvr), |
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.
Embedding dynamic interface dc
into the clientset cs
itself for initializing NamespaceableResourceInterface
.
I could not find another way to initialize NamespaceableResourceInterface
other than this.
@@ -111,7 +111,7 @@ func deleteChildren(client *dynamicclientset.ResourceClient, parent *unstructure | |||
// This observed object wasn't listed as desired. | |||
glog.Infof("%v %v/%v: deleting %v %v", parent.GetKind(), parent.GetNamespace(), parent.GetName(), obj.GetKind(), obj.GetName()) | |||
uid := obj.GetUID() | |||
err := client.Delete(name, &metav1.DeleteOptions{ | |||
err := client.Namespace(parent.GetNamespace()).Delete(name, &metav1.DeleteOptions{ |
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 either set Namespace
here, or earlier in the sync call tree via dynClient.Kind(apiVersion, kind).Namespace(parent.GetNamespace())
. I prefer existing implementation because we may need to work with cluster-scoped client
too.
} | ||
} | ||
|
||
func (nrc *NamespacedResourceClient) UpdateWithRetries(orig *unstructured.Unstructured, update func(obj *unstructured.Unstructured) bool) (result *unstructured.Unstructured, err 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.
Should we implement other functions (in another file) for NamespacedResourceClient
?
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.
For now, let's implement only UpdateWithRetries on NamespacedResourceClient. The others like GroupResource shouldn't be necessary if we pass around ResourceClient and only call .Namespace()
as needed.
Regarding UpdateWithRetries, it should be possible to extract this into a helper method that accepts a dynamic.ResourceInterface
to avoid duplicating code. Both ResourceClient
and NamespacedResourceClient
satisfy that 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.
Sound good to me, thanks!
} | ||
} | ||
|
||
func (nrc *NamespacedResourceClient) UpdateWithRetries(orig *unstructured.Unstructured, update func(obj *unstructured.Unstructured) bool) (result *unstructured.Unstructured, err 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.
For now, let's implement only UpdateWithRetries on NamespacedResourceClient. The others like GroupResource shouldn't be necessary if we pass around ResourceClient and only call .Namespace()
as needed.
Regarding UpdateWithRetries, it should be possible to extract this into a helper method that accepts a dynamic.ResourceInterface
to avoid duplicating code. Both ResourceClient
and NamespacedResourceClient
satisfy that interface.
resource: apiResource, | ||
}, nil | ||
NamespaceableResourceInterface: cs.dc.Resource(apiResource.GroupVersionResource()), | ||
gv: apiResource.GroupVersion(), |
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 don't remember why I originally stored gv
in addition to resource
, since you can get one from the other. Can you try removing gv
from the struct and see if that simplifies things without adding some other cost?
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.
Noted, will try removing gv
.
@@ -105,74 +99,57 @@ func (rc *ResourceClient) Kind() string { | |||
} | |||
|
|||
func (rc *ResourceClient) GroupVersion() schema.GroupVersion { | |||
return rc.gv | |||
return rc.resource.GroupVersion() | |||
} | |||
|
|||
func (rc *ResourceClient) GroupResource() schema.GroupResource { |
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.
GroupResource()
is still necessary for ResourceClient
because it is called here.
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 noticed one more problem (see below). Did the smoke tests already pass? I guess this shows we need more tests.
Everything else looks good to me after that's fixed.
@@ -499,7 +498,7 @@ func (pc *parentController) claimChildren(parent *unstructured.Unstructured) (co | |||
childMap := make(common.ChildMap) | |||
for _, child := range pc.cc.Spec.ChildResources { | |||
// List all objects of the child kind in the parent object's namespace. | |||
childClient, err := pc.dynClient.Resource(child.APIVersion, child.Resource, namespace) | |||
childClient, err := pc.dynClient.Resource(child.APIVersion, child.Resource) |
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.
Sorry for not noticing this earlier: I think since we no longer set the namespace here, we need to fix UnstructuredManager
in the dynamic/controllerref
package to call Namespace()
when needed (if the child object has a namespace set).
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.
@enisoc Yes, they passed. I ran all the tests for every commit now with image containing my changes via skaffold run
after changing skaffold.yaml
and image.yaml
to my docker ID.
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.
Noted, I will change dynamic/controllerref
package accordingly.
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.
@enisoc Do we also need to fix ControllerRevisionManager
? ControllerRevision
are namespaced objects.
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.
That's a good point, I hadn't thought of that. I just checked though and it looks like ControllerRevisionManager is unaffected by your changes because we use a generated client for that rather than the dynamic client.
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. I will push a commit soon. Thanks!
Style-wise, I prefer them as separate blocks. The first block follows the pattern of short-circuiting control flow (return/break/continue) if an error occurs. The second block is not part of error handling, but is actually part of the normal flow -- it's just that the normal flow happens to consist of an if block. |
obj.SetOwnerReferences(ownerRefs) | ||
return true | ||
}) | ||
var err 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.
This logic (check if it has a namespace and call UpdateWithRetries one way or the other) can be wrapped in a helper. If you do that, you can keep the code here looking simple (the only change will be calling your helper instead of directly calling UpdateWithRetries).
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.
Noted, will move the logic to a helper function.
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.
LGTM
This PR addresses issue #47. It changes the codebase to use new dynamic client.
Fixes #47
Fixes #79