k8s.io/kubernetes@v1.29.3/test/e2e/upgrades/apps/mysql.go (about)

     1  /*
     2  Copyright 2017 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 apps
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"net"
    25  	"net/http"
    26  	"path/filepath"
    27  	"strconv"
    28  	"time"
    29  
    30  	"github.com/onsi/ginkgo/v2"
    31  	"github.com/onsi/gomega"
    32  
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/util/version"
    35  	"k8s.io/apimachinery/pkg/util/wait"
    36  
    37  	"k8s.io/kubernetes/test/e2e/framework"
    38  	e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
    39  	e2estatefulset "k8s.io/kubernetes/test/e2e/framework/statefulset"
    40  	e2etestfiles "k8s.io/kubernetes/test/e2e/framework/testfiles"
    41  	"k8s.io/kubernetes/test/e2e/upgrades"
    42  )
    43  
    44  const mysqlManifestPath = "test/e2e/testing-manifests/statefulset/mysql-upgrade"
    45  
    46  // MySQLUpgradeTest implements an upgrade test harness that polls a replicated sql database.
    47  type MySQLUpgradeTest struct {
    48  	ip               string
    49  	successfulWrites int
    50  	nextWrite        int
    51  }
    52  
    53  // Name returns the tracking name of the test.
    54  func (MySQLUpgradeTest) Name() string { return "mysql-upgrade" }
    55  
    56  // Skip returns true when this test can be skipped.
    57  func (MySQLUpgradeTest) Skip(upgCtx upgrades.UpgradeContext) bool {
    58  	minVersion := version.MustParseSemantic("1.5.0")
    59  
    60  	for _, vCtx := range upgCtx.Versions {
    61  		if vCtx.Version.LessThan(minVersion) {
    62  			return true
    63  		}
    64  	}
    65  	return false
    66  }
    67  
    68  func mysqlKubectlCreate(ns, file string) {
    69  	data, err := e2etestfiles.Read(filepath.Join(mysqlManifestPath, file))
    70  	if err != nil {
    71  		framework.Fail(err.Error())
    72  	}
    73  	input := string(data)
    74  	e2ekubectl.RunKubectlOrDieInput(ns, input, "create", "-f", "-")
    75  }
    76  
    77  func (t *MySQLUpgradeTest) getServiceIP(ctx context.Context, f *framework.Framework, ns, svcName string) string {
    78  	svc, err := f.ClientSet.CoreV1().Services(ns).Get(ctx, svcName, metav1.GetOptions{})
    79  	framework.ExpectNoError(err)
    80  	ingress := svc.Status.LoadBalancer.Ingress
    81  	if len(ingress) == 0 {
    82  		return ""
    83  	}
    84  	return ingress[0].IP
    85  }
    86  
    87  // Setup creates a StatefulSet, HeadlessService, a Service to write to the db, and a Service to read
    88  // from the db. It then connects to the db with the write Service and populates the db with a table
    89  // and a few entries. Finally, it connects to the db with the read Service, and confirms the data is
    90  // available. The db connections are left open to be used later in the test.
    91  func (t *MySQLUpgradeTest) Setup(ctx context.Context, f *framework.Framework) {
    92  	ns := f.Namespace.Name
    93  	statefulsetPoll := 30 * time.Second
    94  	statefulsetTimeout := 10 * time.Minute
    95  
    96  	ginkgo.By("Creating a configmap")
    97  	mysqlKubectlCreate(ns, "configmap.yaml")
    98  
    99  	ginkgo.By("Creating a mysql StatefulSet")
   100  	e2estatefulset.CreateStatefulSet(ctx, f.ClientSet, mysqlManifestPath, ns)
   101  
   102  	ginkgo.By("Creating a mysql-test-server deployment")
   103  	mysqlKubectlCreate(ns, "tester.yaml")
   104  
   105  	ginkgo.By("Getting the ingress IPs from the test-service")
   106  	err := wait.PollUntilContextTimeout(ctx, statefulsetPoll, statefulsetTimeout, true, func(ctx context.Context) (bool, error) {
   107  		if t.ip = t.getServiceIP(ctx, f, ns, "test-server"); t.ip == "" {
   108  			return false, nil
   109  		}
   110  		if _, err := t.countNames(); err != nil {
   111  			framework.Logf("Service endpoint is up but isn't responding")
   112  			return false, nil
   113  		}
   114  		return true, nil
   115  	})
   116  	framework.ExpectNoError(err)
   117  	framework.Logf("Service endpoint is up")
   118  
   119  	ginkgo.By("Adding 2 names to the database")
   120  	err = t.addName(strconv.Itoa(t.nextWrite))
   121  	framework.ExpectNoError(err)
   122  	err = t.addName(strconv.Itoa(t.nextWrite))
   123  	framework.ExpectNoError(err)
   124  
   125  	ginkgo.By("Verifying that the 2 names have been inserted")
   126  	count, err := t.countNames()
   127  	framework.ExpectNoError(err)
   128  	gomega.Expect(count).To(gomega.Equal(2))
   129  }
   130  
   131  // Test continually polls the db using the read and write connections, inserting data, and checking
   132  // that all the data is readable.
   133  func (t *MySQLUpgradeTest) Test(ctx context.Context, f *framework.Framework, done <-chan struct{}, upgrade upgrades.UpgradeType) {
   134  	var writeSuccess, readSuccess, writeFailure, readFailure int
   135  	ginkgo.By("Continuously polling the database during upgrade.")
   136  	go wait.Until(func() {
   137  		_, err := t.countNames()
   138  		if err != nil {
   139  			framework.Logf("Error while trying to read data: %v", err)
   140  			readFailure++
   141  		} else {
   142  			readSuccess++
   143  		}
   144  	}, framework.Poll, done)
   145  
   146  	wait.Until(func() {
   147  		err := t.addName(strconv.Itoa(t.nextWrite))
   148  		if err != nil {
   149  			framework.Logf("Error while trying to write data: %v", err)
   150  			writeFailure++
   151  		} else {
   152  			writeSuccess++
   153  		}
   154  	}, framework.Poll, done)
   155  
   156  	t.successfulWrites = writeSuccess
   157  	framework.Logf("Successful reads: %d", readSuccess)
   158  	framework.Logf("Successful writes: %d", writeSuccess)
   159  	framework.Logf("Failed reads: %d", readFailure)
   160  	framework.Logf("Failed writes: %d", writeFailure)
   161  
   162  	// TODO: Not sure what the ratio defining a successful test run should be. At time of writing the
   163  	// test, failures only seem to happen when a race condition occurs (read/write starts, doesn't
   164  	// finish before upgrade interferes).
   165  
   166  	readRatio := float64(readSuccess) / float64(readSuccess+readFailure)
   167  	writeRatio := float64(writeSuccess) / float64(writeSuccess+writeFailure)
   168  	if readRatio < 0.75 {
   169  		framework.Failf("Too many failures reading data. Success ratio: %f", readRatio)
   170  	}
   171  	if writeRatio < 0.75 {
   172  		framework.Failf("Too many failures writing data. Success ratio: %f", writeRatio)
   173  	}
   174  }
   175  
   176  // Teardown performs one final check of the data's availability.
   177  func (t *MySQLUpgradeTest) Teardown(ctx context.Context, f *framework.Framework) {
   178  	count, err := t.countNames()
   179  	framework.ExpectNoError(err)
   180  	gomega.Expect(count).To(gomega.BeNumerically(">=", t.successfulWrites), "count is too small")
   181  }
   182  
   183  // addName adds a new value to the db.
   184  func (t *MySQLUpgradeTest) addName(name string) error {
   185  	val := map[string][]string{"name": {name}}
   186  	t.nextWrite++
   187  	r, err := http.PostForm(fmt.Sprintf("http://%s/addName", net.JoinHostPort(t.ip, "8080")), val)
   188  	if err != nil {
   189  		return err
   190  	}
   191  	defer r.Body.Close()
   192  	if r.StatusCode != http.StatusOK {
   193  		b, err := io.ReadAll(r.Body)
   194  		if err != nil {
   195  			return err
   196  		}
   197  		return fmt.Errorf(string(b))
   198  	}
   199  	return nil
   200  }
   201  
   202  // countNames checks to make sure the values in testing.users are available, and returns
   203  // the count of them.
   204  func (t *MySQLUpgradeTest) countNames() (int, error) {
   205  	r, err := http.Get(fmt.Sprintf("http://%s/countNames", net.JoinHostPort(t.ip, "8080")))
   206  	if err != nil {
   207  		return 0, err
   208  	}
   209  	defer r.Body.Close()
   210  	if r.StatusCode != http.StatusOK {
   211  		b, err := io.ReadAll(r.Body)
   212  		if err != nil {
   213  			return 0, err
   214  		}
   215  		return 0, fmt.Errorf(string(b))
   216  	}
   217  	var count int
   218  	if err := json.NewDecoder(r.Body).Decode(&count); err != nil {
   219  		return 0, err
   220  	}
   221  	return count, nil
   222  }