internal/olm/operator/internal: operator-registry wrappers#2313
internal/olm/operator/internal: operator-registry wrappers#2313estroz merged 9 commits intooperator-framework:masterfrom
Conversation
b6d4312 to
2d03ce3
Compare
|
|
||
| package olm | ||
|
|
||
| import ( |
There was a problem hiding this comment.
Why the path is internal/olm/operator/internal again?
Could not it be just internal/olm/registry since what all it is doing is creating the Registry?
Also, it if is a public API then, I understand that, should be in the pkg instead of internals.
There was a problem hiding this comment.
There was a problem hiding this comment.
OK. But it shows strange internal/.../internal again.
WDYT about internal/olm/operator/registry
There was a problem hiding this comment.
It isn't a common use case but it is a completely viable use of internal.
| withRegistryGRPCContainer(pkgName), | ||
| withVolumeConfigMap(volName, cm.GetName()), | ||
| withContainerVolumeMounts(volName, []string{containerManifestsDir}), | ||
| ) |
There was a problem hiding this comment.
The above impl is strange because the with would make sense it was a composition as:
newRegistryDeployment().withA()..withB()
However, it is not. The withs func impl here just building and returning the data get the data, so wdyt about something such asbuildRegistryGRPCContainerDeployment or newRegistryGRPCContainerDeployment instead of for example? Also, could not their logic be part of the newRegistryDeployment impl?
What are the scenarios that RegistryDeployment should be created with then or not? I could not find another place that is doing the same, so shows that it should always have the RegistryGRPCContainer, VolumeConfigMap and ContainerVolumeMounts. Am I right?
There was a problem hiding this comment.
Going forward we may want to configure the registry Deployment differently. Using functional options allows us to configure the Deployment just by defining a new function to pass as an argument. The same applies to other object constructors.
As for naming see DynamicRESTMapperOption for an example.
There was a problem hiding this comment.
I am ok with the usage of the options.
My comment is regarding the nomenclature of the funcs .
IHMO the with would make more sense if it was used as a composition myResourse().withA().withB() or buildSearch().withA().withB() which it is not the case.
The with funcs are creating/buiding new resources which are returned and used as param and can be used in any part of this code. So:
deployment:= withRegistryGRPCContainer(pkgName) (what with does?)
Would not make easier/intuitive the understatement see something as:
deployment:= newRegistryGRPContainer(pkgName) (what the new does?)
I hope that I could clarify my POV. My suggestion here would change the name only. However, it is not a blocker at all. Am ok with if the others are.
There was a problem hiding this comment.
deployment:= withRegistryGRPCContainer(pkgName) will return a function with a *v1.Deployment parameter, not a v1.Deployment object. Yes I'd like to hear what others have to say as well.
There was a problem hiding this comment.
AH! 💡 I see what you are doing. I too thought the withXXX was strange but you are using it as part of a variable argument list to allow options to be specified when creating the Registry. That's quite clever, I never would've done it that way. I think I would've had a Parameters or options struct that I built up setting the various options and passing that in. But I kind of like the withXXX pattern you have going here.
dep := newRegistryDeployment(pkgName, namespace,
withRegistryGRPCContainer(pkgName),
withVolumeConfigMap(volName, cm.GetName()),
withContainerVolumeMounts(volName, []string{containerManifestsDir}),
Considering the above code from your example, I read this as create me a new RegistryDeployment using a container with GRPC, with a volume configmap with a list of volumemounts.
Again, I never would've approached it that way but it met my time criteria, that is how long does it take me to figure out what this function is doing. I'm okay with the current implementation.
There was a problem hiding this comment.
@jmrodri Yeah the use of functional options is a common pattern that we've used before in the SDK, and upstream in the controller-runtime client and logger.
Though method chaining is probably easier to understand at first glance there are benefits to using functional options when returning errors.
Neither are idiomatic in Go but I prefer functional options.
@estroz I do think we can probably clean this up a bit to actually define Options structs and type aliases for the functional option types for both the RegistryConfigMap options and the RegistryDeployment options.
Take a look at how the controller-runtime's logger defines it's functional options.
https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/log/zap/zap.go
We can have an Options struct for just the RegistryDeployment options and have the helpers only set the options. e.g(roughly speaking):
type Opts func(*RegistryDeploymentOptions)
type RegistryDeploymentOptions struct {
PkgName string
...
}
func withRegistryGRPCContainer(pkgName string) Opts {
return func(o *RegistryDeploymentOptions) {
o.PkgName = pkgName
}
}And then newRegistryDeployment() just applies all the Opts passed in to a default RegistryDeploymentOptions struct and then creates the deployment based on those options. E.g how the logger constructor does it:
https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/log/zap/zap.go#L195-L209
| pkgName := m.Pkg.PackageName | ||
| cm := newRegistryConfigMap(pkgName, namespace) | ||
| dep := newRegistryDeployment(pkgName, namespace) | ||
| service := newRegistryService(pkgName, namespace) |
There was a problem hiding this comment.
Why is required re-create the full objects to delete them? Why not get the object by the name and namespace and then, delete it? Could not this impl face pitfuls scenarios where the object in the cluster was updated and is not so longer exactly equals the generation? Will not it face an error if the object for some reason was not really created?
There was a problem hiding this comment.
Client.Delete() only cares about object metadata (type info, namespace, and name), which an empty object provides. Client.Get()-ing an object first would be an unnecessary network-bound operation because we know all necessary type information before calling Client.Delete().
There was a problem hiding this comment.
IHMO: would be better:
// get the resource by name and namespace
// if exist remove and log
// if not exist just log
There was a problem hiding this comment.
It depends on how often we need to do this and whether you ever care to keep the old ones around. I'm okay with just nuking all of the manifests and then optimizing later if speed or other factors come into play.
There was a problem hiding this comment.
💡 I now see what @camilamacedo86 is saying because the method names say newXXX it looks like you are getting back a FULL XXX object. Looking at the implementation of newXXX you see it creates metadata only since there was no with arguments specified. This one does seem a bit harder to maintain in the long run. It feels too clever :) What @camilamacedo86 stated would definitely be more clear to the maintainer of what is REALLY going on.
I need to think about this one a little more.
There was a problem hiding this comment.
I do not see why having an extra Get() step would be beneficial, since all Get() is doing in this context is populating metadata; this is what newXXX() does implicitly, without a request. Perhaps a comment on this bit of could will suffice? Code comments on each newXXX() method explain that only metadata is populated in the returned object.
| // withTCPPort returns a function that appends a service port to a Service's | ||
| // port list with name and TCP port portNum. | ||
| func withTCPPort(name string, portNum int32) func(*corev1.Service) { | ||
| return func(service *corev1.Service) { |
There was a problem hiding this comment.
IHMO withTCPPort is not helpful for we know what it does. WDYT about something as buildServiceWithTCPPort?
There was a problem hiding this comment.
The comment and return type makes its intended use obvious 😄.
There was a problem hiding this comment.
Sorry, if I could not make clear my POV.
By looking a code as for example: newResource(deployment, service, withTCPPort(pkg))
I am not sure that my understanding will be that the withTCPPort will impl a new Service with a TCP port without the need to go there and check the withTCPPort code implementation.
However, if it was something as newService().withTCPPort() or newResource(deployment, service, newTCPService(pkg)) then, I think that is not required check the details of the impl to know what it does.
My suggestion would be just rename the func to make clear what it does without the need to check its implementation.
There was a problem hiding this comment.
The withTCPPort makes sense now that I've understood the paradigm being used here. I'm okay with it.
| ) | ||
| if err = m.Client.DoCreate(ctx, cm, dep, service); err != nil { | ||
| return fmt.Errorf("error creating operator %q registry-server objects: %w", pkgName, err) | ||
| } |
There was a problem hiding this comment.
Wdyt about creating one and then check, create other and then check again?
Where/How it will be called/used? Could it be used in a controller/reconcile?
Should we not first check if the resource exists and then, if not create?
Could not it face a scenario where the deployment, for example, was already created?
There was a problem hiding this comment.
Check out the error handling logic of DoCreate(). "already exists" errors are logged instead of being returned.
There was a problem hiding this comment.
Have many errors logs in the code that are not actually errors make harder troubleshooting.
IHMO: would be better:
// check if the resource exists
// if not
// then, build a new resource
// and then create and log
There was a problem hiding this comment.
@camilamacedo86 I think the check if resource exists first will get you a bunch of misses. When you are creating things you are likely to not have them there so the check is 90% of the time going to return does not exist. But if you flip the logic the way @estroz has it, you attempt the create, 90% of the time the create will happen, the other 10% it already exists you didn't really spend too much time. As long as the "already exist" error is handled I'm okay with it. I will say I would normally write it like @camilamacedo86 specified because that's just how I do things. But I'm okay with the approach in this PR.
|
|
||
| // FormatOperatorNameDNS1123 ensures name is DNS1123 label-compliant by | ||
| // replacing all non-compliant UTF-8 characters with "-". | ||
| func FormatOperatorNameDNS1123(name string) string { |
There was a problem hiding this comment.
Have we not a func in the apimachinery that we could call directly instead of creating this one?
If not, could we add a comment to make clear that it is a copy and paste from there? WDYT?
There was a problem hiding this comment.
AFAIK there is no function in apimachinery to format a string as a DNS1123 label. Nothing is copied from apimachinery either.
There was a problem hiding this comment.
If there is no library to do this in the SDK or one of our main deps like controller-runtime or kubebuilder, then I'm okay with this function.
jmrodri
left a comment
There was a problem hiding this comment.
The withXXX paradigm is quite clever, not something I would ever have done as I don't tend to think like that. After reading all of the code I understand how it works it might be confusing to others though during maintenance of this code but I say if it becomes a maintenance burden we can refactor it at that point.
| pkgName := m.Pkg.PackageName | ||
| cm := newRegistryConfigMap(pkgName, namespace) | ||
| dep := newRegistryDeployment(pkgName, namespace) | ||
| service := newRegistryService(pkgName, namespace) |
There was a problem hiding this comment.
It depends on how often we need to do this and whether you ever care to keep the old ones around. I'm okay with just nuking all of the manifests and then optimizing later if speed or other factors come into play.
| withRegistryGRPCContainer(pkgName), | ||
| withVolumeConfigMap(volName, cm.GetName()), | ||
| withContainerVolumeMounts(volName, []string{containerManifestsDir}), | ||
| ) |
There was a problem hiding this comment.
AH! 💡 I see what you are doing. I too thought the withXXX was strange but you are using it as part of a variable argument list to allow options to be specified when creating the Registry. That's quite clever, I never would've done it that way. I think I would've had a Parameters or options struct that I built up setting the various options and passing that in. But I kind of like the withXXX pattern you have going here.
dep := newRegistryDeployment(pkgName, namespace,
withRegistryGRPCContainer(pkgName),
withVolumeConfigMap(volName, cm.GetName()),
withContainerVolumeMounts(volName, []string{containerManifestsDir}),
Considering the above code from your example, I read this as create me a new RegistryDeployment using a container with GRPC, with a volume configmap with a list of volumemounts.
Again, I never would've approached it that way but it met my time criteria, that is how long does it take me to figure out what this function is doing. I'm okay with the current implementation.
| ) | ||
| if err = m.Client.DoCreate(ctx, cm, dep, service); err != nil { | ||
| return fmt.Errorf("error creating operator %q registry-server objects: %w", pkgName, err) | ||
| } |
There was a problem hiding this comment.
@camilamacedo86 I think the check if resource exists first will get you a bunch of misses. When you are creating things you are likely to not have them there so the check is 90% of the time going to return does not exist. But if you flip the logic the way @estroz has it, you attempt the create, 90% of the time the create will happen, the other 10% it already exists you didn't really spend too much time. As long as the "already exist" error is handled I'm okay with it. I will say I would normally write it like @camilamacedo86 specified because that's just how I do things. But I'm okay with the approach in this PR.
| pkgName := m.Pkg.PackageName | ||
| cm := newRegistryConfigMap(pkgName, namespace) | ||
| dep := newRegistryDeployment(pkgName, namespace) | ||
| service := newRegistryService(pkgName, namespace) |
There was a problem hiding this comment.
💡 I now see what @camilamacedo86 is saying because the method names say newXXX it looks like you are getting back a FULL XXX object. Looking at the implementation of newXXX you see it creates metadata only since there was no with arguments specified. This one does seem a bit harder to maintain in the long run. It feels too clever :) What @camilamacedo86 stated would definitely be more clear to the maintainer of what is REALLY going on.
I need to think about this one a little more.
| // withTCPPort returns a function that appends a service port to a Service's | ||
| // port list with name and TCP port portNum. | ||
| func withTCPPort(name string, portNum int32) func(*corev1.Service) { | ||
| return func(service *corev1.Service) { |
There was a problem hiding this comment.
The withTCPPort makes sense now that I've understood the paradigm being used here. I'm okay with it.
|
|
||
| // FormatOperatorNameDNS1123 ensures name is DNS1123 label-compliant by | ||
| // replacing all non-compliant UTF-8 characters with "-". | ||
| func FormatOperatorNameDNS1123(name string) string { |
There was a problem hiding this comment.
If there is no library to do this in the SDK or one of our main deps like controller-runtime or kubebuilder, then I'm okay with this function.
|
@jmrodri @hasbro17 @camilamacedo86 I'm going to follow up this PR with some of the suggested changes. These changes won't affect how this library is used very much or at all. |
|
@estroz 👍 We can do a follow up to clean up the formatting of the functional options. |
Description of the change: Methods on a
RegistryResourcesallow creation and deletion operations as well as staleness detection on an operator-registry registry-server.See the proposal doc for details on how the registry is managed.
Motivation for the change: Broken out of #1912.
/ping @ecordell @njhale @kevinrizza