plugin/healthz.go (109 lines of code) (raw):

// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package plugin import ( "net/url" "time" "context" "fmt" kmspb "google.golang.org/api/cloudkms/v1" "google.golang.org/grpc/credentials/insecure" "k8s.io/apimachinery/pkg/util/sets" "net" "net/http" "github.com/golang/glog" "google.golang.org/grpc" ) // HealthCheckerManager types that encapsulates healthz functionality of kms-plugin. // The following health checks are performed: // 1. Getting version of the plugin - validates gRPC connectivity. // 2. Asserting that the caller has encrypt and decrypt permissions on the crypto key. type HealthCheckerManager struct { keyName string KeyService *kmspb.ProjectsLocationsKeyRingsCryptoKeysService unixSocketPath string callTimeout time.Duration servingURL *url.URL plugin HealthChecker } type HealthChecker interface { PingRPC(context.Context, *grpc.ClientConn) error PingKMS(context.Context, *grpc.ClientConn) error } func NewHealthChecker(plugin HealthChecker, keyName string, keyService *kmspb.ProjectsLocationsKeyRingsCryptoKeysService, unixSocketPath string, callTimeout time.Duration, servingURL *url.URL) *HealthCheckerManager { return &HealthCheckerManager{ keyName: keyName, KeyService: keyService, unixSocketPath: unixSocketPath, callTimeout: callTimeout, servingURL: servingURL, plugin: plugin, } } // Serve creates http server for hosting healthz. func (m *HealthCheckerManager) Serve() chan error { errorCh := make(chan error) mux := http.NewServeMux() mux.HandleFunc(fmt.Sprintf("/%s", m.servingURL.EscapedPath()), m.HandlerFunc) go func() { defer close(errorCh) glog.Infof("Registering healthz listener at %v", m.servingURL) select { case errorCh <- http.ListenAndServe(m.servingURL.Host, mux): default: } }() return errorCh } func (m *HealthCheckerManager) HandlerFunc(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), m.callTimeout) defer cancel() conn, err := dialUnix(m.unixSocketPath) if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) return } defer conn.Close() if err := m.plugin.PingRPC(ctx, conn); err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) return } if err := m.TestIAMPermissions(); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } if r.FormValue("ping-kms") == "true" { if err := m.plugin.PingKMS(ctx, conn); err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) return } } w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) } func (h *HealthCheckerManager) TestIAMPermissions() error { want := sets.NewString("cloudkms.cryptoKeyVersions.useToEncrypt", "cloudkms.cryptoKeyVersions.useToDecrypt") glog.Infof("Testing IAM permissions, want %v", want.List()) req := &kmspb.TestIamPermissionsRequest{ Permissions: want.List(), } resp, err := h.KeyService.TestIamPermissions(h.keyName, req).Do() if err != nil { return fmt.Errorf("failed to test IAM Permissions on %s, %v", h.keyName, err) } glog.Infof("Got permissions: %v from CloudKMS for key:%s", resp.Permissions, h.keyName) got := sets.NewString(resp.Permissions...) diff := want.Difference(got) if diff.Len() != 0 { glog.Errorf("Failed to validate IAM Permissions on %s, diff: %v", h.keyName, diff) return fmt.Errorf("missing %v IAM permissions on CryptoKey:%s", diff, h.keyName) } glog.Infof("Successfully validated IAM Permissions on %s.", h.keyName) return nil } func dialUnix(unixSocketPath string) (*grpc.ClientConn, error) { protocol, addr := "unix", unixSocketPath dialer := func(ctx context.Context, addr string) (net.Conn, error) { if deadline, ok := ctx.Deadline(); ok { return net.DialTimeout(protocol, addr, time.Until(deadline)) } return net.DialTimeout(protocol, addr, 0) } return grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(dialer)) }