...

Source file src/github.com/GoogleCloudPlatform/cloudsql-proxy/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck_test.go

Documentation: github.com/GoogleCloudPlatform/cloudsql-proxy/cmd/cloud_sql_proxy/internal/healthcheck

     1  // Copyright 2021 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package healthcheck_test
    16  
    17  import (
    18  	"context"
    19  	"crypto/tls"
    20  	"crypto/x509"
    21  	"errors"
    22  	"net"
    23  	"net/http"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/GoogleCloudPlatform/cloudsql-proxy/cmd/cloud_sql_proxy/internal/healthcheck"
    28  	"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy"
    29  )
    30  
    31  const (
    32  	startupPath   = "/startup"
    33  	livenessPath  = "/liveness"
    34  	readinessPath = "/readiness"
    35  	testPort      = "8090"
    36  )
    37  
    38  type fakeCertSource struct{}
    39  
    40  func (cs *fakeCertSource) Local(instance string) (tls.Certificate, error) {
    41  	return tls.Certificate{
    42  		Leaf: &x509.Certificate{
    43  			NotAfter: time.Date(9999, 0, 0, 0, 0, 0, 0, time.UTC),
    44  		},
    45  	}, nil
    46  }
    47  
    48  func (cs *fakeCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) {
    49  	return &x509.Certificate{}, "fake address", "fake name", "fake version", nil
    50  }
    51  
    52  type failingCertSource struct{}
    53  
    54  func (cs *failingCertSource) Local(instance string) (tls.Certificate, error) {
    55  	return tls.Certificate{}, errors.New("failed")
    56  }
    57  
    58  func (cs *failingCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) {
    59  	return nil, "", "", "", errors.New("failed")
    60  }
    61  
    62  // Test to verify that when the proxy client is up, the liveness endpoint writes http.StatusOK.
    63  func TestLivenessPasses(t *testing.T) {
    64  	s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil)
    65  	if err != nil {
    66  		t.Fatalf("Could not initialize health check: %v", err)
    67  	}
    68  	defer s.Close(context.Background())
    69  
    70  	resp, err := http.Get("http://localhost:" + testPort + livenessPath)
    71  	if err != nil {
    72  		t.Fatalf("HTTP GET failed: %v", err)
    73  	}
    74  	if resp.StatusCode != http.StatusOK {
    75  		t.Errorf("want %v, got %v", http.StatusOK, resp.StatusCode)
    76  	}
    77  }
    78  
    79  func TestLivenessFails(t *testing.T) {
    80  	c := &proxy.Client{
    81  		Certs: &failingCertSource{},
    82  		Dialer: func(string, string) (net.Conn, error) {
    83  			return nil, errors.New("error")
    84  		},
    85  	}
    86  	// ensure cache has errored config
    87  	_, err := c.Dial("proj:region:instance")
    88  	if err == nil {
    89  		t.Fatalf("expected Dial to fail, but it succeeded")
    90  	}
    91  
    92  	s, err := healthcheck.NewServer(c, testPort, []string{"proj:region:instance"})
    93  	if err != nil {
    94  		t.Fatalf("Could not initialize health check: %v", err)
    95  	}
    96  	defer s.Close(context.Background())
    97  
    98  	resp, err := http.Get("http://localhost:" + testPort + livenessPath)
    99  	if err != nil {
   100  		t.Fatalf("HTTP GET failed: %v", err)
   101  	}
   102  	defer resp.Body.Close()
   103  	want := http.StatusServiceUnavailable
   104  	if got := resp.StatusCode; got != want {
   105  		t.Errorf("want %v, got %v", want, got)
   106  	}
   107  }
   108  
   109  // Test to verify that when startup HAS finished (and MaxConnections limit not specified),
   110  // the startup and readiness endpoints write http.StatusOK.
   111  func TestStartupPass(t *testing.T) {
   112  	s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil)
   113  	if err != nil {
   114  		t.Fatalf("Could not initialize health check: %v", err)
   115  	}
   116  	defer s.Close(context.Background())
   117  
   118  	// Simulate the proxy client completing startup.
   119  	s.NotifyStarted()
   120  
   121  	resp, err := http.Get("http://localhost:" + testPort + startupPath)
   122  	if err != nil {
   123  		t.Fatalf("HTTP GET failed: %v", err)
   124  	}
   125  	if resp.StatusCode != http.StatusOK {
   126  		t.Errorf("%v: want %v, got %v", startupPath, http.StatusOK, resp.StatusCode)
   127  	}
   128  
   129  	resp, err = http.Get("http://localhost:" + testPort + readinessPath)
   130  	if err != nil {
   131  		t.Fatalf("HTTP GET failed: %v", err)
   132  	}
   133  	if resp.StatusCode != http.StatusOK {
   134  		t.Errorf("%v: want %v, got %v", readinessPath, http.StatusOK, resp.StatusCode)
   135  	}
   136  }
   137  
   138  // Test to verify that when startup has NOT finished, the startup and readiness endpoints write
   139  // http.StatusServiceUnavailable.
   140  func TestStartupFail(t *testing.T) {
   141  	s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil)
   142  	if err != nil {
   143  		t.Fatalf("Could not initialize health check: %v", err)
   144  	}
   145  	defer s.Close(context.Background())
   146  
   147  	resp, err := http.Get("http://localhost:" + testPort + startupPath)
   148  	if err != nil {
   149  		t.Fatalf("HTTP GET failed: %v", err)
   150  	}
   151  	if resp.StatusCode != http.StatusServiceUnavailable {
   152  		t.Errorf("%v: want %v, got %v", startupPath, http.StatusOK, resp.StatusCode)
   153  	}
   154  
   155  	resp, err = http.Get("http://localhost:" + testPort + readinessPath)
   156  	if err != nil {
   157  		t.Fatalf("HTTP GET failed: %v", err)
   158  	}
   159  	if resp.StatusCode != http.StatusServiceUnavailable {
   160  		t.Errorf("%v: want %v, got %v", readinessPath, http.StatusOK, resp.StatusCode)
   161  	}
   162  }
   163  
   164  // Test to verify that when startup has finished, but MaxConnections has been reached,
   165  // the readiness endpoint writes http.StatusServiceUnavailable.
   166  func TestMaxConnectionsReached(t *testing.T) {
   167  	c := &proxy.Client{
   168  		MaxConnections: 1,
   169  	}
   170  	s, err := healthcheck.NewServer(c, testPort, nil)
   171  	if err != nil {
   172  		t.Fatalf("Could not initialize health check: %v", err)
   173  	}
   174  	defer s.Close(context.Background())
   175  
   176  	s.NotifyStarted()
   177  	c.ConnectionsCounter = c.MaxConnections // Simulate reaching the limit for maximum number of connections
   178  
   179  	resp, err := http.Get("http://localhost:" + testPort + readinessPath)
   180  	if err != nil {
   181  		t.Fatalf("HTTP GET failed: %v", err)
   182  	}
   183  	if resp.StatusCode != http.StatusServiceUnavailable {
   184  		t.Errorf("want %v, got %v", http.StatusServiceUnavailable, resp.StatusCode)
   185  	}
   186  }
   187  
   188  // Test to verify that when dialing instance(s) returns an error, the readiness endpoint
   189  // writes http.StatusServiceUnavailable.
   190  func TestDialFail(t *testing.T) {
   191  	tests := map[string]struct {
   192  		insts []string
   193  	}{
   194  		"Single instance":    {insts: []string{"project:region:instance"}},
   195  		"Multiple instances": {insts: []string{"project:region:instance-1", "project:region:instance-2", "project:region:instance-3"}},
   196  	}
   197  
   198  	c := &proxy.Client{
   199  		Certs: &fakeCertSource{},
   200  		Dialer: func(string, string) (net.Conn, error) {
   201  			return nil, errors.New("error")
   202  		},
   203  	}
   204  
   205  	for name, test := range tests {
   206  		func() {
   207  			s, err := healthcheck.NewServer(c, testPort, test.insts)
   208  			if err != nil {
   209  				t.Fatalf("%v: Could not initialize health check: %v", name, err)
   210  			}
   211  			defer s.Close(context.Background())
   212  			s.NotifyStarted()
   213  
   214  			resp, err := http.Get("http://localhost:" + testPort + readinessPath)
   215  			if err != nil {
   216  				t.Fatalf("%v: HTTP GET failed: %v", name, err)
   217  			}
   218  			if resp.StatusCode != http.StatusServiceUnavailable {
   219  				t.Errorf("want %v, got %v", http.StatusServiceUnavailable, resp.StatusCode)
   220  			}
   221  		}()
   222  	}
   223  }
   224  
   225  // Test to verify that after closing a healthcheck, its liveness endpoint serves
   226  // an error.
   227  func TestCloseHealthCheck(t *testing.T) {
   228  	s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil)
   229  	if err != nil {
   230  		t.Fatalf("Could not initialize health check: %v", err)
   231  	}
   232  	defer s.Close(context.Background())
   233  
   234  	resp, err := http.Get("http://localhost:" + testPort + livenessPath)
   235  	if err != nil {
   236  		t.Fatalf("HTTP GET failed: %v", err)
   237  	}
   238  	if resp.StatusCode != http.StatusOK {
   239  		t.Errorf("want %v, got %v", http.StatusOK, resp.StatusCode)
   240  	}
   241  
   242  	err = s.Close(context.Background())
   243  	if err != nil {
   244  		t.Fatalf("Failed to close health check: %v", err)
   245  	}
   246  
   247  	_, err = http.Get("http://localhost:" + testPort + livenessPath)
   248  	if err == nil {
   249  		t.Fatalf("HTTP GET did not return error after closing health check server.")
   250  	}
   251  }
   252  

View as plain text