github.com/dean7474/operator-registry@v1.21.1-0.20220418203638-d4717f98c2e5/test/e2e/bundle_image_test.go (about)

     1  package e2e_test
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"regexp"
    11  	"strings"
    12  	"time"
    13  
    14  	. "github.com/onsi/ginkgo"
    15  	"github.com/onsi/ginkgo/extensions/table"
    16  	. "github.com/onsi/gomega"
    17  
    18  	"github.com/sirupsen/logrus"
    19  	batchv1 "k8s.io/api/batch/v1"
    20  	corev1 "k8s.io/api/core/v1"
    21  	rbacv1 "k8s.io/api/rbac/v1"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  	"k8s.io/apimachinery/pkg/watch"
    24  	"k8s.io/client-go/kubernetes"
    25  
    26  	"github.com/operator-framework/operator-registry/pkg/configmap"
    27  	unstructuredlib "github.com/operator-framework/operator-registry/pkg/lib/unstructured"
    28  	"github.com/operator-framework/operator-registry/test/e2e/ctx"
    29  )
    30  
    31  var builderCmd string
    32  
    33  const (
    34  	imageDirectory = "testdata/bundles/"
    35  )
    36  
    37  func Logf(format string, a ...interface{}) {
    38  	fmt.Fprintf(GinkgoWriter, "INFO: "+format+"\n", a...)
    39  }
    40  
    41  // checks command that it exists in $PATH, isn't a directory, and has executable permissions set
    42  func checkCommand(filename string) string {
    43  	path, err := exec.LookPath(filename)
    44  	if err != nil {
    45  		Logf("LookPath error: %v", err)
    46  		return ""
    47  	}
    48  
    49  	info, err := os.Stat(path)
    50  	if os.IsNotExist(err) {
    51  		Logf("IsNotExist error: %v", err)
    52  		return ""
    53  	}
    54  	if info.IsDir() {
    55  		return ""
    56  	}
    57  	perm := info.Mode()
    58  	if perm&0111 != 0x49 { // 000100100100 - execute bits set for user/group/everybody
    59  		Logf("permissions failure: %#v %#v", perm, perm&0111)
    60  		return ""
    61  	}
    62  
    63  	Logf("Using builder at '%v'\n", path)
    64  	return path
    65  }
    66  
    67  func init() {
    68  	logrus.SetOutput(GinkgoWriter)
    69  
    70  	if builderCmd = checkCommand("docker"); builderCmd != "" {
    71  		return
    72  	}
    73  	if builderCmd = checkCommand("podman"); builderCmd != "" {
    74  		return
    75  	}
    76  }
    77  
    78  func buildContainer(tag, dockerfilePath, dockerContext string, w io.Writer) {
    79  	cmd := exec.Command(builderCmd, "build", "-t", tag, "-f", dockerfilePath, dockerContext)
    80  	cmd.Stderr = w
    81  	cmd.Stdout = w
    82  	err := cmd.Run()
    83  	Expect(err).NotTo(HaveOccurred())
    84  }
    85  
    86  var _ = Describe("Launch bundle", func() {
    87  	namespace := "default"
    88  	initImage := dockerHost + "/olmtest/init-operator-manifest:test"
    89  
    90  	Context("Deploy bundle job", func() {
    91  		table.DescribeTable("should populate specified configmap", func(bundleName, bundleDirectory string, gzip bool) {
    92  			// these permissions are only necessary for the e2e (and not OLM using the feature)
    93  			By("configuring configmap service account")
    94  			kubeclient, err := kubernetes.NewForConfig(ctx.Ctx().RESTConfig())
    95  			Expect(err).NotTo(HaveOccurred())
    96  
    97  			roleName := "olm-dev-configmap-access"
    98  			roleBindingName := "olm-dev-configmap-access-binding"
    99  
   100  			_, err = kubeclient.RbacV1().Roles(namespace).Create(context.TODO(), &rbacv1.Role{
   101  				ObjectMeta: metav1.ObjectMeta{
   102  					Name:      roleName,
   103  					Namespace: namespace,
   104  				},
   105  				Rules: []rbacv1.PolicyRule{
   106  					{
   107  						APIGroups: []string{""},
   108  						Resources: []string{"configmaps"},
   109  						Verbs:     []string{"create", "get", "update"},
   110  					},
   111  				},
   112  			}, metav1.CreateOptions{})
   113  			Expect(err).NotTo(HaveOccurred())
   114  
   115  			_, err = kubeclient.RbacV1().RoleBindings(namespace).Create(context.TODO(), &rbacv1.RoleBinding{
   116  				ObjectMeta: metav1.ObjectMeta{
   117  					Name:      roleBindingName,
   118  					Namespace: namespace,
   119  				},
   120  				Subjects: []rbacv1.Subject{
   121  					{
   122  						APIGroup:  "",
   123  						Kind:      "ServiceAccount",
   124  						Name:      "default",
   125  						Namespace: namespace,
   126  					},
   127  				},
   128  				RoleRef: rbacv1.RoleRef{
   129  					APIGroup: "rbac.authorization.k8s.io",
   130  					Kind:     "Role",
   131  					Name:     roleName,
   132  				},
   133  			}, metav1.CreateOptions{})
   134  			Expect(err).NotTo(HaveOccurred())
   135  
   136  			By("building required images")
   137  			bundleImage := dockerHost + "/olmtest/" + bundleName + ":test"
   138  			buildContainer(initImage, imageDirectory+"serve.Dockerfile", "../../bin", GinkgoWriter)
   139  			buildContainer(bundleImage, imageDirectory+"bundle.Dockerfile", bundleDirectory, GinkgoWriter)
   140  
   141  			err = pushLoadImages(kubeclient, GinkgoWriter, initImage, bundleImage)
   142  			Expect(err).ToNot(HaveOccurred(), "error loading required images into cluster")
   143  
   144  			By("creating a batch job")
   145  			bundleDataConfigMap, job, err := configmap.LaunchBundleImage(kubeclient, bundleImage, initImage, namespace, gzip)
   146  			Expect(err).NotTo(HaveOccurred())
   147  
   148  			// wait for job to complete
   149  			jobWatcher, err := kubeclient.BatchV1().Jobs(namespace).Watch(context.TODO(), metav1.ListOptions{})
   150  			Expect(err).NotTo(HaveOccurred())
   151  
   152  			done := make(chan struct{})
   153  			quit := make(chan struct{})
   154  			defer close(quit)
   155  			go func() {
   156  				for {
   157  					select {
   158  					case <-quit:
   159  						return
   160  					case evt, ok := <-jobWatcher.ResultChan():
   161  						if !ok {
   162  							Logf("watch channel closed unexpectedly")
   163  							return
   164  						}
   165  						if evt.Type == watch.Modified {
   166  							job, ok := evt.Object.(*batchv1.Job)
   167  							if !ok {
   168  								continue
   169  							}
   170  							for _, condition := range job.Status.Conditions {
   171  								if condition.Type == batchv1.JobComplete && condition.Status == corev1.ConditionTrue {
   172  									Logf("Job complete")
   173  									done <- struct{}{}
   174  								}
   175  							}
   176  						}
   177  					case <-time.After(120 * time.Second):
   178  						Logf("Timeout waiting for job to complete")
   179  						done <- struct{}{}
   180  					}
   181  				}
   182  			}()
   183  
   184  			Logf("Waiting on job to update status")
   185  			<-done
   186  
   187  			pl, err := kubeclient.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: metav1.FormatLabelSelector(job.Spec.Selector)})
   188  			Expect(err).NotTo(HaveOccurred())
   189  			for _, pod := range pl.Items {
   190  				logs, err := kubeclient.CoreV1().Pods(namespace).GetLogs(pod.GetName(), &corev1.PodLogOptions{}).Stream(context.Background())
   191  				Expect(err).NotTo(HaveOccurred())
   192  				logData, err := ioutil.ReadAll(logs)
   193  				Expect(err).NotTo(HaveOccurred())
   194  				Logf("Pod logs for unpack job pod %q:\n%s", pod.GetName(), string(logData))
   195  			}
   196  
   197  			bundleDataConfigMap, err = kubeclient.CoreV1().ConfigMaps(namespace).Get(context.TODO(), bundleDataConfigMap.GetName(), metav1.GetOptions{})
   198  			Expect(err).NotTo(HaveOccurred())
   199  
   200  			bundle, err := configmap.NewBundleLoader().Load(bundleDataConfigMap)
   201  			Expect(err).NotTo(HaveOccurred())
   202  
   203  			expectedObjects, err := unstructuredlib.FromDir(bundleDirectory + "/manifests")
   204  			Expect(err).NotTo(HaveOccurred())
   205  
   206  			configMapObjects, err := unstructuredlib.FromBundle(bundle)
   207  			Expect(err).NotTo(HaveOccurred())
   208  
   209  			Expect(configMapObjects).To(ConsistOf(expectedObjects))
   210  
   211  			// clean up, perhaps better handled elsewhere
   212  			err = kubeclient.CoreV1().ConfigMaps(namespace).Delete(context.TODO(), bundleDataConfigMap.GetName(), metav1.DeleteOptions{})
   213  			Expect(err).NotTo(HaveOccurred())
   214  
   215  			// job deletion does not clean up underlying pods (but using kubectl will do the clean up)
   216  			pods, err := kubeclient.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: fmt.Sprintf("job-name=%s", job.GetName())})
   217  			Expect(err).NotTo(HaveOccurred())
   218  			err = kubeclient.CoreV1().Pods(namespace).Delete(context.TODO(), pods.Items[0].GetName(), metav1.DeleteOptions{})
   219  			Expect(err).NotTo(HaveOccurred())
   220  
   221  			err = kubeclient.BatchV1().Jobs(namespace).Delete(context.TODO(), job.GetName(), metav1.DeleteOptions{})
   222  			Expect(err).NotTo(HaveOccurred())
   223  
   224  			err = kubeclient.RbacV1().Roles(namespace).Delete(context.TODO(), roleName, metav1.DeleteOptions{})
   225  			Expect(err).NotTo(HaveOccurred())
   226  
   227  			err = kubeclient.RbacV1().RoleBindings(namespace).Delete(context.TODO(), roleBindingName, metav1.DeleteOptions{})
   228  			Expect(err).NotTo(HaveOccurred())
   229  		},
   230  
   231  			table.Entry("Small bundle, uncompressed", "kiali.1.4.2", "testdata/bundles/kiali.1.4.2", false),
   232  			table.Entry("Large bundle, compressed", "redis.0.4.0", "testdata/bundles/redis.0.4.0", true),
   233  		)
   234  	})
   235  })
   236  
   237  var kindControlPlaneNodeNameRegex = regexp.MustCompile("^kind-.*-control-plane$|^kind-control-plane$")
   238  
   239  func isKindCluster(client *kubernetes.Clientset) (bool, string, error) {
   240  	nodes, err := client.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
   241  	if err != nil {
   242  		// transient error accessing nodes in cluster
   243  		// return an error, failing the test
   244  		return false, "", fmt.Errorf("accessing nodes in cluster")
   245  	}
   246  
   247  	var kindNode *corev1.Node
   248  	for _, node := range nodes.Items {
   249  		if kindControlPlaneNodeNameRegex.MatchString(node.Name) {
   250  			kindNode = &node
   251  		}
   252  	}
   253  
   254  	if kindNode == nil {
   255  		return false, "", nil
   256  	}
   257  	// found a match... strip off -control-plane from name and return to caller
   258  	return true, strings.TrimSuffix(kindNode.Name, "-control-plane"), nil
   259  }
   260  
   261  // pushLoadImages loads the built image onto the target cluster, either by "kind load docker-image",
   262  // or by pushing to a registry.
   263  func pushLoadImages(client *kubernetes.Clientset, w io.Writer, images ...string) error {
   264  	kind, kindServerName, err := isKindCluster(client)
   265  	if err != nil {
   266  		return err
   267  	}
   268  
   269  	if kind {
   270  		for _, image := range images {
   271  			cmd := exec.Command("kind", "load", "docker-image", image, "--name", kindServerName)
   272  			cmd.Stderr = w
   273  			cmd.Stdout = w
   274  			err := cmd.Run()
   275  			if err != nil {
   276  				return err
   277  			}
   278  		}
   279  	} else {
   280  		for _, image := range images {
   281  			pushWith("docker", image)
   282  			if err != nil {
   283  				return err
   284  			}
   285  		}
   286  	}
   287  
   288  	return nil
   289  }