| // 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 | |
| // | |
| // http://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 fakes contains fake implementations to be used in unit tests. | |
| package fakes | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "io/ioutil" | |
| "log" | |
| "net/http" | |
| "net/http/httptest" | |
| "strings" | |
| "testing" | |
| compute "google.golang.org/api/compute/v1" | |
| "google.golang.org/api/googleapi" | |
| ) | |
| // writeError writes the given error code to the given http.ResponseWriter. | |
| func writeError(w http.ResponseWriter, r *http.Request, code int) { | |
| w.WriteHeader(code) | |
| resp := &googleapi.Error{Code: code} | |
| bytes, err := json.Marshal(resp) | |
| if err != nil { | |
| log.Printf("error: %s URL: %s Response: %v", err, r.URL.Path, resp) | |
| return | |
| } | |
| w.Write(bytes) | |
| } | |
| // GCE is a fake GCE implementation. It is intended to be constructed with NewGCEServer. | |
| // | |
| // The GCE struct represents the state of the fake GCE instance. Fields on this struct can be modified to influence the | |
| // return values of GCE API calls. | |
| // | |
| // GCE should not be considered concurrency-safe. Do not use this struct in a concurrent way. | |
| type GCE struct { | |
| // Images represents the images present in the project. | |
| Images *compute.ImageList | |
| // Deprecated represents the set of deprecated images in the project. | |
| Deprecated map[string]*compute.DeprecationStatus | |
| // Operations is the sequence of operations that the fake GCE server should return. | |
| Operations []*compute.Operation | |
| // server is an HTTP server that serves fake GCE requests. Requests are served using the state stored in | |
| // the other struct fields. | |
| server *httptest.Server | |
| project string | |
| Instances []*compute.Instance | |
| } | |
| // NewGCEServer constructs a fake GCE implementation for a given GCE project. | |
| func NewGCEServer(project string) *GCE { | |
| gce := &GCE{ | |
| Images: &compute.ImageList{}, | |
| Deprecated: make(map[string]*compute.DeprecationStatus), | |
| project: project, | |
| } | |
| mux := http.NewServeMux() | |
| mux.HandleFunc(fmt.Sprintf("/projects/%s/global/images", project), gce.imagesListHandler) | |
| mux.HandleFunc(fmt.Sprintf("/projects/%s/global/images/", project), gce.imageHandler) | |
| mux.HandleFunc(fmt.Sprintf("/projects/%s/global/operations/", project), gce.operationsHandler) | |
| mux.HandleFunc(fmt.Sprintf("/projects/%s/zones/", project), gce.instancesHandler) | |
| gce.server = httptest.NewServer(mux) | |
| return gce | |
| } | |
| // Client gets a GCE client to use for accessing the fake GCE server. | |
| func (g *GCE) Client() (*compute.Service, error) { | |
| client, err := compute.New(g.server.Client()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| client.BasePath = g.server.URL | |
| return client, nil | |
| } | |
| func (g *GCE) operation() *compute.Operation { | |
| op := g.Operations[0] | |
| g.Operations = g.Operations[1:] | |
| return op | |
| } | |
| func (g *GCE) deprecate(name string, status *compute.DeprecationStatus) *compute.Operation { | |
| g.Deprecated[name] = status | |
| return g.operation() | |
| } | |
| func (g *GCE) image(name string) *compute.Image { | |
| for _, image := range g.Images.Items { | |
| if image.Name == name { | |
| return image | |
| } | |
| } | |
| return nil | |
| } | |
| func (g *GCE) imagesListHandler(w http.ResponseWriter, r *http.Request) { | |
| bytes, err := json.Marshal(g.Images) | |
| if err != nil { | |
| writeError(w, r, http.StatusInternalServerError) | |
| return | |
| } | |
| w.Write(bytes) | |
| } | |
| func (g *GCE) imageHandler(w http.ResponseWriter, r *http.Request) { | |
| // Path starts with /project/<project>/global/images/<name> | |
| splitPath := strings.Split(r.URL.Path, "/") | |
| splitPath = splitPath[1:] | |
| switch { | |
| case len(splitPath) == 5: | |
| image := g.image(splitPath[4]) | |
| if image == nil { | |
| writeError(w, r, http.StatusNotFound) | |
| return | |
| } | |
| bytes, err := json.Marshal(image) | |
| if err != nil { | |
| writeError(w, r, http.StatusInternalServerError) | |
| return | |
| } | |
| w.Write(bytes) | |
| case len(splitPath) == 6 && splitPath[5] == "deprecate": | |
| if g.image(splitPath[4]) == nil { | |
| writeError(w, r, http.StatusNotFound) | |
| return | |
| } | |
| body, err := ioutil.ReadAll(r.Body) | |
| if err != nil { | |
| log.Println("failed to read body") | |
| writeError(w, r, http.StatusInternalServerError) | |
| return | |
| } | |
| status := &compute.DeprecationStatus{} | |
| err = json.Unmarshal(body, status) | |
| if err != nil { | |
| log.Printf("failed to parse body: %s", string(body)) | |
| writeError(w, r, http.StatusInternalServerError) | |
| return | |
| } | |
| op := g.deprecate(splitPath[4], status) | |
| bytes, err := json.Marshal(op) | |
| if err != nil { | |
| log.Printf("failed to marshal operation: %v", op) | |
| writeError(w, r, http.StatusInternalServerError) | |
| return | |
| } | |
| w.Write(bytes) | |
| default: | |
| log.Printf("unrecognized path: %s", r.URL.Path) | |
| writeError(w, r, http.StatusNotFound) | |
| } | |
| } | |
| func (g *GCE) operationsHandler(w http.ResponseWriter, r *http.Request) { | |
| bytes, err := json.Marshal(g.operation()) | |
| if err != nil { | |
| writeError(w, r, http.StatusInternalServerError) | |
| return | |
| } | |
| w.Write(bytes) | |
| } | |
| func (g *GCE) instancesHandler(w http.ResponseWriter, r *http.Request) { | |
| splitPath := strings.Split(r.URL.Path, "/") | |
| switch r.Method { | |
| case "GET": | |
| if len(splitPath) != 6 { | |
| writeError(w, r, http.StatusNotFound) | |
| return | |
| } | |
| zone := splitPath[4] | |
| instancesList := compute.InstanceList{} | |
| for _, instance := range g.Instances { | |
| if instance.Zone != zone { | |
| continue | |
| } | |
| instancesList.Items = append(instancesList.Items, instance) | |
| } | |
| bytes, err := json.Marshal(instancesList) | |
| if err != nil { | |
| writeError(w, r, http.StatusInternalServerError) | |
| return | |
| } | |
| w.Write(bytes) | |
| case "DELETE": | |
| if len(splitPath) != 7 { | |
| writeError(w, r, http.StatusNotFound) | |
| return | |
| } | |
| zone := splitPath[4] | |
| name := splitPath[6] | |
| for idx, instance := range g.Instances { | |
| if instance.Zone != zone || instance.Name != name { | |
| continue | |
| } | |
| g.Instances[idx] = g.Instances[len(g.Instances)-1] | |
| g.Instances = g.Instances[:len(g.Instances)-1] | |
| op := compute.Operation{} | |
| bytes, err := json.Marshal(&op) | |
| if err != nil { | |
| writeError(w, r, http.StatusInternalServerError) | |
| return | |
| } | |
| w.Write(bytes) | |
| return | |
| } | |
| log.Printf("instance not found: name=%q, zone=%q", name, zone) | |
| writeError(w, r, http.StatusNotFound) | |
| default: | |
| log.Printf("unrecognized path: %s", r.URL.Path) | |
| writeError(w, r, http.StatusNotFound) | |
| } | |
| } | |
| // Close closes the fake GCE server. | |
| func (g *GCE) Close() { | |
| g.server.Close() | |
| } | |
| // GCEForTest encapsulates boilerplate needed for many test cases. | |
| func GCEForTest(t *testing.T, project string) (*GCE, *compute.Service) { | |
| t.Helper() | |
| gce := NewGCEServer(project) | |
| client, err := gce.Client() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| return gce, client | |
| } |