k8s.io/perf-tests/clusterloader2@v0.0.0-20240304094227-64bdb12da87e/pkg/imagepreload/imagepreload.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package imagepreload
    18  
    19  import (
    20  	"context"
    21  	"embed"
    22  	"strings"
    23  	"sync"
    24  	"time"
    25  
    26  	v1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/apimachinery/pkg/util/wait"
    30  	"k8s.io/apimachinery/pkg/watch"
    31  	"k8s.io/client-go/tools/cache"
    32  	"k8s.io/klog/v2"
    33  	"k8s.io/perf-tests/clusterloader2/pkg/config"
    34  	"k8s.io/perf-tests/clusterloader2/pkg/flags"
    35  	"k8s.io/perf-tests/clusterloader2/pkg/framework"
    36  	"k8s.io/perf-tests/clusterloader2/pkg/framework/client"
    37  	"k8s.io/perf-tests/clusterloader2/pkg/measurement/util/informer"
    38  	"k8s.io/perf-tests/clusterloader2/pkg/measurement/util/runtimeobjects"
    39  )
    40  
    41  const (
    42  	informerTimeout = time.Minute
    43  	manifest        = "manifests/daemonset.yaml"
    44  	namespace       = "preload"
    45  	daemonsetName   = "preload"
    46  	pollingInterval = 5 * time.Second
    47  	pollingTimeout  = 15 * time.Minute
    48  )
    49  
    50  var (
    51  	images []string
    52  	//go:embed manifests
    53  	manifestsFS embed.FS
    54  )
    55  
    56  func init() {
    57  	flags.StringSliceEnvVar(&images, "node-preload-images", "NODE_PRELOAD_IMAGES", []string{}, "List of images to preload on each node in the test cluster before executing tests")
    58  }
    59  
    60  type controller struct {
    61  	// lock for controlling access to doneNodes in PreloadImages
    62  	lock sync.Mutex
    63  
    64  	config          *config.ClusterLoaderConfig
    65  	framework       *framework.Framework
    66  	templateMapping map[string]interface{}
    67  	images          []string
    68  }
    69  
    70  // Setup ensures every node in cluster preloads given list of images before starting tests.
    71  // It does it by creating a Daemonset that call "docker pull" and awaits for Node object to be updated.
    72  // As a side-effect of the image preloading, size of Node objects is increased.
    73  //
    74  // Preloading is skipped in kubemark or if no images have been specified.
    75  func Setup(conf *config.ClusterLoaderConfig, f *framework.Framework) error {
    76  	mapping, err := config.GetMapping(conf, nil)
    77  	if err != nil {
    78  		return err
    79  	}
    80  
    81  	ctl := &controller{
    82  		config:          conf,
    83  		framework:       f,
    84  		templateMapping: mapping,
    85  		images:          images,
    86  	}
    87  	return ctl.PreloadImages()
    88  }
    89  
    90  func (c *controller) PreloadImages() error {
    91  	if len(images) == 0 {
    92  		klog.Warning("No images specified. Skipping image preloading")
    93  		return nil
    94  	}
    95  	if !c.config.ClusterConfig.Provider.Features().SupportImagePreload {
    96  		klog.Warningf("Image preloading is disabled in provider: %s", c.config.ClusterConfig.Provider.Name())
    97  		return nil
    98  	}
    99  
   100  	kclient := c.framework.GetClientSets().GetClient()
   101  
   102  	doneNodes := make(map[string]struct{})
   103  	stopCh := make(chan struct{})
   104  	defer close(stopCh)
   105  
   106  	nodeInformer := informer.NewInformer(
   107  		&cache.ListWatch{
   108  			ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
   109  				return kclient.CoreV1().Nodes().List(context.TODO(), options)
   110  			},
   111  			WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
   112  				return kclient.CoreV1().Nodes().Watch(context.TODO(), options)
   113  			},
   114  		},
   115  		func(old, new interface{}) { c.checkNode(doneNodes, old, new) })
   116  	if err := informer.StartAndSync(nodeInformer, stopCh, informerTimeout); err != nil {
   117  		return err
   118  	}
   119  
   120  	klog.V(2).Infof("Creating namespace %s...", namespace)
   121  	if err := client.CreateNamespace(kclient, namespace); err != nil {
   122  		return err
   123  	}
   124  
   125  	klog.V(2).Info("Creating daemonset to preload images...")
   126  	c.templateMapping["Images"] = c.images
   127  	if err := c.framework.ApplyTemplatedManifests(manifestsFS, manifest, c.templateMapping); err != nil {
   128  		return err
   129  	}
   130  
   131  	klog.V(2).Infof("Getting %s/%s deamonset size...", namespace, daemonsetName)
   132  	ds, err := kclient.AppsV1().DaemonSets(namespace).Get(context.TODO(), daemonsetName, metav1.GetOptions{})
   133  	if err != nil {
   134  		return err
   135  	}
   136  	size, err := runtimeobjects.GetReplicasFromRuntimeObject(kclient, ds)
   137  	if err != nil {
   138  		return err
   139  	}
   140  	if err := size.Start(stopCh); err != nil {
   141  		return err
   142  	}
   143  
   144  	var clusterSize, doneCount int
   145  	klog.V(2).Infof("Waiting for %d Node objects to be updated...", size.Replicas())
   146  	if err := wait.Poll(pollingInterval, pollingTimeout, func() (bool, error) {
   147  		clusterSize = size.Replicas()
   148  		doneCount = c.countDone(doneNodes)
   149  		klog.V(3).Infof("%d out of %d nodes have pulled images", doneCount, clusterSize)
   150  		return doneCount == clusterSize, nil
   151  	}); err != nil {
   152  		klog.Errorf("%d out of %d nodes have pulled images", doneCount, clusterSize)
   153  		return err
   154  	}
   155  	klog.V(2).Info("Waiting... done")
   156  
   157  	klog.V(2).Infof("Deleting namespace %s...", namespace)
   158  	if err := client.DeleteNamespace(kclient, namespace); err != nil {
   159  		return err
   160  	}
   161  	if err := client.WaitForDeleteNamespace(kclient, namespace, client.DefaultNamespaceDeletionTimeout); err != nil {
   162  		return err
   163  	}
   164  	return nil
   165  }
   166  
   167  func (c *controller) checkNode(set map[string]struct{}, old, new interface{}) {
   168  	if new != nil {
   169  		node := new.(*v1.Node)
   170  		preloaded := c.hasPreloadedImages(node)
   171  		c.markDone(set, node.Name, preloaded)
   172  		return
   173  	}
   174  	if old != nil {
   175  		node := old.(*v1.Node)
   176  		c.markDone(set, node.Name, false)
   177  		return
   178  	}
   179  
   180  }
   181  
   182  func (c *controller) markDone(set map[string]struct{}, node string, done bool) {
   183  	c.lock.Lock()
   184  	defer c.lock.Unlock()
   185  	if done {
   186  		set[node] = struct{}{}
   187  	} else {
   188  		delete(set, node)
   189  	}
   190  }
   191  
   192  func (c *controller) countDone(set map[string]struct{}) int {
   193  	c.lock.Lock()
   194  	defer c.lock.Unlock()
   195  	return len(set)
   196  }
   197  
   198  func (c *controller) hasPreloadedImages(node *v1.Node) bool {
   199  	nodeImages := make([]string, 0, 20)
   200  	for _, nodeImg := range node.Status.Images {
   201  		nodeImages = append(nodeImages, nodeImg.Names...)
   202  	}
   203  
   204  	for _, img := range c.images {
   205  		found := false
   206  		for _, nodeImg := range nodeImages {
   207  			found = strings.HasPrefix(nodeImg, img)
   208  			if found {
   209  				break
   210  			}
   211  		}
   212  		if !found {
   213  			return false
   214  		}
   215  	}
   216  	return true
   217  }