-
Notifications
You must be signed in to change notification settings - Fork 306
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
Emit event on Description-only healthcheck update #2072
Emit event on Description-only healthcheck update #2072
Conversation
Hi @DamianSawicki. Thanks for your PR. I'm waiting for a kubernetes member to verify that this patch is reasonable to test. If it is, they should reply with Once the patch is verified, the new status will be reflected by the I understand the commands that are listed here. Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
/assign aojea |
pkg/translator/healthchecks.go
Outdated
@@ -338,3 +341,32 @@ func ApplyProbeSettingsToHC(p *v1.Probe, hc *HealthCheck) { | |||
|
|||
hc.Description = DescriptionForHealthChecksFromReadinessProbe | |||
} | |||
|
|||
// GetObjectKind implements runtime.Object |
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 this change related?
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.
Yes. EventRecorder.Event
requires the first argument to be runtime.Object
, and this interface contains two methods: GetObjectKind() schema.ObjectKind
and DeepCopyObject() Object
.
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 object in this case is the Service or just pass the Namespace, doing this to the healtcheck object seems very brittle
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 you can get it from defaultBackendSvc types.NamespacedName
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 again requires implementing the above methods either for string
(in the case of namespace) or for types.NamespacedName
(in the case of service), and when trying to do that, I'm getting the following error cannot define new methods on non-local type "k8s.io/apimachinery/pkg/types".NamespacedName
. I guess I can write my own alias type for one of these and then implement the methods for the alias. @aojea, do you think it is a good idea?
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.
no sorry, my point is that we should not modify this type, and use one of the Kubernetes types associated to the healtcheck, it can be the Service or the Namespace, ideally the 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.
Thanks for the clarification! Done, please have a look.
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.
you forgot to remove this code
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.
Done.
@@ -142,6 +144,11 @@ func (h *HealthChecks) sync(hc *translator.HealthCheck, bchcc *backendconfigv1.H | |||
changes := calculateDiff(filter(existingHC), filter(hc), bchcc) | |||
if changes.hasDiff() { | |||
klog.V(2).Infof("Health check %q needs update (%s)", existingHC.Name, changes) | |||
if flags.F.EnableUpdateCustomHealthCheckDescription && changes.size() == 1 && changes.has("Description") { | |||
message := fmt.Sprintf("Healthcheck will be updated and the only field updated is Description.\nOld: %+v\nNew: %+v\nDiff: %+v", existingHC, hc, changes) |
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 seems a very large message from an event, can we just send the diff or do we miss some important information?
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 if
guarantees that the diff contains only the Description update. Recall from our discussions that this event emission was intended as a precaution during Description update, so the event would not serve this purpose well if it contained only the "positive scenario" information.
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 message contains the New and Old too, can we send only the changes? do we need the others?
%+v\nNew: %+v\nDiff: %+v", existingHC, hc, changes)
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.
Yes, I'm trying to say that we need the others. The changes
aka "diff" inside the if
consists only of the modified Description (changes.size() == 1 && changes.has("Description")
). However, we want to include more information in the message as a precaution.
A background fact that I should probably mention is that calculateDiff()
does not return the full diff. It checks selected fields only (recently Description
was added as one of them), sometimes just 2 of them.
/ok-to-test looks good overall, some comments that needs to be addressed |
d396a05
to
9d5fea2
Compare
66f7f13
to
ad6a07b
Compare
pkg/healthchecks/healthchecks.go
Outdated
if flags.F.EnableUpdateCustomHealthCheckDescription && changes.size() == 1 && changes.has("Description") { | ||
message := fmt.Sprintf("Healthcheck will be updated and the only field updated is Description.\nOld: %+v\nNew: %+v\nDiff: %+v", existingHC, hc, changes) | ||
h.eventRecorder.Event( | ||
hc.Service, "Warning", "HealthcheckDescriptionUpdate", message) |
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.
hmm, I have to apologize, but now that I look it carefully , it seems Service is not the right object to pass, in this case this method is trying to sync bchcc *backendconfigv1.HealthCheckConfig
, I think this object is implementing runtime.Object
, isnt't it?, that will be the best thing since we can do this only with 4-5 lines of code
Sorry again, I've should have checked this better before doing any suggestion
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.
@aojea we want to emit the even on the Service because UHC is created per Service and one BackendConfig can be assign to multiple Services but all Customer cares is a configuration of HealthCheck for 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.
yeah, Damien commented offline, sounds good
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.
yeah, Damien commented offline, sounds good
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.
*Damian
pkg/backends/backends.go
Outdated
@@ -41,7 +42,7 @@ type Backends struct { | |||
} | |||
|
|||
// Backends is a Pool. | |||
var _ Pool = (*Backends)(nil) | |||
var _ interfaces.Pool = (*Backends)(nil) |
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 this change was needed?
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 you need to rebase
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.
Discussed offline, done.
GetService(namespace, name string) (*api_v1.Service, error) | ||
} | ||
|
||
func NewEmptyServiceGetter() ServiceGetter { |
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.
NewFake
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.
Done.
|
||
type fakeServiceGetter struct{} | ||
|
||
func (fsg *fakeServiceGetter) GetService(namespace, name string) (*api_v1.Service, 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.
I think it will be better to return some default Service here. I don't think is good to return the empty object. Normally when you get nil error you expect that the Object is properly constructed. This may lead to some crashes I think.
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.
According to the comments ,api_v1.Service
has only one non-optional field of type metav1.TypeMeta
, which in turn has only optional fields. So it seems that the "empty object" is a valid object.
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.
you can at least fill name and namespace to match function params.
Where is it used?
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 should be enough
svc := &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: 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's just used as one of the Event parameters.
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.
@aojea Yeah, I missed your comment earlier, but this is exactly what we ended up with.
pkg/controller/controller.go
Outdated
@@ -114,7 +115,8 @@ func NewLoadBalancerController( | |||
Interface: ctx.KubeClient.CoreV1().Events(""), | |||
}) | |||
|
|||
healthChecker := healthchecks.NewHealthChecker(ctx.Cloud, ctx.HealthCheckPath, ctx.DefaultBackendSvcPort.ID.Service) | |||
eventRecorder := ctx.Recorder(ctx.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.
the namespace for recorder needs to be the namespace of the Service you wants to emit the event for. And health checker is created one per controller you need to postpone this function to the place where you have your Service.
You probably need context there.
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.
Done.
just please don't use calls to the apiserver on the getter, remember that you have informers that already have Listers and Getter to a local cache with all the objects information. The other alternative is to plumb down the information on initialization, I'm not familiar with the code, so I can't suggest which one is the best option |
cb61a41
to
60aa3d0
Compare
d851dac
to
d726038
Compare
pkg/healthchecks/healthchecks.go
Outdated
@@ -67,9 +69,23 @@ func (h *HealthChecks) new(sp utils.ServicePort) *translator.HealthCheck { | |||
hc.Name = sp.BackendName() | |||
hc.Port = sp.NodePort | |||
hc.RequestPath = h.pathFromSvcPort(sp) | |||
service := h.mainService(sp) |
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'll flag-gate the following lines.
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.
Done.
d726038
to
daa03ce
Compare
/uncc kl52752 |
/assign cezarygerard |
daa03ce
to
497877a
Compare
pkg/healthchecks/healthchecks.go
Outdated
@@ -142,6 +160,11 @@ func (h *HealthChecks) sync(hc *translator.HealthCheck, bchcc *backendconfigv1.H | |||
changes := calculateDiff(filter(existingHC), filter(hc), bchcc) | |||
if changes.hasDiff() { | |||
klog.V(2).Infof("Health check %q needs update (%s)", existingHC.Name, changes) | |||
if flags.F.EnableUpdateCustomHealthCheckDescription && changes.size() == 1 && changes.has("Description") { | |||
message := fmt.Sprintf("Healthcheck will be updated and the only field updated is Description.\nOld: %+v\nNew: %+v\nDiff: %+v", existingHC, hc, changes) | |||
h.recorderGetter.Recorder(hc.Service.Namespace).Event( |
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.
hc.Service Can be null here,
if it error occurred while assigning the hc.Service in pkg/healthchecks/healthchecks.go
if flags.F.EnableUpdateCustomHealthCheckDescription {
service := h.mainService(sp)
var err error
hc.Service, err = h.serviceGetter.GetService(service.Namespace, service.Name)
if err != nil {
klog.Errorf("Service %s/%s not found: %v.", service.Namespace, service.Name, 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.
Great catch!
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.
Done.
pkg/healthchecks/healthchecks.go
Outdated
@@ -67,9 +69,25 @@ func (h *HealthChecks) new(sp utils.ServicePort) *translator.HealthCheck { | |||
hc.Name = sp.BackendName() | |||
hc.Port = sp.NodePort | |||
hc.RequestPath = h.pathFromSvcPort(sp) | |||
if flags.F.EnableUpdateCustomHealthCheckDescription { |
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.
you should not be using flags this deep down the logic
convert them into fields somewhere in context or close to main
it makes it much more testable
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 had this discussion already with kl52752, panslava, and bowei: #2018 (comment). This is the same flag as there and we're not adding any new non-parallelizable tests in this PR, so I'll stick to the approach bowei suggested in #2018 and keep the flag deep down.
pkg/healthchecks/healthchecks.go
Outdated
var err error | ||
hc.Service, err = h.serviceGetter.GetService(service.Namespace, service.Name) | ||
if err != nil { | ||
klog.Errorf("Service %s/%s not found: %v.", service.Namespace, service.Name, 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.
-
why erorr?
we keep going so it should be a Warning -
Add one more sentence to this log, what are the consequences (not big, we keep going)
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.
Done.
pkg/healthchecks/healthchecks.go
Outdated
@@ -67,9 +69,25 @@ func (h *HealthChecks) new(sp utils.ServicePort) *translator.HealthCheck { | |||
hc.Name = sp.BackendName() | |||
hc.Port = sp.NodePort | |||
hc.RequestPath = h.pathFromSvcPort(sp) | |||
if flags.F.EnableUpdateCustomHealthCheckDescription { | |||
service := h.mainService(sp) |
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.
service is a confusing name here
It suggests it's v1.Service
but it is just name and namespace
see my next 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.
Done.
pkg/healthchecks/healthchecks.go
Outdated
if flags.F.EnableUpdateCustomHealthCheckDescription { | ||
service := h.mainService(sp) | ||
var err error | ||
hc.Service, err = h.serviceGetter.GetService(service.Namespace, service.Name) | ||
if err != nil { | ||
klog.Errorf("Service %s/%s not found: %v.", service.Namespace, service.Name, 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.
this whole part can be well hidden:
see this:
// new returns a *HealthCheck with default settings and specified port/protocol
func (h *HealthChecks) new(sp utils.ServicePort) *translator.HealthCheck {
var hc *translator.HealthCheck
if sp.NEGEnabled && !sp.L7ILBEnabled {
hc = translator.DefaultNEGHealthCheck(sp.Protocol)
} else if sp.L7ILBEnabled {
hc = translator.DefaultILBHealthCheck(sp.Protocol)
} else {
hc = translator.DefaultHealthCheck(sp.NodePort, sp.Protocol)
}
// port is the key for retrieving existing health-check
// TODO: rename backend-service and health-check to not use port as key
hc.Name = sp.BackendName()
hc.Port = sp.NodePort
hc.RequestPath = h.pathFromSvcPort(sp)
hc.Service = h.getService(sp)
return hc
}
func (h *HealthChecks) getService(sp utils.ServicePort) *v1.Service {
if flags.F.EnableUpdateCustomHealthCheckDescription {
namespacedName := h.mainService(sp)
var err error
service, err := h.serviceGetter.GetService(namespacedName.Namespace, namespacedName.Name)
if err != nil {
klog.Errorf("Service %s/%s not found: %v.", namespacedName.Namespace, namespacedName.Name, err)
}
return service
}
return nil
}
func (h *HealthChecks) mainService(sp utils.ServicePort) types.NamespacedName {
service := h.defaultBackendSvc
if sp.ID.Service.Name != "" {
service = sp.ID.Service
}
return 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.
Much better, I somehow missed it!
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.
Done.
pkg/healthchecks/healthchecks.go
Outdated
if flags.F.EnableUpdateCustomHealthCheckDescription && changes.size() == 1 && changes.has("Description") { | ||
message := fmt.Sprintf("Healthcheck will be updated and the only field updated is Description.\nOld: %+v\nNew: %+v\nDiff: %+v", existingHC, hc, changes) | ||
h.recorderGetter.Recorder(hc.Service.Namespace).Event( | ||
hc.Service, "Warning", "HealthcheckDescriptionUpdate", message) |
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 Info type of message, not Warning
be cautious what we log to customer projects ;-)
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 use const from this package
vendor/k8s.io/api/core/v1/types.go
as we do in other places
// Valid values for event types (new types could be added in future)
const (
// Information only and will not cause any problems
EventTypeNormal string = "Normal"
// These events are to warn that something might go wrong
EventTypeWarning string = "Warning"
)
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.
Done.
pkg/healthchecks/interfaces.go
Outdated
|
||
// ServiceGetter is an interface to retrieve Kubernetes Services. | ||
type ServiceGetter interface { | ||
GetService(namespace, name string) (*v1.Service, error) | ||
} | ||
|
||
func NewFakeServiceGetter() ServiceGetter { | ||
return &fakeServiceGetter{} | ||
} | ||
|
||
type fakeServiceGetter struct{} | ||
|
||
func (fsg *fakeServiceGetter) GetService(namespace, name string) (*v1.Service, error) { | ||
return &v1.Service{ | ||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name}, | ||
}, nil | ||
} | ||
|
||
type RecorderGetter interface { | ||
Recorder(ns string) record.EventRecorder | ||
} | ||
|
||
func NewFakeRecorderGetter(bufferSize int) RecorderGetter { | ||
return &fakeRecorderGetter{bufferSize} | ||
} | ||
|
||
type fakeRecorderGetter struct { | ||
bufferSize int | ||
} | ||
|
||
// Returns a different record.EventRecorder for every call. | ||
func (frg *fakeRecorderGetter) Recorder(namespace string) record.EventRecorder { | ||
return record.NewFakeRecorder(frg.bufferSize) | ||
} | ||
|
||
type singletonFakeRecorderGetter struct { | ||
recorder *record.FakeRecorder | ||
} | ||
|
||
// Returns the same record.EventRecorder irrespective of the namespace. | ||
func (sfrg *singletonFakeRecorderGetter) Recorder(namespace string) record.EventRecorder { | ||
return sfrg.FakeRecorder() | ||
} | ||
|
||
func (sfrg *singletonFakeRecorderGetter) FakeRecorder() *record.FakeRecorder { | ||
if sfrg.recorder == nil { | ||
panic("singletonFakeRecorderGetter not initialised: recorder is nil.") | ||
} | ||
return sfrg.recorder | ||
} | ||
|
||
func NewFakeSingletonRecorderGetter(bufferSize int) *singletonFakeRecorderGetter { | ||
return &singletonFakeRecorderGetter{recorder: record.NewFakeRecorder(bufferSize)} | ||
} |
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.
all fakes should be moved to _test.go file
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.
Moved to a new flakes.go
file because somehow when I moved it to healthchecks_test.go
it was not visible in other packages.
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.
yeah... so it has to be compiled into final binary
I have tried to use build constraint
//go:build test
or
//go:build testing
but it does not work
golang keeps amazing me :-D
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.
497877a
to
fe47055
Compare
/lgtm |
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: cezarygerard, DamianSawicki 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 |
Since #2008, the health check description is modified if the service health check is configured with a BackendConfig CRD. The purpose of the present PR is to emit an event on the service on this occasion.
The changes are guarded with the flag
flags.F.EnableUpdateCustomHealthCheckDescription
from #2018.