github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/kubernetes/util_test.go (about)

     1  package kubernetes
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"golang.org/x/net/context"
    16  
    17  	api "k8s.io/api/core/v1"
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	"k8s.io/apimachinery/pkg/runtime"
    20  	"k8s.io/apimachinery/pkg/runtime/schema"
    21  	runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
    22  	"k8s.io/client-go/kubernetes"
    23  	restclient "k8s.io/client-go/rest"
    24  	"k8s.io/client-go/rest/fake"
    25  
    26  	"gitlab.com/gitlab-org/gitlab-runner/common"
    27  )
    28  
    29  func TestGetKubeClientConfig(t *testing.T) {
    30  	originalInClusterConfig := inClusterConfig
    31  	originalDefaultKubectlConfig := defaultKubectlConfig
    32  	defer func() {
    33  		inClusterConfig = originalInClusterConfig
    34  		defaultKubectlConfig = originalDefaultKubectlConfig
    35  	}()
    36  
    37  	completeConfig := &restclient.Config{
    38  		Host:        "host",
    39  		BearerToken: "token",
    40  		TLSClientConfig: restclient.TLSClientConfig{
    41  			CAFile: "ca",
    42  		},
    43  		UserAgent: common.AppVersion.UserAgent(),
    44  	}
    45  
    46  	noConfigAvailable := func() (*restclient.Config, error) {
    47  		return nil, fmt.Errorf("config not available")
    48  	}
    49  
    50  	aConfig := func() (*restclient.Config, error) {
    51  		config := *completeConfig
    52  		return &config, nil
    53  
    54  	}
    55  
    56  	tests := []struct {
    57  		name                 string
    58  		config               *common.KubernetesConfig
    59  		overwrites           *overwrites
    60  		inClusterConfig      kubeConfigProvider
    61  		defaultKubectlConfig kubeConfigProvider
    62  		error                bool
    63  		expected             *restclient.Config
    64  	}{
    65  		{
    66  			name: "Incomplete cert based auth outside cluster",
    67  			config: &common.KubernetesConfig{
    68  				Host:     "host",
    69  				CertFile: "test",
    70  			},
    71  			inClusterConfig:      noConfigAvailable,
    72  			defaultKubectlConfig: noConfigAvailable,
    73  			overwrites:           &overwrites{},
    74  			error:                true,
    75  		},
    76  		{
    77  			name: "Complete cert based auth take precedence over in cluster config",
    78  			config: &common.KubernetesConfig{
    79  				CertFile: "crt",
    80  				KeyFile:  "key",
    81  				CAFile:   "ca",
    82  				Host:     "another_host",
    83  			},
    84  			overwrites:           &overwrites{},
    85  			inClusterConfig:      aConfig,
    86  			defaultKubectlConfig: aConfig,
    87  			expected: &restclient.Config{
    88  				Host: "another_host",
    89  				TLSClientConfig: restclient.TLSClientConfig{
    90  					CertFile: "crt",
    91  					KeyFile:  "key",
    92  					CAFile:   "ca",
    93  				},
    94  				UserAgent: common.AppVersion.UserAgent(),
    95  			},
    96  		},
    97  		{
    98  			name: "User provided configuration take precedence",
    99  			config: &common.KubernetesConfig{
   100  				Host:   "another_host",
   101  				CAFile: "ca",
   102  			},
   103  			overwrites: &overwrites{
   104  				bearerToken: "another_token",
   105  			},
   106  			inClusterConfig:      aConfig,
   107  			defaultKubectlConfig: aConfig,
   108  			expected: &restclient.Config{
   109  				Host:        "another_host",
   110  				BearerToken: "another_token",
   111  				TLSClientConfig: restclient.TLSClientConfig{
   112  					CAFile: "ca",
   113  				},
   114  				UserAgent: common.AppVersion.UserAgent(),
   115  			},
   116  		},
   117  		{
   118  			name:                 "InCluster config",
   119  			config:               &common.KubernetesConfig{},
   120  			overwrites:           &overwrites{},
   121  			inClusterConfig:      aConfig,
   122  			defaultKubectlConfig: noConfigAvailable,
   123  			expected:             completeConfig,
   124  		},
   125  		{
   126  			name:                 "Default cluster config",
   127  			config:               &common.KubernetesConfig{},
   128  			overwrites:           &overwrites{},
   129  			inClusterConfig:      noConfigAvailable,
   130  			defaultKubectlConfig: aConfig,
   131  			expected:             completeConfig,
   132  		},
   133  		{
   134  			name:   "Overwrites works also in cluster",
   135  			config: &common.KubernetesConfig{},
   136  			overwrites: &overwrites{
   137  				bearerToken: "bearerToken",
   138  			},
   139  			inClusterConfig:      aConfig,
   140  			defaultKubectlConfig: noConfigAvailable,
   141  			expected: &restclient.Config{
   142  				Host:        "host",
   143  				BearerToken: "bearerToken",
   144  				TLSClientConfig: restclient.TLSClientConfig{
   145  					CAFile: "ca",
   146  				},
   147  				UserAgent: common.AppVersion.UserAgent(),
   148  			},
   149  		},
   150  	}
   151  	for _, test := range tests {
   152  		t.Run(test.name, func(t *testing.T) {
   153  			inClusterConfig = test.inClusterConfig
   154  			defaultKubectlConfig = test.defaultKubectlConfig
   155  
   156  			rcConf, err := getKubeClientConfig(test.config, test.overwrites)
   157  
   158  			if test.error {
   159  				require.Error(t, err)
   160  			} else {
   161  				require.NoError(t, err)
   162  			}
   163  
   164  			assert.Equal(t, test.expected, rcConf)
   165  		})
   166  	}
   167  }
   168  
   169  func TestWaitForPodRunning(t *testing.T) {
   170  	version, codec := testVersionAndCodec()
   171  	retries := 0
   172  
   173  	tests := []struct {
   174  		Name         string
   175  		Pod          *api.Pod
   176  		Config       *common.KubernetesConfig
   177  		ClientFunc   func(*http.Request) (*http.Response, error)
   178  		PodEndPhase  api.PodPhase
   179  		Retries      int
   180  		Error        bool
   181  		ExactRetries bool
   182  	}{
   183  		{
   184  			Name: "ensure function retries until ready",
   185  			Pod: &api.Pod{
   186  				ObjectMeta: metav1.ObjectMeta{
   187  					Name:      "test-pod",
   188  					Namespace: "test-ns",
   189  				},
   190  			},
   191  			Config: &common.KubernetesConfig{},
   192  			ClientFunc: func(req *http.Request) (*http.Response, error) {
   193  				switch p, m := req.URL.Path, req.Method; {
   194  				case p == "/api/"+version+"/namespaces/test-ns/pods/test-pod" && m == "GET":
   195  					pod := &api.Pod{
   196  						ObjectMeta: metav1.ObjectMeta{
   197  							Name:      "test-pod",
   198  							Namespace: "test-ns",
   199  						},
   200  						Status: api.PodStatus{
   201  							Phase: api.PodPending,
   202  						},
   203  					}
   204  
   205  					if retries > 1 {
   206  						pod.Status.Phase = api.PodRunning
   207  						pod.Status.ContainerStatuses = []api.ContainerStatus{
   208  							{
   209  								Ready: false,
   210  							},
   211  						}
   212  					}
   213  
   214  					if retries > 2 {
   215  						pod.Status.Phase = api.PodRunning
   216  						pod.Status.ContainerStatuses = []api.ContainerStatus{
   217  							{
   218  								Ready: true,
   219  							},
   220  						}
   221  					}
   222  					retries++
   223  					return &http.Response{StatusCode: http.StatusOK, Body: objBody(codec, pod), Header: map[string][]string{
   224  						"Content-Type": []string{"application/json"},
   225  					}}, nil
   226  				default:
   227  					// Ensures no GET is performed when deleting by name
   228  					t.Errorf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
   229  					return nil, fmt.Errorf("unexpected request")
   230  				}
   231  			},
   232  			PodEndPhase: api.PodRunning,
   233  			Retries:     2,
   234  		},
   235  		{
   236  			Name: "ensure function errors if pod already succeeded",
   237  			Pod: &api.Pod{
   238  				ObjectMeta: metav1.ObjectMeta{
   239  					Name:      "test-pod",
   240  					Namespace: "test-ns",
   241  				},
   242  			},
   243  			Config: &common.KubernetesConfig{},
   244  			ClientFunc: func(req *http.Request) (*http.Response, error) {
   245  				switch p, m := req.URL.Path, req.Method; {
   246  				case p == "/api/"+version+"/namespaces/test-ns/pods/test-pod" && m == "GET":
   247  					pod := &api.Pod{
   248  						ObjectMeta: metav1.ObjectMeta{
   249  							Name:      "test-pod",
   250  							Namespace: "test-ns",
   251  						},
   252  						Status: api.PodStatus{
   253  							Phase: api.PodSucceeded,
   254  						},
   255  					}
   256  					return &http.Response{StatusCode: http.StatusOK, Body: objBody(codec, pod), Header: map[string][]string{
   257  						"Content-Type": []string{"application/json"},
   258  					}}, nil
   259  				default:
   260  					// Ensures no GET is performed when deleting by name
   261  					t.Errorf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
   262  					return nil, fmt.Errorf("unexpected request")
   263  				}
   264  			},
   265  			Error:       true,
   266  			PodEndPhase: api.PodSucceeded,
   267  		},
   268  		{
   269  			Name: "ensure function returns error if pod unknown",
   270  			Pod: &api.Pod{
   271  				ObjectMeta: metav1.ObjectMeta{
   272  					Name:      "test-pod",
   273  					Namespace: "test-ns",
   274  				},
   275  			},
   276  			Config: &common.KubernetesConfig{},
   277  			ClientFunc: func(req *http.Request) (*http.Response, error) {
   278  				return nil, fmt.Errorf("error getting pod")
   279  			},
   280  			PodEndPhase: api.PodUnknown,
   281  			Error:       true,
   282  		},
   283  		{
   284  			Name: "ensure poll parameters work correctly",
   285  			Pod: &api.Pod{
   286  				ObjectMeta: metav1.ObjectMeta{
   287  					Name:      "test-pod",
   288  					Namespace: "test-ns",
   289  				},
   290  			},
   291  			// Will result in 3 attempts at 0, 3, and 6 seconds
   292  			Config: &common.KubernetesConfig{
   293  				PollInterval: 0, // Should get changed to default of 3 by GetPollInterval()
   294  				PollTimeout:  6,
   295  			},
   296  			ClientFunc: func(req *http.Request) (*http.Response, error) {
   297  				switch p, m := req.URL.Path, req.Method; {
   298  				case p == "/api/"+version+"/namespaces/test-ns/pods/test-pod" && m == "GET":
   299  					pod := &api.Pod{
   300  						ObjectMeta: metav1.ObjectMeta{
   301  							Name:      "test-pod",
   302  							Namespace: "test-ns",
   303  						},
   304  					}
   305  					if retries > 3 {
   306  						t.Errorf("Too many retries for the given poll parameters. (Expected 3)")
   307  					}
   308  					retries++
   309  					return &http.Response{StatusCode: http.StatusOK, Body: objBody(codec, pod), Header: map[string][]string{
   310  						"Content-Type": []string{"application/json"},
   311  					}}, nil
   312  				default:
   313  					// Ensures no GET is performed when deleting by name
   314  					t.Errorf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
   315  					return nil, fmt.Errorf("unexpected request")
   316  				}
   317  			},
   318  			PodEndPhase:  api.PodUnknown,
   319  			Retries:      3,
   320  			Error:        true,
   321  			ExactRetries: true,
   322  		},
   323  	}
   324  
   325  	for _, test := range tests {
   326  		t.Run(test.Name, func(t *testing.T) {
   327  			retries = 0
   328  			c := testKubernetesClient(version, fake.CreateHTTPClient(test.ClientFunc))
   329  
   330  			fw := testWriter{
   331  				call: func(b []byte) (int, error) {
   332  					if retries < test.Retries {
   333  						if !strings.Contains(string(b), "Waiting for pod") {
   334  							t.Errorf("[%s] Expected to continue waiting for pod. Got: '%s'", test.Name, string(b))
   335  						}
   336  					}
   337  					return len(b), nil
   338  				},
   339  			}
   340  			phase, err := waitForPodRunning(context.Background(), c, test.Pod, fw, test.Config)
   341  
   342  			if err != nil && !test.Error {
   343  				t.Errorf("[%s] Expected success. Got: %s", test.Name, err.Error())
   344  				return
   345  			}
   346  
   347  			if phase != test.PodEndPhase {
   348  				t.Errorf("[%s] Invalid end state. Expected '%v', got: '%v'", test.Name, test.PodEndPhase, phase)
   349  				return
   350  			}
   351  
   352  			if test.ExactRetries && retries < test.Retries {
   353  				t.Errorf("[%s] Not enough retries. Expected: %d, got: %d", test.Name, test.Retries, retries)
   354  				return
   355  			}
   356  		})
   357  	}
   358  }
   359  
   360  type testWriter struct {
   361  	call func([]byte) (int, error)
   362  }
   363  
   364  func (t testWriter) Write(b []byte) (int, error) {
   365  	return t.call(b)
   366  }
   367  
   368  func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser {
   369  	return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
   370  }
   371  
   372  func testKubernetesClient(version string, httpClient *http.Client) *kubernetes.Clientset {
   373  	conf := restclient.Config{
   374  		ContentConfig: restclient.ContentConfig{
   375  			GroupVersion: &schema.GroupVersion{Version: version},
   376  		},
   377  	}
   378  	kube := kubernetes.NewForConfigOrDie(&conf)
   379  	fakeClient := fake.RESTClient{Client: httpClient}
   380  	kube.CoreV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client
   381  	kube.ExtensionsV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client
   382  
   383  	return kube
   384  }
   385  
   386  // minimal port from k8s.io/kubernetes/pkg/testapi
   387  func testVersionAndCodec() (version string, codec runtime.Codec) {
   388  	scheme := runtime.NewScheme()
   389  
   390  	scheme.AddIgnoredConversionType(&metav1.TypeMeta{}, &metav1.TypeMeta{})
   391  	scheme.AddKnownTypes(
   392  		api.SchemeGroupVersion,
   393  		&api.Pod{},
   394  		&metav1.Status{},
   395  	)
   396  
   397  	codecs := runtimeserializer.NewCodecFactory(scheme)
   398  	codec = codecs.LegacyCodec(api.SchemeGroupVersion)
   399  	version = api.SchemeGroupVersion.Version
   400  
   401  	return
   402  }