| // 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 |
| } |