| package apiGatewayDeploy |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "io" |
| "net/http" |
| "net/url" |
| "os" |
| "encoding/base64" |
| "path" |
| "errors" |
| "io/ioutil" |
| "time" |
| "github.com/30x/apid" |
| ) |
| |
| // todo: remove downloaded bundle files from old deployments |
| |
| const ( |
| DOWNLOAD_ATTEMPTS = 3 |
| ) |
| |
| var ( |
| downloadMultiplier = 10 * time.Second |
| ) |
| |
| type systemBundle struct { |
| URI string `json:"uri"` |
| } |
| |
| type dependantBundle struct { |
| URI string `json:"uri"` |
| Scope string `json:"scope"` |
| Org string `json:"org"` |
| Env string `json:"env"` |
| } |
| |
| type bundleManifest struct { |
| SysBun systemBundle `json:"system"` |
| DepBun []dependantBundle `json:"bundles"` |
| } |
| |
| // event bundle |
| type bundle struct { |
| BundleID string `json:"bundleId"` |
| URI string `json:"uri"` |
| Scope string `json:"scope"` |
| Org string `json:"org"` |
| Env string `json:"env"` |
| } |
| |
| // event deployment |
| type deployment struct { |
| DeploymentID string `json:"deploymentId"` |
| System bundle `json:"system"` |
| Bundles []bundle `json:"bundles"` |
| } |
| |
| type deploymentErrorDetail struct { |
| ErrorCode int `json:"errorCode"` |
| Reason string `json:"reason"` |
| BundleID string `json:"bundleId"` |
| } |
| |
| type deploymentErrorResponse struct { |
| ErrorCode int `json:"errorCode"` |
| Reason string `json:"reason"` |
| ErrorDetails []deploymentErrorDetail `json:"bundleErrors"` |
| } |
| |
| type deploymentResponse struct { |
| Status string `json:"status"` |
| Error deploymentErrorResponse `json:"error"` |
| } |
| |
| // retrieveBundle retrieves bundle data from a URI |
| func getBundleReader(uriString string) (io.ReadCloser, error) { |
| |
| uri, err := url.Parse(uriString) |
| if err != nil { |
| return nil, fmt.Errorf("DownloadFileUrl: Failed to parse urlStr: %s", uriString) |
| } |
| |
| // todo: Temporary blind bundle download, Bundle storage TBD |
| |
| // assume it's a file if no scheme |
| if uri.Scheme == "" || uri.Scheme == "file" { |
| f, err := os.Open(uri.Path) |
| if err != nil { |
| return nil, err |
| } |
| return f, nil |
| } |
| |
| // some random uri, try to GET it |
| res, err := http.Get(uriString) |
| if err != nil { |
| return nil, err |
| } |
| if res.StatusCode != 200 { |
| return nil, fmt.Errorf("Bundle uri %s failed with status %d", uriString, res.StatusCode) |
| } |
| return res.Body, nil |
| } |
| |
| // check if already exists and skip |
| func prepareBundle(depID string, bun bundle) error { |
| |
| bundleFile := getBundleFilePath(depID, bun.URI) |
| bundleDir := path.Dir(bundleFile) |
| |
| downloadBundle := func() (fileName string, err error) { |
| |
| log.Debugf("Downloading bundle: %s", bun.URI) |
| |
| var tempFile *os.File |
| tempFile, err = ioutil.TempFile(bundleDir, "download") |
| if err != nil { |
| log.Errorf("Unable to create temp file: %v", err) |
| return |
| } |
| defer tempFile.Close() |
| fileName = tempFile.Name() |
| |
| var bundleReader io.ReadCloser |
| bundleReader, err = getBundleReader(bun.URI) |
| if err != nil { |
| log.Errorf("Unable to retrieve bundle %s: %v", bun.URI, err) |
| return |
| } |
| defer bundleReader.Close() |
| |
| _, err = io.Copy(tempFile, bundleReader) |
| if err != nil { |
| log.Errorf("Unable to write bundle %s: %v", tempFile, err) |
| return |
| } |
| |
| log.Debugf("Bundle downloaded: %s", bun.URI) |
| return |
| } |
| |
| // retry |
| var tempFile string |
| var err error |
| for i := 1; i <= DOWNLOAD_ATTEMPTS; i++ { |
| tempFile, err = downloadBundle() |
| if err == nil { |
| break |
| } |
| if tempFile != "" { |
| os.Remove(tempFile) |
| } |
| |
| // simple back-off, we could potentially be more sophisticated |
| retryIn := time.Duration(i) * downloadMultiplier |
| log.Debugf("will retry download in %s", retryIn) |
| time.Sleep(retryIn) |
| } |
| |
| if err != nil { |
| log.Errorf("failed %s download attempts. aborting.", DOWNLOAD_ATTEMPTS) |
| return err |
| } |
| |
| err = os.Rename(tempFile, bundleFile) |
| if err != nil { |
| log.Errorf("Unable to rename temp bundle file %s to %s: %s", tempFile, bundleFile, err) |
| os.Remove(tempFile) |
| return err |
| } |
| |
| return nil |
| } |
| |
| func getDeploymentFilesPath(depID string) string { |
| return path.Join(bundlePath, depID) |
| } |
| |
| func getBundleFilePath(depID string, bundleURI string) string { |
| return path.Join(getDeploymentFilesPath(depID), base64.StdEncoding.EncodeToString([]byte(bundleURI))) |
| } |
| |
| // returns first bundle download error |
| // all bundles will be attempted regardless of errors, in the future we could retry |
| func prepareDeployment(db apid.DB, depID string, dep deployment) error { |
| |
| log.Debugf("preparing deployment: %s", depID) |
| |
| err := insertDeployment(db, depID, dep) |
| if err != nil { |
| log.Errorf("insert deployment failed: %v", err) |
| return err |
| } |
| |
| deploymentPath := getDeploymentFilesPath(depID) |
| err = os.MkdirAll(deploymentPath, 0700) |
| if err != nil { |
| log.Errorf("Deployment dir creation failed: %v", err) |
| return err |
| } |
| |
| // download bundles and store them locally |
| errorsChan := make(chan error, len(dep.Bundles)) |
| for i := range dep.Bundles { |
| bun := dep.Bundles[i] |
| go func() { |
| err := prepareBundle(depID, bun) |
| errorsChan<- err |
| if err != nil { |
| id := string(i) |
| err = updateBundleStatus(db, depID, id, DEPLOYMENT_STATE_ERR_APID, ERROR_CODE_TODO, err.Error()) |
| if err != nil { |
| log.Errorf("Update bundle %s:%s status failed: %v", depID, id, err) |
| } |
| } |
| }() |
| } |
| |
| // fail fast on first error, otherwise wait for completion |
| for range dep.Bundles { |
| err := <-errorsChan |
| if err != nil { |
| updateDeploymentStatus(db, depID, DEPLOYMENT_STATE_ERR_APID, ERROR_CODE_TODO) |
| return err |
| } |
| } |
| |
| return updateDeploymentStatus(db, depID, DEPLOYMENT_STATE_READY, 0) |
| } |
| |
| func parseManifest(manifestString string) (dep deployment, err error) { |
| err = json.Unmarshal([]byte(manifestString), &dep) |
| if err != nil { |
| log.Errorf("JSON decoding Manifest failed: %v", err) |
| return |
| } |
| |
| // validate manifest |
| if dep.System.URI == "" { |
| err = errors.New("system bundle 'uri' is required") |
| return |
| } |
| for _, bun := range dep.Bundles { |
| if bun.BundleID == "" { |
| err = errors.New("bundle 'bundleId' is required") |
| return |
| } |
| if bun.URI == "" { |
| err = errors.New("bundle 'uri' is required") |
| return |
| } |
| if bun.Scope == "" { |
| err = errors.New("bundle 'scope' is required") |
| return |
| } |
| } |
| |
| return |
| } |