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 }