Define initial config file format

- Users may want to consume pkg/config to generate configuration files.
- This also involved putting config-related utilities in the config
  package for ease of consumption.
- We did not add in versioning into the Config type for now...this is
  something we will likely do in the future, but it is not deemed
  necessary this early in the project.
- The config file format tries to follow the patterns of Kube. One such
  example of this is requiring the use of base64-encoded CA bundle PEM
  bytes instead of a file path. This also slightly simplifies the config
  file handling because we don't have to 1) read in a file or 2) deal
  with the error case of the file not being there.

- The webhook code from k8s.io/apiserver is really exactly what we want
  here. If this dependency gets too burdensome, we can always drop it,
  but the pros outweigh the cons at the moment.
- Writing out a kubeconfig to disk to configure the webhook is a little
  janky, but hopefully this won't hurt performance too much in the year
  2020.

- Also bonus: call the right *Serve*() function when starting our
  servers.

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
This commit is contained in:
Andrew Keesler
2020-07-14 11:50:28 -04:00
parent 5a66b56b93
commit 63f5416b21
11 changed files with 334 additions and 9 deletions
+36 -7
View File
@@ -21,7 +21,10 @@ import (
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"k8s.io/apiserver/pkg/authentication/authenticator"
"github.com/suzerain-io/placeholder-name/internal/certauthority"
"github.com/suzerain-io/placeholder-name/pkg/config"
"github.com/suzerain-io/placeholder-name/pkg/handlers"
)
@@ -38,6 +41,9 @@ type App struct {
// listen address for main serve
mainAddr string
// webhook authenticates tokens
webhook authenticator.Token
// runFunc runs the actual program, after the parsing of flags has been done.
//
// It is mostly a field for the sake of testing.
@@ -86,6 +92,17 @@ func (a *App) Run() error {
}
func (a *App) serve(ctx context.Context, configPath string) error {
cfg, err := config.FromPath(configPath)
if err != nil {
return fmt.Errorf("could not load config: %w", err)
}
webhook, err := config.NewWebhook(cfg.WebhookConfig)
if err != nil {
return fmt.Errorf("could create webhook client: %w", err)
}
a.webhook = webhook
ca, err := certauthority.New(pkix.Name{CommonName: "Placeholder CA"})
if err != nil {
return fmt.Errorf("could not initialize CA: %w", err)
@@ -116,7 +133,7 @@ func (a *App) serve(ctx context.Context, configPath string) error {
Addr: a.healthAddr,
Handler: handlers.New(),
}
return runGracefully(ctx, &server, eg)
return runGracefully(ctx, &server, eg, server.ListenAndServe)
})
// Start main service listener
@@ -129,9 +146,13 @@ func (a *App) serve(ctx context.Context, configPath string) error {
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{*cert},
},
Handler: http.HandlerFunc(exampleHandler),
Handler: http.HandlerFunc(a.exampleHandler),
}
return runGracefully(ctx, &server, eg)
return runGracefully(ctx, &server, eg, func() error {
// Doc for ListenAndServeTLS says we can pass empty strings if we configured
// keypair for TLS in http.Server.TLSConfig.
return server.ListenAndServeTLS("", "")
})
})
if err := eg.Wait(); !errors.Is(err, http.ErrServerClosed) {
@@ -141,14 +162,22 @@ func (a *App) serve(ctx context.Context, configPath string) error {
}
// exampleHandler is a stub to be replaced with our real server logic.
func exampleHandler(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Hello world"))
func (a *App) exampleHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
rsp, authenticated, err := a.webhook.AuthenticateToken(ctx, "")
log.Printf("token response: %+v", rsp)
log.Printf("token authenticated: %+v", authenticated)
log.Printf("token err: %+v", err)
_, _ = w.Write([]byte("hello world"))
}
// runGracefully runs an http.Server with graceful shutdown.
func runGracefully(ctx context.Context, srv *http.Server, eg *errgroup.Group) error {
func runGracefully(ctx context.Context, srv *http.Server, eg *errgroup.Group, f func() error) error {
// Start the listener in a child goroutine.
eg.Go(srv.ListenAndServe)
eg.Go(f)
// If/when the context is canceled or times out, initiate shutting down the serve.
<-ctx.Done()
+2 -2
View File
@@ -97,7 +97,7 @@ func TestServeApp(t *testing.T) {
healthAddr: "127.0.0.1:0",
mainAddr: "127.0.0.1:8443",
}
err := a.serve(ctx, "some/path/to/config.yaml")
err := a.serve(ctx, "testdata/valid-config.yaml")
require.NoError(t, err)
})
@@ -110,7 +110,7 @@ func TestServeApp(t *testing.T) {
healthAddr: "127.0.0.1:8081",
mainAddr: "127.0.0.1:8081",
}
err := a.serve(ctx, "some/path/to/config.yaml")
err := a.serve(ctx, "testdata/valid-config.yaml")
require.EqualError(t, err, "listen tcp 127.0.0.1:8081: bind: address already in use")
})
}
+5
View File
@@ -0,0 +1,5 @@
---
webhook:
url: https://tuna.com/fish?marlin
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tLi4u