impersonator_config.go: refactor to clean up cert name handling

This commit is contained in:
Ryan Richard 2021-03-03 09:22:35 -08:00
parent d3599c541b
commit 730092f39c
4 changed files with 79 additions and 73 deletions

View File

@ -41,6 +41,10 @@ type Config struct {
Endpoint string `json:"endpoint,omitempty"`
}
func (c *Config) HasEndpoint() bool {
return c.Endpoint != ""
}
func NewConfig() *Config {
return &Config{Mode: ModeAuto}
}

View File

@ -18,6 +18,13 @@ func TestNewConfig(t *testing.T) {
require.Equal(t, &Config{Mode: ModeAuto}, NewConfig())
}
func TestHasEndpoint(t *testing.T) {
configWithoutEndpoint := Config{}
configWithEndpoint := Config{Endpoint: "something"}
require.False(t, configWithoutEndpoint.HasEndpoint())
require.True(t, configWithEndpoint.HasEndpoint())
}
func TestConfigFromConfigMap(t *testing.T) {
tests := []struct {
name string

View File

@ -170,6 +170,22 @@ func (c *impersonatorConfigController) Sync(syncCtx controllerlib.Context) error
return err
}
type certNameInfo struct {
// ready will be true when the certificate name information is known.
// ready will be false when it is pending because we are waiting for a load balancer to get assigned an ip/hostname.
// When false, the other fields in this struct should not be considered meaningful and may be zero values.
ready bool
// The IP address or hostname which was selected to be used as the name in the cert.
// Either selectedIP or selectedHostname will be set, but not both.
selectedIP net.IP
selectedHostname string
// The name of the endpoint to which a client should connect to talk to the impersonator.
// This may be a hostname or an IP, and may include a port number.
clientEndpoint string
}
func (c *impersonatorConfigController) doSync(ctx context.Context) (*v1alpha1.CredentialIssuerStrategy, error) {
config, err := c.loadImpersonationProxyConfiguration()
if err != nil {
@ -209,20 +225,24 @@ func (c *impersonatorConfigController) doSync(ctx context.Context) (*v1alpha1.Cr
}
}
waitingForLoadBalancer := false
nameInfo, err := c.findDesiredTLSCertificateName(config)
if err != nil {
return nil, err
}
var impersonationCA *certauthority.CA
if c.shouldHaveTLSSecret(config) {
if impersonationCA, err = c.ensureCASecretIsCreated(ctx); err != nil {
return nil, err
}
if waitingForLoadBalancer, err = c.ensureTLSSecret(ctx, config, impersonationCA); err != nil {
if err = c.ensureTLSSecret(ctx, nameInfo, impersonationCA); err != nil {
return nil, err
}
} else if err = c.ensureTLSSecretIsRemoved(ctx); err != nil {
return nil, err
}
return c.doSyncResult(waitingForLoadBalancer, config, impersonationCA), nil
return c.doSyncResult(nameInfo, config, impersonationCA), nil
}
func (c *impersonatorConfigController) loadImpersonationProxyConfiguration() (*impersonator.Config, error) {
@ -270,7 +290,7 @@ func (c *impersonatorConfigController) disabledExplicitly(config *impersonator.C
}
func (c *impersonatorConfigController) shouldHaveLoadBalancer(config *impersonator.Config) bool {
return c.shouldHaveImpersonator(config) && config.Endpoint == ""
return c.shouldHaveImpersonator(config) && !config.HasEndpoint()
}
func (c *impersonatorConfigController) shouldHaveTLSSecret(config *impersonator.Config) bool {
@ -400,17 +420,17 @@ func (c *impersonatorConfigController) ensureLoadBalancerIsStopped(ctx context.C
return c.k8sClient.CoreV1().Services(c.namespace).Delete(ctx, c.generatedLoadBalancerServiceName, metav1.DeleteOptions{})
}
func (c *impersonatorConfigController) ensureTLSSecret(ctx context.Context, config *impersonator.Config, ca *certauthority.CA) (bool, error) {
func (c *impersonatorConfigController) ensureTLSSecret(ctx context.Context, nameInfo *certNameInfo, ca *certauthority.CA) error {
secretFromInformer, err := c.secretsInformer.Lister().Secrets(c.namespace).Get(c.tlsSecretName)
notFound := k8serrors.IsNotFound(err)
if !notFound && err != nil {
return false, err
return err
}
if !notFound {
secretWasDeleted, err := c.deleteTLSSecretWhenCertificateDoesNotMatchDesiredState(ctx, config, ca, secretFromInformer)
secretWasDeleted, err := c.deleteTLSSecretWhenCertificateDoesNotMatchDesiredState(ctx, nameInfo, ca, secretFromInformer)
if err != nil {
return false, err
return err
}
// If it was deleted by the above call, then set it to nil. This allows us to avoid waiting
// for the informer cache to update before deciding to proceed to create the new Secret below.
@ -419,10 +439,10 @@ func (c *impersonatorConfigController) ensureTLSSecret(ctx context.Context, conf
}
}
return c.ensureTLSSecretIsCreatedAndLoaded(ctx, config, secretFromInformer, ca)
return c.ensureTLSSecretIsCreatedAndLoaded(ctx, nameInfo, secretFromInformer, ca)
}
func (c *impersonatorConfigController) deleteTLSSecretWhenCertificateDoesNotMatchDesiredState(ctx context.Context, config *impersonator.Config, ca *certauthority.CA, secret *v1.Secret) (bool, error) {
func (c *impersonatorConfigController) deleteTLSSecretWhenCertificateDoesNotMatchDesiredState(ctx context.Context, nameInfo *certNameInfo, ca *certauthority.CA, secret *v1.Secret) (bool, error) {
certPEM := secret.Data[v1.TLSCertKey]
block, _ := pem.Decode(certPEM)
if block == nil {
@ -471,11 +491,7 @@ func (c *impersonatorConfigController) deleteTLSSecretWhenCertificateDoesNotMatc
return true, nil
}
desiredIP, desiredHostname, nameIsReady, err := c.findDesiredTLSCertificateName(config)
if err != nil {
return false, err
}
if !nameIsReady {
if !nameInfo.ready {
// We currently have a secret but we are waiting for a load balancer to be assigned an ingress, so
// our current secret must be old/unwanted.
if err = c.ensureTLSSecretIsRemoved(ctx); err != nil {
@ -487,14 +503,14 @@ func (c *impersonatorConfigController) deleteTLSSecretWhenCertificateDoesNotMatc
actualIPs := actualCertFromSecret.IPAddresses
actualHostnames := actualCertFromSecret.DNSNames
plog.Info("Checking TLS certificate names",
"desiredIP", desiredIP,
"desiredHostname", desiredHostname,
"desiredIP", nameInfo.selectedIP,
"desiredHostname", nameInfo.selectedHostname,
"actualIPs", actualIPs,
"actualHostnames", actualHostnames,
"secret", c.tlsSecretName,
"namespace", c.namespace)
if certHostnameAndIPMatchDesiredState(desiredIP, actualIPs, desiredHostname, actualHostnames) {
if certHostnameAndIPMatchDesiredState(nameInfo.selectedIP, actualIPs, nameInfo.selectedHostname, actualHostnames) {
// The cert already matches the desired state, so there is no need to delete/recreate it.
return false, nil
}
@ -515,36 +531,30 @@ func certHostnameAndIPMatchDesiredState(desiredIP net.IP, actualIPs []net.IP, de
return false
}
func (c *impersonatorConfigController) ensureTLSSecretIsCreatedAndLoaded(ctx context.Context, config *impersonator.Config, secret *v1.Secret, ca *certauthority.CA) (bool, error) {
func (c *impersonatorConfigController) ensureTLSSecretIsCreatedAndLoaded(ctx context.Context, nameInfo *certNameInfo, secret *v1.Secret, ca *certauthority.CA) error {
if secret != nil {
err := c.loadTLSCertFromSecret(secret)
if err != nil {
return false, err
return err
}
return false, nil
return nil
}
ip, hostname, nameIsReady, err := c.findDesiredTLSCertificateName(config)
if err != nil {
return false, err
}
if !nameIsReady {
// Sync will get called again when the load balancer is updated with its ingress info, so this is not an error.
// Return "true" meaning that we are waiting for the load balancer.
return true, nil
if !nameInfo.ready {
return nil
}
newTLSSecret, err := c.createNewTLSSecret(ctx, ca, ip, hostname)
newTLSSecret, err := c.createNewTLSSecret(ctx, ca, nameInfo.selectedIP, nameInfo.selectedHostname)
if err != nil {
return false, err
return err
}
err = c.loadTLSCertFromSecret(newTLSSecret)
if err != nil {
return false, err
return err
}
return false, nil
return nil
}
func (c *impersonatorConfigController) ensureCASecretIsCreated(ctx context.Context) (*certauthority.CA, error) {
@ -602,55 +612,55 @@ func (c *impersonatorConfigController) createCASecret(ctx context.Context) (*cer
return impersonationCA, nil
}
func (c *impersonatorConfigController) findDesiredTLSCertificateName(config *impersonator.Config) (net.IP, string, bool, error) {
if config.Endpoint != "" {
func (c *impersonatorConfigController) findDesiredTLSCertificateName(config *impersonator.Config) (*certNameInfo, error) {
if config.HasEndpoint() {
return c.findTLSCertificateNameFromEndpointConfig(config)
}
return c.findTLSCertificateNameFromLoadBalancer()
}
func (c *impersonatorConfigController) findTLSCertificateNameFromEndpointConfig(config *impersonator.Config) (net.IP, string, bool, error) {
endpointWithoutPort := strings.Split(config.Endpoint, ":")[0]
func (c *impersonatorConfigController) findTLSCertificateNameFromEndpointConfig(config *impersonator.Config) (*certNameInfo, error) {
endpointMaybeWithPort := config.Endpoint
endpointWithoutPort := strings.Split(endpointMaybeWithPort, ":")[0]
parsedAsIP := net.ParseIP(endpointWithoutPort)
if parsedAsIP != nil {
return parsedAsIP, "", true, nil
return &certNameInfo{ready: true, selectedIP: parsedAsIP, clientEndpoint: endpointMaybeWithPort}, nil
}
return nil, endpointWithoutPort, true, nil
return &certNameInfo{ready: true, selectedHostname: endpointWithoutPort, clientEndpoint: endpointMaybeWithPort}, nil
}
func (c *impersonatorConfigController) findTLSCertificateNameFromLoadBalancer() (net.IP, string, bool, error) {
func (c *impersonatorConfigController) findTLSCertificateNameFromLoadBalancer() (*certNameInfo, error) {
lb, err := c.servicesInformer.Lister().Services(c.namespace).Get(c.generatedLoadBalancerServiceName)
notFound := k8serrors.IsNotFound(err)
if notFound {
// Although we created the load balancer, maybe it hasn't been cached in the informer yet.
// We aren't ready and will try again later in this case.
return nil, "", false, nil
return &certNameInfo{ready: false}, nil
}
if err != nil {
return nil, "", false, err
return nil, err
}
ingresses := lb.Status.LoadBalancer.Ingress
if len(ingresses) == 0 || (ingresses[0].Hostname == "" && ingresses[0].IP == "") {
plog.Info("load balancer for impersonation proxy does not have an ingress yet, so skipping tls cert generation while we wait",
"service", c.generatedLoadBalancerServiceName,
"namespace", c.namespace)
return nil, "", false, nil
return &certNameInfo{ready: false}, nil
}
for _, ingress := range ingresses {
hostname := ingress.Hostname
if hostname != "" {
return nil, hostname, true, nil
return &certNameInfo{ready: true, selectedHostname: hostname, clientEndpoint: hostname}, nil
}
}
for _, ingress := range ingresses {
ip := ingress.IP
parsedIP := net.ParseIP(ip)
if parsedIP != nil {
return parsedIP, "", true, nil
return &certNameInfo{ready: true, selectedIP: parsedIP, clientEndpoint: ip}, nil
}
}
return nil, "", false, fmt.Errorf("could not find valid IP addresses or hostnames from load balancer %s/%s", c.namespace, lb.Name)
return nil, fmt.Errorf("could not find valid IP addresses or hostnames from load balancer %s/%s", c.namespace, lb.Name)
}
func (c *impersonatorConfigController) createNewTLSSecret(ctx context.Context, ca *certauthority.CA, ip net.IP, hostname string) (*v1.Secret, error) {
@ -731,16 +741,8 @@ func (c *impersonatorConfigController) ensureTLSSecretIsRemoved(ctx context.Cont
return nil
}
func (c *impersonatorConfigController) doSyncResult(waitingForLoadBalancer bool, config *impersonator.Config, ca *certauthority.CA) *v1alpha1.CredentialIssuerStrategy {
func (c *impersonatorConfigController) doSyncResult(nameInfo *certNameInfo, config *impersonator.Config, ca *certauthority.CA) *v1alpha1.CredentialIssuerStrategy {
switch {
case waitingForLoadBalancer:
return &v1alpha1.CredentialIssuerStrategy{
Type: v1alpha1.ImpersonationProxyStrategyType,
Status: v1alpha1.ErrorStrategyStatus,
Reason: v1alpha1.PendingStrategyReason,
Message: "waiting for load balancer Service to be assigned IP or hostname",
LastUpdateTime: metav1.NewTime(c.clock.Now()),
}
case c.disabledExplicitly(config):
return &v1alpha1.CredentialIssuerStrategy{
Type: v1alpha1.ImpersonationProxyStrategyType,
@ -757,21 +759,15 @@ func (c *impersonatorConfigController) doSyncResult(waitingForLoadBalancer bool,
Message: "automatically determined that impersonation proxy should be disabled",
LastUpdateTime: metav1.NewTime(c.clock.Now()),
}
default:
var endpointName string
if config.Endpoint != "" {
endpointName = config.Endpoint
} else {
desiredIP, desiredHostname, _, _ := c.findTLSCertificateNameFromLoadBalancer()
switch {
case desiredIP != nil:
endpointName = desiredIP.String()
case desiredHostname != "":
endpointName = desiredHostname
default:
endpointName = "" // this shouldn't actually happen in practice
}
case !nameInfo.ready:
return &v1alpha1.CredentialIssuerStrategy{
Type: v1alpha1.ImpersonationProxyStrategyType,
Status: v1alpha1.ErrorStrategyStatus,
Reason: v1alpha1.PendingStrategyReason,
Message: "waiting for load balancer Service to be assigned IP or hostname",
LastUpdateTime: metav1.NewTime(c.clock.Now()),
}
default:
return &v1alpha1.CredentialIssuerStrategy{
Type: v1alpha1.ImpersonationProxyStrategyType,
Status: v1alpha1.SuccessStrategyStatus,
@ -781,7 +777,7 @@ func (c *impersonatorConfigController) doSyncResult(waitingForLoadBalancer bool,
Frontend: &v1alpha1.CredentialIssuerFrontend{
Type: v1alpha1.ImpersonationProxyFrontendType,
ImpersonationProxyInfo: &v1alpha1.ImpersonationProxyInfo{
Endpoint: "https://" + endpointName,
Endpoint: "https://" + nameInfo.clientEndpoint,
CertificateAuthorityData: base64.StdEncoding.EncodeToString(ca.Bundle()),
},
},

View File

@ -942,9 +942,8 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
it("does not start the load balancer automatically", func() {
requireTLSServerIsRunningWithoutCerts()
r.Len(kubeAPIClient.Actions(), 2)
r.Len(kubeAPIClient.Actions(), 1)
requireNodesListed(kubeAPIClient.Actions()[0])
requireCASecretWasCreated(kubeAPIClient.Actions()[1])
requireCredentialIssuer(newErrorStrategy("could not find valid IP addresses or hostnames from load balancer some-namespace/some-service-resource-name"))
})
})