func ResolveAzureAsyncOperation[Props any]()

in e2etest/arm.go [48:191]


func ResolveAzureAsyncOperation[Props any](OAuth AccessToken, uri string, properties *Props) (armResp *ARMAsyncResponse[Props], err error) {
	if properties != nil && reflect.TypeOf(properties).Kind() != reflect.Ptr {
		return nil, fmt.Errorf("properties must be a pointer (or nil)")
	}

	req, err := http.NewRequest("GET", uri, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	var resp *http.Response
	var lastWaitSeconds int64 = 0
	for {
		if lastWaitSeconds == 0 {
			lastWaitSeconds = 1 // pretend we waited, proceed to the initial request.
		} else {
			fmt.Println("Sleeping", lastWaitSeconds, "seconds")
			time.Sleep(time.Second * time.Duration(lastWaitSeconds))
		}

		oAuthToken, err := OAuth.FreshToken()
		if err != nil {
			return nil, fmt.Errorf("failed to get fresh token: %w", err)
		}
		// Update the OAuth token if we have to
		req.Header["Authorization"] = []string{"Bearer " + oAuthToken}

		armResp = &ARMAsyncResponse[Props]{
			Properties: properties, // the user may have supplied a ptr to a struct, let encoding/json resolve that
		}

		resp, err = http.DefaultClient.Do(req)
		if err != nil {
			return nil, fmt.Errorf("failed to send request: %w", err)
		}

		/*
			Lessons learned from past attempts:
			- The body will not always be an ARMAsyncResponse
			- The body will not always be present.
			- The body not being present is not indicative that the job has finished.
			- The body not being present is not indicative that the job has not finished.
			- The response code shouldn't be trusted if there's a body.
			- The response code shouldn't be trusted if there's a followup location.
		*/

		// First things first. Let's pull our retry values.
		// There are two different places retry values can come back. `Location` and `Azure-AsyncOperation`
		followUpLoc := resp.Header.Get("Location")
		if followUpLoc == "" {
			followUpLoc = resp.Header.Get("Azure-AsyncOperation")
		}
		if followUpLoc != "" {
			uri = followUpLoc
		}

		// Let's see if we can find out how long to wait.
		// This can appear *sometimes*, but not always.
		retryAfterRaw := resp.Header.Get("Retry-After")
		var retryAfter int64
		if retryAfterRaw != "" {
			count, err := strconv.ParseInt(retryAfterRaw, 10, 32)
			if err != nil {
				retryAfter = count
			}
		}
		if retryAfter == 0 { // Fall back to our last wait, exponential, capped to 60s.
			retryAfter = lastWaitSeconds * 2
			retryAfter = common.Iff(retryAfter > 60, 60, retryAfter)
			lastWaitSeconds = retryAfter
		}

		// If the body is nonzero, we should read it.
		// This might contain status info that is more reliable than the response code (why? good question, that's why.)
		if resp.ContentLength != 0 {
			buf, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, fmt.Errorf("failed to read response body (resp code %d): %w", resp.StatusCode, err)
			}

			// Sometimes, this body *can* be an ARMAsyncResponse! If it is, this is great and useful information.
			// Other times, it may include "provisioningState":
			// Let's check!
			rawResp := map[string]any{}
			err = json.Unmarshal(buf, &rawResp)
			if err != nil {
				return nil, fmt.Errorf("failed to unmarshal body to raw struct: %w", err)
			}

			search := func(data map[string]any, key string) (any, bool) {
				queue := strings.Split(key, "/")
				for k, v := range queue {
					value, ok := data[v]
					if !ok {
						return nil, false
					}

					if k+1 == len(queue) {
						return value, ok
					} else {
						data, ok = value.(map[string]any)
						if !ok {
							return nil, false
						}
					}
				}

				// Go can't properly detect that this is unreachable, but we'll always hit
				panic("unreachable code")
			}

			usesARMStatus := false
			if status, ok := rawResp["status"]; ok {
				usesARMStatus = true

				if status == ARMStatusInProgress || status == ARMStatusRunning || status == ARMStatusResolvingDNS {
					continue
				}
			} else if status, ok = search(rawResp, "properties/provisioningState"); ok {
				// workaround for storage accounts.
				// todo: this will probably burn us eventually, but it's the only exception listed on the docs page, and so far the only one we've encountered.
				strStatus, ok := status.(string)
				if ok && (strStatus == ARMStatusInProgress || strStatus == ARMStatusRunning || strStatus == ARMStatusResolvingDNS) {
					continue
				}
			}

			if usesARMStatus {
				err = json.Unmarshal(buf, &armResp)
				return armResp, err
			} else {
				err = json.Unmarshal(buf, &properties)
				return nil, err
			}
		} else {
			if followUpLoc != "" { // Continue if there's a follow-up location
				continue
			} else {
				// Quoth the documentation: If no value is returned for provisioningState, the operation finished and succeeded.
				return nil, nil
			}
		}
	}
}