diff options
-rw-r--r-- | internal/source/gitlab/api/client.go | 5 | ||||
-rw-r--r-- | internal/source/gitlab/api/resolver.go | 5 | ||||
-rw-r--r-- | internal/source/gitlab/cache/cache.go | 5 | ||||
-rw-r--r-- | internal/source/gitlab/cache/cache_test.go | 4 | ||||
-rw-r--r-- | internal/source/gitlab/client/client.go | 21 | ||||
-rw-r--r-- | internal/source/gitlab/client/client_stub.go | 7 | ||||
-rw-r--r-- | internal/source/gitlab/client/client_test.go | 82 | ||||
-rw-r--r-- | internal/source/gitlab/gitlab.go | 14 | ||||
-rw-r--r-- | internal/source/gitlab/gitlab_poll.go | 38 | ||||
-rw-r--r-- | internal/source/gitlab/gitlab_poll_test.go | 79 |
10 files changed, 254 insertions, 6 deletions
diff --git a/internal/source/gitlab/api/client.go b/internal/source/gitlab/api/client.go index 7206e25a..181c580b 100644 --- a/internal/source/gitlab/api/client.go +++ b/internal/source/gitlab/api/client.go @@ -6,6 +6,9 @@ import ( // Client represents an interface we use to retrieve information from GitLab type Client interface { - // GetLookup retrives an VirtualDomain from GitLab API and wraps it into Lookup + // Resolve retrieves an VirtualDomain from the GitLab API and wraps it into a Lookup GetLookup(ctx context.Context, domain string) Lookup + + // Status checks the connectivity with the GitLab API + Status() error } diff --git a/internal/source/gitlab/api/resolver.go b/internal/source/gitlab/api/resolver.go index 061a1ddd..738278e2 100644 --- a/internal/source/gitlab/api/resolver.go +++ b/internal/source/gitlab/api/resolver.go @@ -7,6 +7,9 @@ import ( // Resolver represents an interface we use to retrieve information from GitLab // in a more generic way. It can be a concrete API client or cached client. type Resolver interface { - // Resolve retrives an VirtualDomain from GitLab API and wraps it into Lookup + // Resolve retrieves an VirtualDomain from the GitLab API and wraps it into a Lookup Resolve(ctx context.Context, domain string) *Lookup + + // Status checks the connectivity with the GitLab API + Status() error } diff --git a/internal/source/gitlab/cache/cache.go b/internal/source/gitlab/cache/cache.go index c8d166b5..37cef111 100644 --- a/internal/source/gitlab/cache/cache.go +++ b/internal/source/gitlab/cache/cache.go @@ -109,3 +109,8 @@ func (c *Cache) Resolve(ctx context.Context, domain string) *api.Lookup { metrics.DomainsSourceCacheMiss.Inc() return entry.Retrieve(ctx, c.client) } + +// Status calls the client Status to check connectivity with the API +func (c *Cache) Status() error { + return c.client.Status() +} diff --git a/internal/source/gitlab/cache/cache_test.go b/internal/source/gitlab/cache/cache_test.go index 7a12cd3b..52a3a489 100644 --- a/internal/source/gitlab/cache/cache_test.go +++ b/internal/source/gitlab/cache/cache_test.go @@ -69,6 +69,10 @@ func (c *client) GetLookup(ctx context.Context, _ string) api.Lookup { return lookup } +func (c *client) Status() error { + return nil +} + func withTestCache(config resolverConfig, cacheConfig *cacheConfig, block func(*Cache, *client)) { var chanSize int diff --git a/internal/source/gitlab/client/client.go b/internal/source/gitlab/client/client.go index 3a805ce9..e06a87d1 100644 --- a/internal/source/gitlab/client/client.go +++ b/internal/source/gitlab/client/client.go @@ -18,6 +18,11 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/metrics" ) +// ConnectionErrorMsg to be returned with `gc.Status` if Pages +// fails to connect to the internal GitLab API, times out +// or a 401 given that the credentials used are wrong +const ConnectionErrorMsg = "failed to connect to internal Pages API" + // Client is a HTTP client to access Pages internal API type Client struct { secretKey []byte @@ -100,6 +105,22 @@ func (gc *Client) GetLookup(ctx context.Context, host string) api.Lookup { return lookup } +// Status checks that Pages can reach the rails internal Pages API +// for source domain configuration. +// Timeout is the same as -gitlab-client-http-timeout +func (gc *Client) Status() error { + res, err := gc.get(context.Background(), "/api/v4/internal/pages/status", url.Values{}) + if err != nil { + return fmt.Errorf("%s: %v", ConnectionErrorMsg, err) + } + + if res != nil && res.Body != nil { + res.Body.Close() + } + + return nil +} + func (gc *Client) get(ctx context.Context, path string, params url.Values) (*http.Response, error) { endpoint, err := gc.endpoint(path, params) if err != nil { diff --git a/internal/source/gitlab/client/client_stub.go b/internal/source/gitlab/client/client_stub.go index 604f127b..de6161e6 100644 --- a/internal/source/gitlab/client/client_stub.go +++ b/internal/source/gitlab/client/client_stub.go @@ -10,7 +10,8 @@ import ( // StubClient is a stubbed client used for testing type StubClient struct { - File string + File string + StatusErr func() error } // Resolve implements api.Resolver @@ -35,3 +36,7 @@ func (c StubClient) GetLookup(ctx context.Context, host string) api.Lookup { return lookup } + +func (c StubClient) Status() error { + return c.StatusErr() +} diff --git a/internal/source/gitlab/client/client_test.go b/internal/source/gitlab/client/client_test.go index 02684860..ab90b474 100644 --- a/internal/source/gitlab/client/client_test.go +++ b/internal/source/gitlab/client/client_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - jwt "github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitlab-pages/internal/fixture" @@ -231,6 +231,86 @@ func TestGetVirtualDomainAuthenticatedRequest(t *testing.T) { require.Equal(t, "mygroup/myproject/public/", lookupPath.Source.Path) } +func TestClientStatus(t *testing.T) { + tests := []struct { + name string + status int + wantErr bool + }{ + { + name: "api_enabled", + status: http.StatusNoContent, + }, + { + name: "api_unauthorized", + status: http.StatusUnauthorized, + wantErr: true, + }, + { + name: "server_error", + status: http.StatusInternalServerError, + wantErr: true, + }, + { + name: "gateway_timeout", + status: http.StatusGatewayTimeout, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/v4/internal/pages/status", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.status) + }) + + server := httptest.NewServer(mux) + defer server.Close() + + client := defaultClient(t, server.URL) + + err := client.Status() + if tt.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), ConnectionErrorMsg) + return + } + + require.NoError(t, err) + }) + } +} + +func TestClientStatusClientTimeout(t *testing.T) { + timeout := 3 * time.Millisecond + + mux := http.NewServeMux() + mux.HandleFunc("/api/v4/internal/pages/status", func(w http.ResponseWriter, r *http.Request) { + time.Sleep(timeout * 3) + + w.WriteHeader(http.StatusOK) + }) + + server := httptest.NewServer(mux) + defer server.Close() + + client := defaultClient(t, server.URL) + client.httpClient.Timeout = timeout + + err := client.Status() + require.Error(t, err) + require.Contains(t, err.Error(), "Client.Timeout") +} + +func TestClientStatusConnectionRefused(t *testing.T) { + client := defaultClient(t, "http://localhost:1234") + + err := client.Status() + require.Error(t, err) + require.Contains(t, err.Error(), "connection refused") +} + func validateToken(t *testing.T, tokenString string) { t.Helper() token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { diff --git a/internal/source/gitlab/gitlab.go b/internal/source/gitlab/gitlab.go index 12da9af1..5bacb603 100644 --- a/internal/source/gitlab/gitlab.go +++ b/internal/source/gitlab/gitlab.go @@ -6,6 +6,7 @@ import ( "net/http" "path" "strings" + "sync" "gitlab.com/gitlab-org/gitlab-pages/internal/domain" "gitlab.com/gitlab-org/gitlab-pages/internal/request" @@ -18,7 +19,9 @@ import ( // Gitlab source represent a new domains configuration source. We fetch all the // information about domains from GitLab instance. type Gitlab struct { - client api.Resolver + client api.Resolver + mu *sync.RWMutex + isReady bool } // New returns a new instance of gitlab domain source. @@ -28,8 +31,15 @@ func New(config client.Config) (*Gitlab, error) { return nil, err } + g := &Gitlab{ + client: cache.NewCache(client, nil), + mu: &sync.RWMutex{}, + } + + go g.poll(defaultPollingMaxRetries, defaultPollingInterval) + // using nil for cache config will use the default values specified in internal/source/gitlab/cache/cache.go#12 - return &Gitlab{client: cache.NewCache(client, nil)}, nil + return g, nil } // GetDomain return a representation of a domain that we have fetched from diff --git a/internal/source/gitlab/gitlab_poll.go b/internal/source/gitlab/gitlab_poll.go new file mode 100644 index 00000000..70644822 --- /dev/null +++ b/internal/source/gitlab/gitlab_poll.go @@ -0,0 +1,38 @@ +package gitlab + +import ( + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + // defaultPollingMaxRetries to be used by poll + defaultPollingMaxRetries = 30 + // defaultPollingInterval to be used by poll + defaultPollingInterval = time.Minute +) + +// poll tries to call the /internal/pages/status API endpoint once plus +// `retries` every `interval`. +// TODO: Remove in https://gitlab.com/gitlab-org/gitlab/-/issues/218357 +func (g *Gitlab) poll(retries int, interval time.Duration) { + var err error + for i := 0; i <= retries; i++ { + log.Info("Checking GitLab internal API availability") + err = g.client.Status() + if err == nil { + log.Info("GitLab internal pages status API connected successfully") + g.mu.Lock() + g.isReady = true + g.mu.Unlock() + + // return as soon as we connect to the API + return + } + + time.Sleep(interval) + } + + log.WithError(err).Errorf("Failed to connect to the internal GitLab API after %d tries every %.2fs", retries+1, interval.Seconds()) +} diff --git a/internal/source/gitlab/gitlab_poll_test.go b/internal/source/gitlab/gitlab_poll_test.go new file mode 100644 index 00000000..01f13846 --- /dev/null +++ b/internal/source/gitlab/gitlab_poll_test.go @@ -0,0 +1,79 @@ +package gitlab + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/client" +) + +func TestClient_Poll(t *testing.T) { + hook := test.NewGlobal() + tests := []struct { + name string + retries int + interval time.Duration + expectedFail bool + }{ + { + name: "success_with_no_retry", + retries: 0, + interval: 5 * time.Millisecond, + expectedFail: false, + }, + { + name: "success_after_N_retries", + retries: 3, + interval: 10 * time.Millisecond, + expectedFail: false, + }, + { + name: "fail_with_no_retries", + retries: 0, + interval: 5 * time.Millisecond, + expectedFail: true, + }, + { + name: "fail_after_N_retries", + retries: 3, + interval: 5 * time.Millisecond, + expectedFail: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer hook.Reset() + var counter int + client := client.StubClient{StatusErr: func() error { + if tt.expectedFail { + return fmt.Errorf(client.ConnectionErrorMsg) + } + + if counter < tt.retries { + counter++ + return fmt.Errorf(client.ConnectionErrorMsg) + } + + return nil + }} + + glClient := Gitlab{client: client, mu: &sync.RWMutex{}} + + glClient.poll(tt.retries, tt.interval) + if tt.expectedFail { + require.False(t, glClient.isReady) + s := fmt.Sprintf("Failed to connect to the internal GitLab API after %d tries every %.2fs", tt.retries+1, tt.interval.Seconds()) + require.Equal(t, s, hook.LastEntry().Message) + return + } + + require.True(t, glClient.isReady) + require.Equal(t, "GitLab internal pages status API connected successfully", hook.LastEntry().Message) + }) + } +} |