testutils/fakekubeapi/fakekubeapi.go (164 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 fakekubeapi supports integration testing of kms-plugin by faking K8S kube-apiserver. package fakekubeapi import ( "context" "encoding/json" "fmt" "io/ioutil" "net" "net/http" "net/http/httptest" "regexp" "strings" "sync" "time" msgspb "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/plugin/v1" "github.com/GoogleCloudPlatform/k8s-cloudkms-plugin/testutils/kmspluginclient" "github.com/golang/glog" "github.com/google/go-cmp/cmp" "github.com/phayes/freeport" corev1 "k8s.io/api/core/v1" ) var ( secretsURLRegex = regexp.MustCompile(`/api/v1/namespaces/[a-z-]*/secrets/[a-z-]*`) ) // Server fakes kube-apiserver. type Server struct { srv *httptest.Server port int namespaces corev1.NamespaceList secrets map[string][]corev1.Secret kms *kmspluginclient.Client timeout time.Duration mux sync.Mutex secretsListLog []corev1.Secret secretsPutLog []corev1.Secret } // Client returns *http.Client for the fake. func (f *Server) Client() *http.Client { return f.srv.Client() } // URL returns URL on which the fake is expecting requests. func (f *Server) URL() string { return f.srv.URL } // Close closes the underlying httptest.Server. func (f *Server) Close() { f.srv.Close() } // ListSecretsRequestsEquals validates that the supplied Secrets are equal to all secrets // processed by the server via http.Get. func (f *Server) ListSecretsRequestsEquals(r []corev1.Secret) error { f.mux.Lock() defer f.mux.Unlock() if diff := cmp.Diff(f.secretsListLog, r); diff != "" { return fmt.Errorf("list log differs from expected: (-want +got)\n%s", diff) } return nil } // PutSecretsEquals validates that the supplied Secrets are equal to all // secrets processed by the server via http.Put. func (f *Server) PutSecretsEquals(r []corev1.Secret) error { f.mux.Lock() defer f.mux.Unlock() if diff := cmp.Diff(f.secretsPutLog, r); diff != "" { return fmt.Errorf("put log differs from expected: (-want +got)\n%s", diff) } return nil } // New constructs kube-apiserver fake. // It is the responsibility of the caller to call Close. func New(namespaces corev1.NamespaceList, secrets map[string][]corev1.Secret, port int, kmsClient *kmspluginclient.Client, timeout time.Duration) (*Server, error) { var err error if port == 0 { port, err = freeport.GetFreePort() if err != nil { return nil, fmt.Errorf("failed to allocate port for fake kube-apiserver, error: %v", err) } } s := &Server{ namespaces: namespaces, secrets: secrets, kms: kmsClient, timeout: timeout, } s.srv = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: s.processGet(r.URL.EscapedPath(), w) case http.MethodPut: s.processPut(r, w) default: http.Error(w, fmt.Sprintf("unexpected http method %v", r.Method), http.StatusBadRequest) } })) l, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) if err != nil { return nil, fmt.Errorf("failed to listen on port %d, error: %v", port, err) } s.srv.Listener = l s.srv.Start() return s, nil } func (f *Server) recordSecretList(s []corev1.Secret) { f.mux.Lock() defer f.mux.Unlock() f.secretsListLog = append(f.secretsListLog, s...) } func (f *Server) recordSecretPut(s corev1.Secret) { f.mux.Lock() defer f.mux.Unlock() f.secretsPutLog = append(f.secretsPutLog, s) } func (f *Server) processPut(r *http.Request, w http.ResponseWriter) { ctx, cancel := context.WithTimeout(r.Context(), f.timeout) defer cancel() glog.Infof("Processing PUT request %v", r) if !secretsURLRegex.MatchString(r.URL.EscapedPath()) { http.Error(w, fmt.Sprintf("unexpected uri: %s", r.URL.EscapedPath()), http.StatusNotFound) return } b, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, fmt.Sprintf("failed to read the body of the request, error: %v", err), http.StatusBadRequest) return } s := &corev1.Secret{} if err := json.Unmarshal(b, s); err != nil { http.Error(w, fmt.Sprintf("failed to unmarshal request, error: %v", err), http.StatusBadRequest) return } f.recordSecretPut(*s) glog.Infoln("Sending secret for encryption to kms-plugin.") if _, err := f.kms.Encrypt(ctx, &msgspb.EncryptRequest{Version: "v1beta1", Plain: b}); err != nil { m := fmt.Sprintf("failed to transform secret, error: %v", err) glog.Warning(m) http.Error(w, m, http.StatusServiceUnavailable) return } glog.Info("kms-plugin processed the encryption request.") w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(s); err != nil { http.Error(w, fmt.Sprintf("failed to write response for secret put, error: %v", err), http.StatusBadRequest) return } } func (f *Server) processGet(url string, w http.ResponseWriter) { glog.Infof("Processing Get request %s", url) // TODO(alextc) Check URL - is it actually a get/list request for a Secret? var response interface{} switch { case url == "/api/v1/namespaces": response = f.namespaces // Expect url to be of the following format: /api/v1/namespaces/default/secrets. case strings.HasSuffix(url, "/secrets"): urlParts := strings.Split(url, "/") if len(urlParts) != 6 { http.Error(w, fmt.Sprintf("unexpected format of url: %q, wanted len of 4, got %d, parts: %#v", url, len(urlParts), urlParts), http.StatusBadRequest) return } s, ok := f.secrets[urlParts[4]] if !ok { http.Error(w, fmt.Sprintf("invalid test data, request for %q, but namespace %s was not provided", url, urlParts[4]), http.StatusNotFound) return } response = corev1.SecretList{ Items: s, } f.recordSecretList(s) default: http.Error(w, fmt.Sprintf("Was not expecting call to %q", url), http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(response); err != nil { http.Error(w, fmt.Sprintf("failed to write response for request:%s, err: %v", url, err), http.StatusInternalServerError) } }