Skip to content
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

return 503 for aggregated APIs when the APIServiceRegistrationController hasn't finished installing all known APIServices #104748

Merged
merged 9 commits into from Oct 26, 2021

Conversation

p0lyn0mial
Copy link
Contributor

@p0lyn0mial p0lyn0mial commented Sep 3, 2021

What type of PR is this?

/kind bug

What this PR does / why we need it:

Ensures that requests to aggregated APIs will get 503, not 404 while the APIServiceRegistrationController hasn't finished installing all known APIServices. It is especially important for controllers like GC and NS since they act on 404

Note that this is an already existing behavior (returning 503 when the CRD informer hasn't been sync).
We already have it for the apiextentionserver, see #105653 for more details.

Which issue(s) this PR fixes:

Fixes #104342

Does this PR introduce a user-facing change?

resolves a potential issue with GC and NS controllers which may delete objects after getting a 404 response from the server during its startup. This PR ensures that requests to aggregated APIs will get 503, not 404 while the APIServiceRegistrationController hasn't finished its job.

Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:


@k8s-ci-robot k8s-ci-robot added release-note Denotes a PR that will be considered when it comes time to generate release notes. kind/bug Categorizes issue or PR as related to a bug. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. do-not-merge/needs-kind Indicates a PR lacks a `kind/foo` label and requires one. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. do-not-merge/needs-sig Indicates an issue or PR lacks a `sig/foo` label and requires one. needs-triage Indicates an issue or PR lacks a `triage/foo` label and requires one. needs-priority Indicates a PR lacks a `priority/foo` label and requires one. and removed do-not-merge/needs-kind Indicates a PR lacks a `kind/foo` label and requires one. labels Sep 3, 2021
@p0lyn0mial
Copy link
Contributor Author

/assing @tkashem @sttts

@k8s-ci-robot k8s-ci-robot added area/apiserver sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. and removed do-not-merge/needs-sig Indicates an issue or PR lacks a `sig/foo` label and requires one. labels Sep 3, 2021
@@ -22,6 +22,7 @@ package app
import (
"crypto/tls"
"fmt"
"k8s.io/apiserver/pkg/util/notfoundhandler"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

order

func (s emptyDelegate) UnprotectedHandler() http.Handler {
return nil
return s.handler
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remind me why we only need this for the unprotected handler

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please help me understand, by spelling out the rest of the answer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API server is composed of three servers. The AggregatorServer is first in the chain and delegates to the KubeAPIServer which in turn delegates to the ExtentionAPIServer.

Delegation happens when the server doesn't have a path registered to handle a request.

Only the first server in the chain is protected in a sense that it will apply all the registered filters, like AuthZ/AuthnN.

Unprotected in this case means give me a handler that won't apply the filters. In this specific case it make sense since the first server have already done the hard work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sttts : I am not sure I understand your question. A DelegationTarget has only one method that returns a Handler (this PR does not change this fact).

"k8s.io/apiserver/pkg/util/signal"
)

// New returns an HTTP handler that is meant to be executed at the end of the delegation chain and check if the server has been fully prepared before replying to the client.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mention that this is long before readiness and is meant to block request before the muxes and handlers are completely initialized such that requests are properly routed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And that preparedToServe must not depend on other components in the cluster other than etcd


func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
for _, preparedToServeSignal := range h.preparedToServeSignals {
if !preparedToServeSignal.IsSignaled() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continue early

@@ -113,10 +114,26 @@ type preparedAPIAggregator struct {
runnable runnable
}

// preparedToServeSignals holds signals/events that indicate this server has been fully initialized and is ready to serve requests
type preparedToServeSignals struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why a wrapper around another type? Just use signal.Interface directly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to expose only the read-only interface

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the current text exposes the full interface.
It looks to me like the reason for a struct here is to prepare for more signals in the future. For example, one about RBAC being initialized.

@p0lyn0mial
Copy link
Contributor Author

I will remove the signal pkg and use raw types instead. I will also add a signal from the extension API server.

case <-e.ch:
// already closed, don't close again.
default:
close(e.ch)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if Signal is used concurrently, i think we need to use a sync.Once to protect it.

}
}

type namedChannelWrapper struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we are converting this to a shared utility, i would recommend unit tests for it.

@caesarxuchao
Copy link
Member

/cc @cheftako @jpbetz

Walter, I think you mentioned a specific http status code during the sig meeting.

@caesarxuchao
Copy link
Member

/triage accepted

@k8s-ci-robot k8s-ci-robot added triage/accepted Indicates an issue or PR is ready to be actively worked on. and removed needs-triage Indicates an issue or PR lacks a `triage/foo` label and requires one. labels Sep 7, 2021
@tkashem
Copy link
Contributor

tkashem commented Sep 7, 2021

/cc @MikeSpreitzer

func (s emptyDelegate) UnprotectedHandler() http.Handler {
return nil
return s.handler
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please help me understand, by spelling out the rest of the answer?

@@ -271,14 +271,19 @@ func (s *GenericAPIServer) NextDelegate() DelegationTarget {
}

type emptyDelegate struct {
handler http.Handler
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What did "empty delegate" mean, exactly? Is it still empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty delegate is an HTTP handler that doesn't have any paths registered and won't pass the requests through to the next delegate.

It will still be empty as it doesn't registers any paths nor the next delegate. It allows us to register a special NotFound handler.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a go comment describing what handler is and when it is called

@@ -113,10 +114,26 @@ type preparedAPIAggregator struct {
runnable runnable
}

// preparedToServeSignals holds signals/events that indicate this server has been fully initialized and is ready to serve requests
type preparedToServeSignals struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the current text exposes the full interface.
It looks to me like the reason for a struct here is to prepare for more signals in the future. For example, one about RBAC being initialized.

… as a MuxComplete signal

apiServiceRegistrationControllerInitiated is closed when APIServiceRegistrationController has finished "installing" all known APIServices.
At this point we know that the proxy handler knows about APIServices and can handle client requests.
Before it might have resulted in a 404 response which could have serious consequences for some controllers like GC and NS
@p0lyn0mial
Copy link
Contributor Author

/retest

}
}
s.lifecycleSignals.MuxAndDiscoveryComplete.Signal()
klog.V(1).Infof("%s completed, all registered MuxAndDiscoveryComplete signals (%d) have finished", s.lifecycleSignals.MuxAndDiscoveryComplete.Name(), s.GenericAPIServer.MuxAndDiscoveryCompleteSignals())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be less technical. Better: %s has all endpoints registered and discovery information is complete

//
// Note that we don't want to add additional checks to the readyz path as it might prevent fixing bricked clusters.
// This specific handler is meant to "protect" requests that arrive before the paths and handlers are fully initialized.
func New(serializer runtime.NegotiatedSerializer, hasMuxAndDiscoveryIncompleteKeyFn func(ctx context.Context) bool) *Handler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the negation: isMuxAndDiscoveryComplete

@sttts sttts added the priority/important-soon Must be staffed and worked on either currently, or very soon, ideally in time for the next release. label Oct 19, 2021
@k8s-ci-robot k8s-ci-robot removed the needs-priority Indicates a PR lacks a `priority/foo` label and requires one. label Oct 19, 2021
@sttts
Copy link
Contributor

sttts commented Oct 19, 2021

Lgtm and approved.

Will give @MikeSpreitzer @caesarxuchao and others a chance to take a final look.

Copy link
Member

@MikeSpreitzer MikeSpreitzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about https://docs.google.com/document/d/1JQNT0eu6Gpn9UZ4rcfIUDkJf1wKc3f_57KKC4c-_s-U ? My recollection of the SIG api-machinery discussion is that a client opt-in was favored, out of concern that a non-optional solution would ruining some startup sequences that involve multiple processes.

type emptyDelegate struct {
// handler is called at the end of the delegation chain
// when a request has been made against an unregistered HTTP path the individual servers will simply pass it through until it reaches the handler.
handler http.Handler
}

func NewEmptyDelegate() DelegationTarget {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this func?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

projects using the generic api server libary might still use this method.


func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if !h.isMuxAndDiscoveryCompleteFn(req.Context()) {
errMsg := fmt.Sprintf("the request has been made before all known HTTP paths have been installed, please try again")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is fmt.Sprintf doing for us here?

// Before it might have resulted in a 404 response which could have serious consequences for some controllers like GC and NS
//
// Note that the APIServiceRegistrationController waits for APIServiceInformer to synced before doing its work.
apiServiceRegistrationControllerInitiated := make(chan struct{})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need both this channel and the handlerSyncedCh channel in the apiservice-registration-controller post start hook? Could we use just one channel for both purposes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, we can have just one channel.

//
// The presence of the key is checked in the NotFoundHandler (staging/src/k8s.io/apiserver/pkg/util/notfoundhandler/not_found_handler.go)
//
// The race may happen when a request reaches the NotFoundHandler because not all paths have been registered in the mux
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Referring to "the race" here is appropriate if the reader already knows there is a race involved.
However, I think the reader does not know that.
"A race" would be more appropriate for introducing the fact that there is a race.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the gdoc.

@sttts
Copy link
Contributor

sttts commented Oct 20, 2021

My recollection of the SIG api-machinery discussion is that a client opt-in was favored, out of concern that a non-optional solution would ruining some startup sequences that involve multiple processes.

@MikeSpreitzer in that discussion we didn't know that we had already a solution in place for CRDs, i.e. something that has become part of the API semantics expected by clients and that we cannot easily change.

Whether we want anything beyond this, we can discuss.

@p0lyn0mial
Copy link
Contributor Author

@sttts @MikeSpreitzer many thanks for reviewing the PR. I applied the last batch of comments. I think we are ready to tag.

@p0lyn0mial
Copy link
Contributor Author

/test pull-kubernetes-unit

Copy link
Member

@MikeSpreitzer MikeSpreitzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks basically OK to me.
I have just a few minor comments.

func (s emptyDelegate) UnprotectedHandler() http.Handler {
return nil
return s.handler
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sttts : I am not sure I understand your question. A DelegationTarget has only one method that returns a Handler (this PR does not change this fact).

@p0lyn0mial
Copy link
Contributor Author

p0lyn0mial commented Oct 25, 2021

My recollection of the SIG api-machinery discussion is that a client opt-in was favored, out of concern that a non-optional solution would ruining some startup sequences that involve multiple processes.

@MikeSpreitzer in that discussion we didn't know that we had already a solution in place for CRDs, i.e. something that has become part of the API semantics expected by clients and that we cannot easily change.

Whether we want anything beyond this, we can discuss.

It looks like there is still room for improvement. It looks like we should protect requests to the discovery endpoint /apis too. The restmapper used by the GC might get incomplete discovery info (missing Kinds).

This could be fixed as proposed in the KEP by rejecting requests with a 503 response when the server hasn't become ready (/readyz) for some controllers.

@p0lyn0mial
Copy link
Contributor Author

This could be fixed as proposed in the KEP by rejecting requests with a 503 response when the server hasn't become ready (/readyz) for some controllers.

or we could open a new PR that would simply reject (503) requests made by the GC controller to the discovery API when it hasn't observed all GVK

@sttts
Copy link
Contributor

sttts commented Oct 26, 2021

#104748 (comment) will follow for the remaining bit. For now this clearly improves the situation.

/lgtm
/approve

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Oct 26, 2021
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: p0lyn0mial, sttts

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 /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Oct 26, 2021
@k8s-ci-robot k8s-ci-robot merged commit 7c53095 into kubernetes:master Oct 26, 2021
@k8s-ci-robot k8s-ci-robot added this to the v1.23 milestone Oct 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. area/apiserver cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. kind/bug Categorizes issue or PR as related to a bug. lgtm "Looks good to me", indicates that a PR is ready to be merged. priority/important-soon Must be staffed and worked on either currently, or very soon, ideally in time for the next release. release-note Denotes a PR that will be considered when it comes time to generate release notes. sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. triage/accepted Indicates an issue or PR is ready to be actively worked on.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GC might delete objects during kube-apiserver startup
7 participants