github.com/alloyci/alloy-runner@v1.0.1-0.20180222164613-925503ccafd6/executors/kubernetes/util_test.go (about)

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