-
Notifications
You must be signed in to change notification settings - Fork 1.8k
internal/olm/operator/internal: operator-registry wrappers #2313
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
internal/olm/operator/internal: operator-registry wrappers #2313
Conversation
b6d4312
to
2d03ce3
Compare
|
||
package olm | ||
|
||
import ( |
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.
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.
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.
OK. But it shows strange internal/.../internal again.
WDYT about internal/olm/operator/registry
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IHMO withTCPPort
is not helpful for we know what it does. WDYT about something as buildServiceWithTCPPort
?
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 comment and return type makes its intended use obvious 😄.
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, 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment above: #2313 (comment).
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check out the error handling logic of DoCreate()
. "already exists" errors are logged instead of being returned.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
RegistryResources
allow 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