6b90dc8bb7
The rotation is forced by a new controller that deletes the serving cert secret, as other controllers will see this deletion and ensure that a new serving cert is created. Note that the integration tests now have an addition worst case runtime of 60 seconds. This is because of the way that the aggregated API server code reloads certificates. We will fix this in a future story. Then, the integration tests should hopefully get much faster. Signed-off-by: Andrew Keesler <akeesler@vmware.com>
266 lines
8.6 KiB
Go
266 lines
8.6 KiB
Go
/*
|
|
Copyright 2020 VMware, Inc.
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
// Package server is the command line entry point for placeholder-name-server.
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
|
genericoptions "k8s.io/apiserver/pkg/server/options"
|
|
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
|
|
"k8s.io/client-go/kubernetes"
|
|
restclient "k8s.io/client-go/rest"
|
|
|
|
"github.com/suzerain-io/placeholder-name/internal/apiserver"
|
|
"github.com/suzerain-io/placeholder-name/internal/certauthority/kubecertauthority"
|
|
"github.com/suzerain-io/placeholder-name/internal/constable"
|
|
"github.com/suzerain-io/placeholder-name/internal/controllermanager"
|
|
"github.com/suzerain-io/placeholder-name/internal/downward"
|
|
"github.com/suzerain-io/placeholder-name/internal/provider"
|
|
"github.com/suzerain-io/placeholder-name/internal/registry/credentialrequest"
|
|
placeholderv1alpha1 "github.com/suzerain-io/placeholder-name/kubernetes/1.19/api/apis/placeholder/v1alpha1"
|
|
"github.com/suzerain-io/placeholder-name/pkg/config"
|
|
)
|
|
|
|
type percentageValue struct {
|
|
percentage float32
|
|
}
|
|
|
|
var _ pflag.Value = &percentageValue{}
|
|
|
|
func (p *percentageValue) String() string {
|
|
return fmt.Sprintf("%.2f%%", p.percentage*100)
|
|
}
|
|
|
|
func (p *percentageValue) Set(s string) error {
|
|
f, err := strconv.ParseFloat(s, 32)
|
|
if err != nil || f < 0 || f > 1 {
|
|
return constable.Error("must pass real number between 0 and 1")
|
|
}
|
|
|
|
p.percentage = float32(f)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *percentageValue) Type() string {
|
|
return "percentage"
|
|
}
|
|
|
|
// App is an object that represents the placeholder-name-server application.
|
|
type App struct {
|
|
cmd *cobra.Command
|
|
|
|
// CLI flags
|
|
configPath string
|
|
downwardAPIPath string
|
|
servingCertRotationThreshold percentageValue
|
|
}
|
|
|
|
// This is ignored for now because we turn off etcd storage below, but this is
|
|
// the right prefix in case we turn it back on.
|
|
const defaultEtcdPathPrefix = "/registry/" + placeholderv1alpha1.GroupName
|
|
|
|
// New constructs a new App with command line args, stdout and stderr.
|
|
func New(ctx context.Context, args []string, stdout, stderr io.Writer) *App {
|
|
app := &App{}
|
|
app.addServerCommand(ctx, args, stdout, stderr)
|
|
return app
|
|
}
|
|
|
|
// Run the server.
|
|
func (a *App) Run() error {
|
|
return a.cmd.Execute()
|
|
}
|
|
|
|
// Create the server command and save it into the App.
|
|
func (a *App) addServerCommand(ctx context.Context, args []string, stdout, stderr io.Writer) {
|
|
cmd := &cobra.Command{
|
|
Use: `placeholder-name-server`,
|
|
Long: "placeholder-name-server provides a generic API for mapping an external\n" +
|
|
"credential from somewhere to an internal credential to be used for\n" +
|
|
"authenticating to the Kubernetes API.",
|
|
RunE: func(cmd *cobra.Command, args []string) error { return a.runServer(ctx) },
|
|
Args: cobra.NoArgs,
|
|
}
|
|
|
|
cmd.SetArgs(args)
|
|
cmd.SetOut(stdout)
|
|
cmd.SetErr(stderr)
|
|
addCommandlineFlagsToCommand(cmd, a)
|
|
|
|
a.cmd = cmd
|
|
}
|
|
|
|
// Define the app's commandline flags.
|
|
func addCommandlineFlagsToCommand(cmd *cobra.Command, app *App) {
|
|
cmd.Flags().StringVarP(
|
|
&app.configPath,
|
|
"config",
|
|
"c",
|
|
"placeholder-name.yaml",
|
|
"path to configuration file",
|
|
)
|
|
|
|
cmd.Flags().StringVar(
|
|
&app.downwardAPIPath,
|
|
"downward-api-path",
|
|
"/etc/podinfo",
|
|
"path to Downward API volume mount",
|
|
)
|
|
|
|
app.servingCertRotationThreshold.percentage = .70 // default
|
|
cmd.Flags().Var(
|
|
&app.servingCertRotationThreshold,
|
|
"serving-cert-rotation-threshold",
|
|
"real number between 0 and 1 indicating percentage of lifetime before rotation of serving cert",
|
|
)
|
|
}
|
|
|
|
// Boot the aggregated API server, which will in turn boot the controllers.
|
|
func (a *App) runServer(ctx context.Context) error {
|
|
// Read the server config file.
|
|
cfg, err := config.FromPath(a.configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("could not load config: %w", err)
|
|
}
|
|
|
|
// Load the Kubernetes cluster signing CA.
|
|
k8sClusterCA, shutdownCA, err := getClusterCASigner()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer shutdownCA()
|
|
|
|
// Create a WebhookTokenAuthenticator.
|
|
webhookTokenAuthenticator, err := config.NewWebhook(cfg.WebhookConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create webhook client: %w", err)
|
|
}
|
|
|
|
// Discover in which namespace we are installed.
|
|
podInfo, err := downward.Load(a.downwardAPIPath)
|
|
if err != nil {
|
|
return fmt.Errorf("could not read pod metadata: %w", err)
|
|
}
|
|
serverInstallationNamespace := podInfo.Namespace
|
|
|
|
// This cert provider will provide certs to the API server and will
|
|
// be mutated by a controller to keep the certs up to date with what
|
|
// is stored in a k8s Secret. Therefore it also effectively acting as
|
|
// an in-memory cache of what is stored in the k8s Secret, helping to
|
|
// keep incoming requests fast.
|
|
dynamicCertProvider := provider.NewDynamicTLSServingCertProvider()
|
|
|
|
// Prepare to start the controllers, but defer actually starting them until the
|
|
// post start hook of the aggregated API server.
|
|
startControllersFunc, err := controllermanager.PrepareControllers(
|
|
serverInstallationNamespace,
|
|
cfg.DiscoveryConfig.URL,
|
|
dynamicCertProvider,
|
|
a.servingCertRotationThreshold.percentage,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not prepare controllers: %w", err)
|
|
}
|
|
|
|
// Get the aggregated API server config.
|
|
aggregatedAPIServerConfig, err := getAggregatedAPIServerConfig(
|
|
dynamicCertProvider,
|
|
webhookTokenAuthenticator,
|
|
k8sClusterCA,
|
|
startControllersFunc,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not configure aggregated API server: %w", err)
|
|
}
|
|
|
|
// Complete the aggregated API server config and make a server instance.
|
|
server, err := aggregatedAPIServerConfig.Complete().New()
|
|
if err != nil {
|
|
return fmt.Errorf("could not create aggregated API server: %w", err)
|
|
}
|
|
|
|
// Run the server. Its post-start hook will start the controllers.
|
|
return server.GenericAPIServer.PrepareRun().Run(ctx.Done())
|
|
}
|
|
|
|
func getClusterCASigner() (*kubecertauthority.CA, kubecertauthority.ShutdownFunc, error) {
|
|
// Load the Kubernetes client configuration.
|
|
kubeConfig, err := restclient.InClusterConfig()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not load in-cluster configuration: %w", err)
|
|
}
|
|
|
|
// Connect to the core Kubernetes API.
|
|
kubeClient, err := kubernetes.NewForConfig(kubeConfig)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not initialize Kubernetes client: %w", err)
|
|
}
|
|
|
|
// Make a clock tick that triggers a periodic refresh.
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
|
|
// Make a CA which uses the Kubernetes cluster API server's signing certs.
|
|
k8sClusterCA, shutdownCA, err := kubecertauthority.New(
|
|
kubeClient,
|
|
kubecertauthority.NewPodCommandExecutor(kubeConfig, kubeClient),
|
|
ticker.C,
|
|
)
|
|
if err != nil {
|
|
ticker.Stop()
|
|
return nil, nil, fmt.Errorf("could not load cluster signing CA: %w", err)
|
|
}
|
|
return k8sClusterCA, func() { shutdownCA(); ticker.Stop() }, nil
|
|
}
|
|
|
|
// Create a configuration for the aggregated API server.
|
|
func getAggregatedAPIServerConfig(
|
|
dynamicCertProvider provider.DynamicTLSServingCertProvider,
|
|
webhookTokenAuthenticator *webhook.WebhookTokenAuthenticator,
|
|
ca credentialrequest.CertIssuer,
|
|
startControllersPostStartHook func(context.Context),
|
|
) (*apiserver.Config, error) {
|
|
recommendedOptions := genericoptions.NewRecommendedOptions(
|
|
defaultEtcdPathPrefix,
|
|
apiserver.Codecs.LegacyCodec(placeholderv1alpha1.SchemeGroupVersion),
|
|
// TODO we should check to see if all the other default settings are acceptable for us
|
|
)
|
|
recommendedOptions.Etcd = nil // turn off etcd storage because we don't need it yet
|
|
recommendedOptions.SecureServing.ServerCert.GeneratedCert = dynamicCertProvider
|
|
|
|
serverConfig := genericapiserver.NewRecommendedConfig(apiserver.Codecs)
|
|
// Note that among other things, this ApplyTo() function copies
|
|
// `recommendedOptions.SecureServing.ServerCert.GeneratedCert` into
|
|
// `serverConfig.SecureServing.Cert` thus making `dynamicCertProvider`
|
|
// the cert provider for the running server. The provider will be called
|
|
// by the API machinery periodically. When the provider returns nil certs,
|
|
// the API server will return "the server is currently unable to
|
|
// handle the request" error responses for all incoming requests.
|
|
// If the provider later starts returning certs, then the API server
|
|
// will use them to handle the incoming requests successfully.
|
|
if err := recommendedOptions.ApplyTo(serverConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
apiServerConfig := &apiserver.Config{
|
|
GenericConfig: serverConfig,
|
|
ExtraConfig: apiserver.ExtraConfig{
|
|
Webhook: webhookTokenAuthenticator,
|
|
Issuer: ca,
|
|
StartControllersPostStartHook: startControllersPostStartHook,
|
|
},
|
|
}
|
|
return apiServerConfig, nil
|
|
}
|