-
Notifications
You must be signed in to change notification settings - Fork 1.2k
✨ Export HTTP server manager runnable implementation #2473
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,41 +21,89 @@ import ( | |
"errors" | ||
"net" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/go-logr/logr" | ||
crlog "sigs.k8s.io/controller-runtime/pkg/log" | ||
) | ||
|
||
// server is a general purpose HTTP server Runnable for a manager | ||
// to serve some internal handlers such as health probes, metrics and profiling. | ||
type server struct { | ||
Kind string | ||
Log logr.Logger | ||
Server *http.Server | ||
var ( | ||
_ Runnable = (*Server)(nil) | ||
_ LeaderElectionRunnable = (*Server)(nil) | ||
) | ||
|
||
// Server is a general purpose HTTP server Runnable for a manager. | ||
// It is used to serve some internal handlers for health probes and profiling, | ||
// but it can also be used to run custom servers. | ||
type Server struct { | ||
// Name is an optional string that describes the purpose of the server. It is used in logs to distinguish | ||
// among multiple servers. | ||
Name string | ||
|
||
// Server is the HTTP server to run. It is required. | ||
Server *http.Server | ||
|
||
// Listener is an optional listener to use. If not set, the server start a listener using the server.Addr. | ||
// Using a listener is useful when the port reservation needs to happen in advance of this runnable starting. | ||
Listener net.Listener | ||
|
||
// OnlyServeWhenLeader is an optional bool that indicates that the server should only be started when the manager is the leader. | ||
OnlyServeWhenLeader bool | ||
|
||
// ShutdownTimeout is an optional duration that indicates how long to wait for the server to shutdown gracefully. If not set, | ||
// the server will wait indefinitely for all connections to close. | ||
ShutdownTimeout *time.Duration | ||
} | ||
|
||
func (s *server) Start(ctx context.Context) error { | ||
log := s.Log.WithValues("kind", s.Kind, "addr", s.Listener.Addr()) | ||
// Start starts the server. It will block until the server is stopped or an error occurs. | ||
func (s *Server) Start(ctx context.Context) error { | ||
log := crlog.FromContext(ctx) | ||
if s.Name != "" { | ||
log = log.WithValues("name", s.Name) | ||
} | ||
log = log.WithValues("addr", s.addr()) | ||
|
||
serverShutdown := make(chan struct{}) | ||
go func() { | ||
<-ctx.Done() | ||
log.Info("shutting down server") | ||
if err := s.Server.Shutdown(context.Background()); err != nil { | ||
|
||
shutdownCtx := context.Background() | ||
if s.ShutdownTimeout != nil { | ||
var shutdownCancel context.CancelFunc | ||
shutdownCtx, shutdownCancel = context.WithTimeout(context.Background(), *s.ShutdownTimeout) | ||
defer shutdownCancel() | ||
} | ||
|
||
if err := s.Server.Shutdown(shutdownCtx); err != nil { | ||
log.Error(err, "error shutting down server") | ||
} | ||
close(serverShutdown) | ||
}() | ||
|
||
log.Info("starting server") | ||
if err := s.Server.Serve(s.Listener); err != nil && !errors.Is(err, http.ErrServerClosed) { | ||
if err := s.serve(); err != nil && !errors.Is(err, http.ErrServerClosed) { | ||
return err | ||
} | ||
|
||
<-serverShutdown | ||
return nil | ||
} | ||
|
||
func (s *server) NeedLeaderElection() bool { | ||
return false | ||
// NeedLeaderElection returns true if the server should only be started when the manager is the leader. | ||
func (s *Server) NeedLeaderElection() bool { | ||
return s.OnlyServeWhenLeader | ||
} | ||
Comment on lines
+92
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a use case for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use case I have in mind is when there is a strong coupling between what is being reconciled and what is being served. One example might be an API that represents OCI images, where creating an instance of that API makes the OCI image contents available as a tar archive via the HTTP server. |
||
|
||
func (s *Server) addr() string { | ||
if s.Listener != nil { | ||
return s.Listener.Addr().String() | ||
} | ||
return s.Server.Addr | ||
} | ||
|
||
func (s *Server) serve() error { | ||
if s.Listener != nil { | ||
return s.Server.Serve(s.Listener) | ||
} | ||
return s.Server.ListenAndServe() | ||
} |
Uh oh!
There was an error while loading. Please reload this page.