| // Common code for backing up datastore entities. |
| // |
| // At the time of this writing there is a cloud golang client, but it is ugly, |
| // filled with default named types such as |
| // GoogleDatastoreAdminV1beta1ExportEntitiesRequest, which I presume will |
| // change before leaving beta. We can update to the cloud golang client once it |
| // leaves beta. |
| package backup |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "io" |
| "net/http" |
| "strings" |
| "time" |
| |
| "go.skia.org/infra/go/ds" |
| "go.skia.org/infra/go/metrics2" |
| "go.skia.org/infra/go/sklog" |
| "go.skia.org/infra/go/util" |
| ) |
| |
| const ( |
| URL = "https://datastore.googleapis.com/v1beta1/projects/%s:export" |
| ) |
| |
| var ( |
| backupStep = metrics2.NewLiveness("backup_step") |
| backupSuccess = metrics2.NewLiveness("backup_success") |
| ) |
| |
| type EntityFilter struct { |
| Kinds []ds.Kind `json:"kinds"` |
| NamespaceIds []string `json:"namespaceIds"` |
| } |
| |
| type Request struct { |
| OutputUrlPrefix string `json:"outputUrlPrefix"` |
| EntityFilter EntityFilter `json:"entityFilter"` |
| } |
| |
| // singleRequest makes a single http POST request to 'url' with a body of |
| // 'buf'. |
| // |
| // Returns the http.Response and a bool that is true if the request should be |
| // retried because there are already the maximum number of exports running. |
| func singleRequest(client *http.Client, url string, buf *bytes.Buffer) (*http.Response, error, bool) { |
| shouldRetry := false |
| resp, err := client.Post(url, "application/json", buf) |
| if resp != nil { |
| if resp.StatusCode == 429 { |
| sklog.Infof("Got 429 RESOURCE_EXHAUSTED, waiting to retry operation.") |
| shouldRetry = true |
| util.Close(resp.Body) |
| } |
| } |
| return resp, err, shouldRetry |
| } |
| |
| // Step runs a single backup of all the entities listed in ds.KindsToBackup |
| // for the given project, data is written to the given GCS bucket. |
| func Step(client *http.Client, project, bucket string) error { |
| // |
| // Configure what gets backed up here by adding to ds.KindsToBackup. |
| // |
| safeProject := strings.ReplaceAll(project, ":", "_") |
| success := true |
| for ns, kinds := range ds.KindsToBackup { |
| req := Request{ |
| OutputUrlPrefix: fmt.Sprintf("gs://%s/%s/", bucket, safeProject) + time.Now().Format("2006/01/02/15/04"), |
| EntityFilter: EntityFilter{ |
| Kinds: kinds, |
| NamespaceIds: []string{ns}, |
| }, |
| } |
| b, err := json.Marshal(req) |
| if err != nil { |
| sklog.Errorf("Failed to encode request: %s-%v: %s", ns, kinds, err) |
| success = false |
| continue |
| } |
| url := fmt.Sprintf(URL, project) |
| var resp *http.Response |
| |
| shouldRetry := true |
| for shouldRetry { // Could retry forever, but then backupSuccess will never be Reset() and that will trigger an alert. |
| resp, err, shouldRetry = singleRequest(client, url, bytes.NewBuffer(b)) |
| if shouldRetry { |
| time.Sleep(10 * time.Minute) |
| } |
| } |
| if err != nil { |
| sklog.Errorf("Request failed: %s-%v: %s", ns, kinds, err) |
| success = false |
| continue |
| } else { |
| sklog.Infof("Successfully started backup: %s-%v", ns, kinds) |
| } |
| bodyBytes, err := io.ReadAll(resp.Body) |
| if err != nil { |
| sklog.Errorf("Failed to read response: %s-%v: %s", ns, kinds, err) |
| success = false |
| continue |
| } |
| if resp.StatusCode >= 500 { |
| success = false |
| |
| // Emit the reponse into the structured logs, but make sure the JSON is |
| // only a singe line by decoding and re-encoding as JSON using |
| // json.Marshal(). |
| var parsed interface{} |
| err := json.Unmarshal(bodyBytes, &parsed) |
| if err != nil { |
| sklog.Errorf("Response was invalid JSON: %s", err) |
| continue |
| } |
| singleLine, err := json.Marshal(parsed) |
| if err != nil { |
| sklog.Errorf("Unable to convert response to JSON: %s", err) |
| continue |
| } |
| fmt.Print(string(singleLine)) |
| continue |
| } |
| sklog.Infof("Started backup of %s-%v", ns, kinds) |
| } |
| if success { |
| backupSuccess.Reset() |
| } |
| backupStep.Reset() |
| return nil |
| } |