Better messaging when GKE certificate signing fails.

On errors, the GKE signing API can respond with a JSON body that
contains an error message explaining the failure. If we're able to
extract it, use that message when reporting the error instead of the
generic error returned by the webhook library. Also, always add an event
to the CSR object on signing errors.
This commit is contained in:
Jacob Beacham
2017-03-21 19:44:33 -07:00
parent 3575348733
commit b889fb3566
4 changed files with 40 additions and 4 deletions

View File

@@ -31,6 +31,7 @@ go_library(
"//vendor:k8s.io/apimachinery/pkg/runtime/schema", "//vendor:k8s.io/apimachinery/pkg/runtime/schema",
"//vendor:k8s.io/apiserver/pkg/util/webhook", "//vendor:k8s.io/apiserver/pkg/util/webhook",
"//vendor:k8s.io/client-go/kubernetes/typed/core/v1", "//vendor:k8s.io/client-go/kubernetes/typed/core/v1",
"//vendor:k8s.io/client-go/pkg/api/v1",
"//vendor:k8s.io/client-go/plugin/pkg/client/auth", "//vendor:k8s.io/client-go/plugin/pkg/client/auth",
"//vendor:k8s.io/client-go/rest", "//vendor:k8s.io/client-go/rest",
"//vendor:k8s.io/client-go/tools/clientcmd", "//vendor:k8s.io/client-go/tools/clientcmd",
@@ -56,5 +57,8 @@ go_test(
srcs = ["gke_signer_test.go"], srcs = ["gke_signer_test.go"],
library = ":go_default_library", library = ":go_default_library",
tags = ["automanaged"], tags = ["automanaged"],
deps = ["//pkg/apis/certificates/v1beta1:go_default_library"], deps = [
"//pkg/apis/certificates/v1beta1:go_default_library",
"//vendor:k8s.io/client-go/tools/record",
],
) )

View File

@@ -22,9 +22,11 @@ import (
"time" "time"
v1core "k8s.io/client-go/kubernetes/typed/core/v1" v1core "k8s.io/client-go/kubernetes/typed/core/v1"
clientv1 "k8s.io/client-go/pkg/api/v1"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/record" "k8s.io/client-go/tools/record"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/clientset_generated/clientset" "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/externalversions" informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/externalversions"
"k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/controller"
@@ -63,13 +65,14 @@ func Run(s *GKECertificatesController) error {
eventBroadcaster := record.NewBroadcaster() eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(glog.Infof) eventBroadcaster.StartLogging(glog.Infof)
eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: v1core.New(kubeClient.Core().RESTClient()).Events("")}) eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: v1core.New(kubeClient.Core().RESTClient()).Events("")})
recorder := eventBroadcaster.NewRecorder(api.Scheme, clientv1.EventSource{Component: "gke-certificates-controller"})
clientBuilder := controller.SimpleControllerClientBuilder{ClientConfig: kubeconfig} clientBuilder := controller.SimpleControllerClientBuilder{ClientConfig: kubeconfig}
client := clientBuilder.ClientOrDie("certificate-controller") client := clientBuilder.ClientOrDie("certificate-controller")
sharedInformers := informers.NewSharedInformerFactory(client, time.Duration(12)*time.Hour) sharedInformers := informers.NewSharedInformerFactory(client, time.Duration(12)*time.Hour)
signer, err := NewGKESigner(s.ClusterSigningGKEKubeconfig, s.ClusterSigningGKERetryBackoff.Duration) signer, err := NewGKESigner(s.ClusterSigningGKEKubeconfig, s.ClusterSigningGKERetryBackoff.Duration, recorder)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -17,6 +17,7 @@ limitations under the License.
package app package app
import ( import (
"encoding/json"
"fmt" "fmt"
"time" "time"
@@ -25,6 +26,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/util/webhook" "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
_ "k8s.io/kubernetes/pkg/apis/certificates/install" _ "k8s.io/kubernetes/pkg/apis/certificates/install"
certificates "k8s.io/kubernetes/pkg/apis/certificates/v1beta1" certificates "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
@@ -40,10 +42,11 @@ type GKESigner struct {
webhook *webhook.GenericWebhook webhook *webhook.GenericWebhook
kubeConfigFile string kubeConfigFile string
retryBackoff time.Duration retryBackoff time.Duration
recorder record.EventRecorder
} }
// NewGKESigner will create a new instance of a GKESigner. // NewGKESigner will create a new instance of a GKESigner.
func NewGKESigner(kubeConfigFile string, retryBackoff time.Duration) (*GKESigner, error) { func NewGKESigner(kubeConfigFile string, retryBackoff time.Duration, recorder record.EventRecorder) (*GKESigner, error) {
webhook, err := webhook.NewGenericWebhook(api.Registry, api.Codecs, kubeConfigFile, groupVersions, retryBackoff) webhook, err := webhook.NewGenericWebhook(api.Registry, api.Codecs, kubeConfigFile, groupVersions, retryBackoff)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -53,6 +56,7 @@ func NewGKESigner(kubeConfigFile string, retryBackoff time.Duration) (*GKESigner
webhook: webhook, webhook: webhook,
kubeConfigFile: kubeConfigFile, kubeConfigFile: kubeConfigFile,
retryBackoff: retryBackoff, retryBackoff: retryBackoff,
recorder: recorder,
}, nil }, nil
} }
@@ -65,6 +69,9 @@ func (s *GKESigner) Sign(csr *certificates.CertificateSigningRequest) (*certific
}) })
if err := result.Error(); err != nil { if err := result.Error(); err != nil {
if bodyErr := s.resultBodyError(result); bodyErr != nil {
return nil, s.webhookError(csr, bodyErr)
}
return nil, s.webhookError(csr, err) return nil, s.webhookError(csr, err)
} }
@@ -86,5 +93,26 @@ func (s *GKESigner) Sign(csr *certificates.CertificateSigningRequest) (*certific
func (s *GKESigner) webhookError(csr *certificates.CertificateSigningRequest, err error) error { func (s *GKESigner) webhookError(csr *certificates.CertificateSigningRequest, err error) error {
glog.V(2).Infof("error contacting webhook backend: %s", err) glog.V(2).Infof("error contacting webhook backend: %s", err)
s.recorder.Eventf(csr, "Warning", "SigningError", "error while calling GKE: %v", err)
return err return err
} }
// signResultError represents the structured response body of a failed call to
// GKE's SignCertificate API.
type signResultError struct {
Error struct {
Code int
Message string
Status string
}
}
// resultBodyError attempts to extract an error out of a response body.
func (s *GKESigner) resultBodyError(result rest.Result) error {
body, _ := result.Raw()
var sre signResultError
if err := json.Unmarshal(body, &sre); err == nil {
return fmt.Errorf("server responded with error: %s", sre.Error.Message)
}
return nil
}

View File

@@ -26,6 +26,7 @@ import (
"text/template" "text/template"
"time" "time"
"k8s.io/client-go/tools/record"
certificates "k8s.io/kubernetes/pkg/apis/certificates/v1beta1" certificates "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
) )
@@ -103,7 +104,7 @@ func TestGKESigner(t *testing.T) {
t.Fatalf("error closing kubeconfig template: %v", err) t.Fatalf("error closing kubeconfig template: %v", err)
} }
signer, err := NewGKESigner(kubeConfig.Name(), time.Duration(500)*time.Millisecond) signer, err := NewGKESigner(kubeConfig.Name(), time.Duration(500)*time.Millisecond, record.NewFakeRecorder(10))
if err != nil { if err != nil {
t.Fatalf("error creating GKESigner: %v", err) t.Fatalf("error creating GKESigner: %v", err)
} }