package handlers import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "otaupdate/services" "sort" "strconv" "strings" "github.com/fiskerinc/cloud-services/pkg/cache" "github.com/fiskerinc/cloud-services/pkg/common" "github.com/fiskerinc/cloud-services/pkg/logger" "github.com/fiskerinc/cloud-services/pkg/utils" "github.com/fiskerinc/cloud-services/pkg/utils/envtool" "github.com/fiskerinc/cloud-services/pkg/validator" "github.com/julienschmidt/httprouter" "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" ) type ManifestMigrateVersion struct { Version int } var ERR_MANIFEST_MIGRATE_BAD_VERSION = errors.New("received version not recognized") // TargetURLS is multiple as stage pushes to Prod and Euro Prod var apiToken string = envtool.GetEnv("MANIFEST_MIGRATE_TOKEN", "MISSING API TOKEN") var targetURLS []string = strings.Split(envtool.GetEnv("MANIFEST_MIGRATE_URLS", "MISSING_TARGET_URL"), ",") // So we are going to take the information from our lower env and push it into a higher one. // Need to decide how we want to get the information from the lower level to this higher level // HandleUpdateManifestMigrate godoc // @Summary Push update manifest from this environment to a higher one // @Accept json // @Produce json // @Param Authorization header string false "Bearer " // @Param Api-Key header string false "" // @Param manifest_id path string true "Manifest ID" // @Success 200 {object} common.JSONMessage // @Failure 400 {object} common.JSONError "Bad Request" // @Failure 401 {object} common.JSONError "Unauthorized" // @Failure 503 {object} common.JSONError "Service unavailable" // @Router /manifestmigrate/{manifest_id} [post] func HandleUpdateManifestMigrate(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) manifestIdString := params.ByName("manifest_id") // Gather the right information manifestId, err := strconv.Atoi(manifestIdString) if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { return } var manifest common.UpdateManifest manifest.ID = int64(manifestId) err = validator.ValidateIDField(manifest.ID) if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { return } um := services.GetDB().GetUpdateManifests() err = um.Load(&manifest) if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { return } manifest.SUMS = "" manifest.Env = "" manifest.MigratePrepareHardwareVersion() fileKeys, err := getFileKeys(manifest) if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { return } sendStruct := ManifestMigrateBody{} sendStruct.MigratedManifest = manifest sendStruct.FileKeys = fileKeys // Send message to the other API's sendBody, err := PrepareMigrateBodyForSending(&sendStruct) if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { return } for index, targetURL := range targetURLS { turl, err := url.JoinPath(targetURL, "manifestmigrate") if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { return } req, err := http.NewRequest("POST", turl, bytes.NewBuffer(sendBody)) if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { return } req.Header.Set("Content-Type", "application/json") req.Header.Add("Api-Key", apiToken) resp, err := http.DefaultClient.Do(req) if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { deleteBecauseFailedMigrate(targetURLS[:index], int(manifest.ID)) return } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) // Forgot to check the status code if resp.StatusCode >= 300 { logger.Error().Msgf("Failed to send manifest migrate: %s \n Status: %s\n Resp Body: %s\n", string(sendBody), resp.Status, string(b)) var response common.JSONError err := json.Unmarshal(b, &response) if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { return } utils.RespJSON(w, http.StatusBadRequest, response) return } } utils.RespJSON(w, http.StatusOK, common.JSONMessage{ Message: "Migration Sent", }) } // If we fail to upload a manifest to one of the urls, delete from the other environments we are uploading to func deleteBecauseFailedMigrate(urls []string, manifestID int) { for _, targetURL := range urls { turl, err := url.JoinPath(targetURL, "manifestraw") if err != nil { logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload", manifestID, targetURL) continue } turl += fmt.Sprintf("?id=%d", manifestID) req, err := http.NewRequest("DELETE", turl, nil) if err != nil { logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload", manifestID, targetURL) continue } req.Header.Set("Content-Type", "application/json") req.Header.Add("Api-Key", apiToken) resp, err := http.DefaultClient.Do(req) if err != nil { logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload %s", manifestID, targetURL, resp.Status) continue } defer resp.Body.Close() if resp.StatusCode >= 300 { if err != nil { logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload %s", manifestID, targetURL, resp.Status) continue } } } } func getFileKeys(manifest common.UpdateManifest) ([]common.FileKeyResponse, error) { conn := services.RedisClientPool().GetFromPool() dbFK := services.GetDB().GetFileKeys() defer conn.Close() fileIDs := make([]string, 0) for _, ecu := range manifest.ECUs { for _, file := range ecu.Files { fileIDs = append(fileIDs, file.FileID) } } return cache.RetrieveFileEncryptionParams(conn, dbFK, fileIDs) } // In case we need a completely different struct, we will just return the bytes func PrepareMigrateBodyForSending(manifestMigrate *ManifestMigrateBody) (jsonBody []byte, err error) { v, err := getVersionToSend() if err != nil { return } err = transformMigrateBody(manifestMigrate, v) if err != nil { return } return json.Marshal(manifestMigrate) } // Get the version that the env we are pushing to is going to run, then we can transform is needed or not send at all // If we do not get a version, it defaults to 0 func getVersionToSend() (version int, err error) { version = -1 if len(targetURLS) == 0 { return version, errors.New("missing target url") } targetURL := targetURLS[0] // The eu prod and normal prod should always be the same version, so just going to act like they are turl, err := url.JoinPath(targetURL, "manifestmigrate-version") if err != nil { return } req, err := http.NewRequest("GET", turl, nil) if err != nil { return } req.Header.Add("Api-Key", apiToken) resp, err := http.DefaultClient.Do(req) if err != nil { return } defer resp.Body.Close() // If they don't have this route, we can assume they are on version 0 if resp.StatusCode >= 300 { logger.Warn().Msgf("manifest migrate version returned status code %s", resp.Status) version = 0 return } var mmv ManifestMigrateVersion err = json.NewDecoder(resp.Body).Decode(&mmv) if err != nil { return } version = mmv.Version return } const MIGRATION_VERSION = 1 // If we do not get a version, it defaults to 0 // If we cannot send the manifest due to some major change, then return an error func transformMigrateBody(manifestMigrate *ManifestMigrateBody, version int) (err error) { // We have to go from our version down to their version // EX we are on version 5, they are on version 3 // So do the conversion of 5 -> 4, then 4 -> 3 // For each version that is added on, add to this switch statement // This switch statement is more of a way to organize the flow, can probably be improved switch MIGRATION_VERSION { case 1: // The conversion needed to go from VERSION 1 to VERSION 0 if version == 1 { // If we are at the desired version, then we can break break } swapECUInstallPriority(manifestMigrate) fallthrough case 0: // THis is just an example row to show the fallthrough from case of 1 -> 0 if version == 0 { break } default: err = ERR_MANIFEST_MIGRATE_BAD_VERSION } return } func swapECUInstallPriority(manifestMigrate *ManifestMigrateBody) { ecuList := manifestMigrate.MigratedManifest.ECUs // Order the array so we can begin to swap easily sort.Slice(ecuList, func(i, j int) bool { return ecuList[i].InstallPriority < ecuList[j].InstallPriority }) rightIndex := len(ecuList) - 1 for leftIndex := 0; leftIndex < rightIndex; leftIndex++ { ecuList[leftIndex].InstallPriority, ecuList[rightIndex].InstallPriority = ecuList[rightIndex].InstallPriority, ecuList[leftIndex].InstallPriority rightIndex-- } }