code: func PrepullImagesInParallel(kubePrepuller Prepuller, timeout time.Duration, componentsToPrepull []string) error {
  	fmt.Printf("[upgrade/prepull] Will prepull images for components %v\n", componentsToPrepull)
  
  	timeoutChan := time.After(timeout)
  
  	// Synchronously create the DaemonSets
  	for _, component := range componentsToPrepull {
  		if err := kubePrepuller.CreateFunc(component); err != nil {
  			return err
  		}
  	}
  
  	// Create a channel for streaming data from goroutines that run in parallel to a blocking for loop that cleans up
  	prePulledChan := make(chan string, len(componentsToPrepull))
  	for _, component := range componentsToPrepull {
  		go func(c string) {
  			// Wait as long as needed. This WaitFunc call should be blocking until completion
  			kubePrepuller.WaitFunc(c)
  			// When the task is done, go ahead and cleanup by sending the name to the channel
  			prePulledChan <- c
  		}(component)
  	}
  
  	// This call blocks until all expected messages are received from the channel or errors out if timeoutChan fires.
  	// For every successful wait, kubePrepuller.DeleteFunc is executed
  	if err := waitForItemsFromChan(timeoutChan, prePulledChan, len(componentsToPrepull), kubePrepuller.DeleteFunc); err != nil {
  		return err
  	}
  
  	fmt.Println("[upgrade/prepull] Successfully prepulled the images for all the control plane components")
  	return nil
  }
docs: // PrepullImagesInParallel creates DaemonSets synchronously but waits in parallel for the images to pull