diff options
author | Patrick Steinhardt <psteinhardt@gitlab.com> | 2021-04-26 11:50:56 +0300 |
---|---|---|
committer | Patrick Steinhardt <psteinhardt@gitlab.com> | 2021-04-27 12:13:12 +0300 |
commit | 447b5185c005ffded1ea515f6fd43544a59c9c2e (patch) | |
tree | 8cffd298b230cb2186e4572c86e344fa87448bf6 | |
parent | fe6b500dd60fc6efdca1397bc9abe487a4723650 (diff) |
gitlab: Merge client and API implementations
The HTTPClient and the internal API implementation are currently split
up across two different times, even though the latter defers everything
to the HTTPClient. This separation of concerns is at times a bit weird,
and doesn't really help to understand the code.
Merge these two types into HTTPClient to lift this artificial
restriction.
-rw-r--r-- | cmd/gitaly-hooks/hooks.go | 2 | ||||
-rw-r--r-- | cmd/gitaly-hooks/hooks_test.go | 6 | ||||
-rw-r--r-- | cmd/gitaly/main.go | 2 | ||||
-rw-r--r-- | internal/gitaly/service/hook/post_receive_test.go | 4 | ||||
-rw-r--r-- | internal/gitaly/service/hook/pre_receive_test.go | 8 | ||||
-rw-r--r-- | internal/gitlab/access.go | 243 | ||||
-rw-r--r-- | internal/gitlab/check.go | 50 | ||||
-rw-r--r-- | internal/gitlab/http_client.go | 233 | ||||
-rw-r--r-- | internal/gitlab/http_client_test.go (renamed from internal/gitlab/access_test.go) | 10 |
9 files changed, 267 insertions, 291 deletions
diff --git a/cmd/gitaly-hooks/hooks.go b/cmd/gitaly-hooks/hooks.go index 4f4814b69..822a8947b 100644 --- a/cmd/gitaly-hooks/hooks.go +++ b/cmd/gitaly-hooks/hooks.go @@ -182,7 +182,7 @@ func check(configPath string) (*gitlab.CheckInfo, error) { return nil, err } - gitlabAPI, err := gitlab.NewGitlabAPI(cfg.Gitlab, cfg.TLS) + gitlabAPI, err := gitlab.NewHTTPClient(cfg.Gitlab, cfg.TLS) if err != nil { return nil, err } diff --git a/cmd/gitaly-hooks/hooks_test.go b/cmd/gitaly-hooks/hooks_test.go index ba04d7f20..3ca038d36 100644 --- a/cmd/gitaly-hooks/hooks_test.go +++ b/cmd/gitaly-hooks/hooks_test.go @@ -172,7 +172,7 @@ func testHooksPrePostReceive(t *testing.T, cfg config.Cfg, repo *gitalypb.Reposi t.Run(fmt.Sprintf("hookName: %s", hookName), func(t *testing.T) { customHookOutputPath := gittest.WriteEnvToCustomHook(t, repoPath, hookName) - gitlabAPI, err := gitlab.NewGitlabAPI(cfg.Gitlab, cfg.TLS) + gitlabAPI, err := gitlab.NewHTTPClient(cfg.Gitlab, cfg.TLS) require.NoError(t, err) stop := runHookServiceServerWithAPI(t, cfg, gitlabAPI) @@ -351,7 +351,7 @@ func TestHooksPostReceiveFailed(t *testing.T) { cfg.Gitlab.URL = serverURL cfg.Gitlab.SecretFile = testhelper.WriteShellSecretFile(t, cfg.GitlabShell.Dir, secretToken) - gitlabAPI, err := gitlab.NewGitlabAPI(cfg.Gitlab, cfg.TLS) + gitlabAPI, err := gitlab.NewHTTPClient(cfg.Gitlab, cfg.TLS) require.NoError(t, err) customHookOutputPath := gittest.WriteEnvToCustomHook(t, repoPath, "post-receive") @@ -465,7 +465,7 @@ func TestHooksNotAllowed(t *testing.T) { customHookOutputPath := gittest.WriteEnvToCustomHook(t, repoPath, "post-receive") - gitlabAPI, err := gitlab.NewGitlabAPI(cfg.Gitlab, cfg.TLS) + gitlabAPI, err := gitlab.NewHTTPClient(cfg.Gitlab, cfg.TLS) require.NoError(t, err) stop := runHookServiceServerWithAPI(t, cfg, gitlabAPI) diff --git a/cmd/gitaly/main.go b/cmd/gitaly/main.go index 1b05f05f5..8a186ff0f 100644 --- a/cmd/gitaly/main.go +++ b/cmd/gitaly/main.go @@ -147,7 +147,7 @@ func run(cfg config.Cfg) error { if config.SkipHooks() { log.Warn("skipping GitLab API client creation since hooks are bypassed via GITALY_TESTING_NO_GIT_HOOKS") } else { - gitlabAPI, err := gitlab.NewGitlabAPI(cfg.Gitlab, cfg.TLS) + gitlabAPI, err := gitlab.NewHTTPClient(cfg.Gitlab, cfg.TLS) if err != nil { return fmt.Errorf("could not create GitLab API client: %w", err) } diff --git a/internal/gitaly/service/hook/post_receive_test.go b/internal/gitaly/service/hook/post_receive_test.go index fda796537..215a0c4fc 100644 --- a/internal/gitaly/service/hook/post_receive_test.go +++ b/internal/gitaly/service/hook/post_receive_test.go @@ -66,7 +66,7 @@ func TestHooksMissingStdin(t *testing.T) { }, } - api, err := gitlab.NewGitlabAPI(cfg.Gitlab, cfg.TLS) + api, err := gitlab.NewHTTPClient(cfg.Gitlab, cfg.TLS) require.NoError(t, err) testCases := []struct { @@ -217,7 +217,7 @@ To create a merge request for okay, visit: }, } - api, err := gitlab.NewGitlabAPI(cfg.Gitlab, cfg.TLS) + api, err := gitlab.NewHTTPClient(cfg.Gitlab, cfg.TLS) require.NoError(t, err) serverSocketPath := runHooksServer(t, cfg, nil, testserver.WithGitLabAPI(api)) diff --git a/internal/gitaly/service/hook/pre_receive_test.go b/internal/gitaly/service/hook/pre_receive_test.go index 4e8895b1d..5cd13494d 100644 --- a/internal/gitaly/service/hook/pre_receive_test.go +++ b/internal/gitaly/service/hook/pre_receive_test.go @@ -132,7 +132,7 @@ func TestPreReceiveHook_GitlabAPIAccess(t *testing.T) { SecretFile: secretFilePath, } - gitlabAPI, err := gitlab.NewGitlabAPI(gitlabConfig, cfg.TLS) + gitlabAPI, err := gitlab.NewHTTPClient(gitlabConfig, cfg.TLS) require.NoError(t, err) serverSocketPath := runHooksServer(t, cfg, nil, testserver.WithGitLabAPI(gitlabAPI)) @@ -248,7 +248,7 @@ func TestPreReceive_APIErrors(t *testing.T) { SecretFile: secretFilePath, } - gitlabAPI, err := gitlab.NewGitlabAPI(gitlabConfig, cfg.TLS) + gitlabAPI, err := gitlab.NewHTTPClient(gitlabConfig, cfg.TLS) require.NoError(t, err) serverSocketPath := runHooksServer(t, cfg, nil, testserver.WithGitLabAPI(gitlabAPI)) @@ -321,7 +321,7 @@ exit %d SecretFile: secretFilePath, } - gitlabAPI, err := gitlab.NewGitlabAPI(gitlabConfig, cfg.TLS) + gitlabAPI, err := gitlab.NewHTTPClient(gitlabConfig, cfg.TLS) require.NoError(t, err) serverSocketPath := runHooksServer(t, cfg, nil, testserver.WithGitLabAPI(gitlabAPI)) @@ -446,7 +446,7 @@ func TestPreReceiveHook_Primary(t *testing.T) { gittest.WriteCustomHook(t, testRepoPath, "pre-receive", []byte(fmt.Sprintf("#!/bin/bash\nexit %d", tc.hookExitCode))) - gitlabAPI, err := gitlab.NewGitlabAPI(config.Gitlab{ + gitlabAPI, err := gitlab.NewHTTPClient(config.Gitlab{ URL: srv.URL, SecretFile: secretFilePath, }, cfg.TLS) diff --git a/internal/gitlab/access.go b/internal/gitlab/access.go index 1547693e4..5a672130b 100644 --- a/internal/gitlab/access.go +++ b/internal/gitlab/access.go @@ -2,53 +2,8 @@ package gitlab import ( "context" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "mime" - "net/http" - "regexp" - "strings" - - "gitlab.com/gitlab-org/gitaly/internal/gitaly/config" - "gitlab.com/gitlab-org/gitlab-shell/client" ) -// AllowedResponse is a response for the internal gitlab api's /allowed endpoint with a subset -// of fields -type AllowedResponse struct { - Status bool `json:"status"` - Message string `json:"message"` -} - -// AllowedRequest is a request for the internal gitlab api /allowed endpoint -type AllowedRequest struct { - Action string `json:"action,omitempty"` - GLRepository string `json:"gl_repository,omitempty"` - Project string `json:"project,omitempty"` - Changes string `json:"changes,omitempty"` - Protocol string `json:"protocol,omitempty"` - Env string `json:"env,omitempty"` - Username string `json:"username,omitempty"` - KeyID string `json:"key_id,omitempty"` - UserID string `json:"user_id,omitempty"` -} - -// marshallGitObjectDirs generates a json encoded string containing GIT_OBJECT_DIRECTORY_RELATIVE, and GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE -func marshallGitObjectDirs(gitObjectDirRel string, gitAltObjectDirsRel []string) (string, error) { - envString, err := json.Marshal(map[string]interface{}{ - "GIT_OBJECT_DIRECTORY_RELATIVE": gitObjectDirRel, - "GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE": gitAltObjectDirsRel, - }) - - if err != nil { - return "", err - } - - return string(envString), nil -} - // AllowedParams compose set of parameters required to call 'GitlabAPI.Allowed' method. type AllowedParams struct { // RepoPath is an absolute path to the repository. @@ -67,6 +22,26 @@ type AllowedParams struct { Changes string } +// PostReceiveMessage encapsulates a message from the /post_receive endpoint that gets printed to stdout +type PostReceiveMessage struct { + Message string `json:"message"` + Type string `json:"type"` +} + +// CheckInfo represents the response of GitLabs `check` API endpoint +type CheckInfo struct { + // Version of the GitLab Rails component + Version string `json:"gitlab_version"` + // Revision of the Git object of the running GitLab + Revision string `json:"gitlab_revision"` + // APIVersion of GitLab, expected to be v4 + APIVersion string `json:"api_version"` + // RedisReachable shows if GitLab can reach Redis. This can be false + // while the check itself succeeds. Normal hook API calls will likely + // fail. + RedisReachable bool `json:"redis"` +} + // GitlabAPI is an interface for accessing the gitlab internal API type GitlabAPI interface { // Allowed queries the gitlab internal api /allowed endpoint to determine if a ref change for a given repository and user is allowed @@ -78,181 +53,3 @@ type GitlabAPI interface { // PostReceive queries the gitlab internal api /post_receive to decrease the reference counter PostReceive(ctx context.Context, glRepository, glID, changes string, pushOptions ...string) (bool, []PostReceiveMessage, error) } - -// gitlabAPI is a wrapper around client.GitlabNetClient with API methods for gitlab git receive hooks -type gitlabAPI struct { - client *client.GitlabNetClient -} - -// NewGitlabAPI creates a GitLabAPI to talk to the Rails internal API -func NewGitlabAPI(gitlabCfg config.Gitlab, tlsCfg config.TLS) (GitlabAPI, error) { - client, err := NewHTTPClient(gitlabCfg, tlsCfg) - if err != nil { - return nil, err - } - - return &gitlabAPI{client: client}, nil -} - -// Allowed checks if a ref change for a given repository is allowed through the gitlab internal api /allowed endpoint -func (a *gitlabAPI) Allowed(ctx context.Context, params AllowedParams) (bool, string, error) { - gitObjDirVars, err := marshallGitObjectDirs(params.GitObjectDirectory, params.GitAlternateObjectDirectories) - if err != nil { - return false, "", fmt.Errorf("when getting git object directories json encoded string: %w", err) - } - - req := AllowedRequest{ - Action: "git-receive-pack", - GLRepository: params.GLRepository, - Changes: params.Changes, - Protocol: params.GLProtocol, - Project: strings.Replace(params.RepoPath, "'", "", -1), - Env: gitObjDirVars, - } - - if err := req.parseAndSetGLID(params.GLID); err != nil { - return false, "", fmt.Errorf("setting gl_id: %w", err) - } - - resp, err := a.client.Post(ctx, "/allowed", &req) - if err != nil { - return false, "", err - } - - defer func() { - io.Copy(ioutil.Discard, resp.Body) - resp.Body.Close() - }() - - var response AllowedResponse - - switch resp.StatusCode { - case http.StatusOK, - http.StatusMultipleChoices: - - mtype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) - if err != nil { - return false, "", fmt.Errorf("/allowed endpoint respond with unsupported content type: %w", err) - } - - if mtype != "application/json" { - return false, "", fmt.Errorf("/allowed endpoint respond with unsupported content type: %s", mtype) - } - - if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { - return false, "", fmt.Errorf("decoding response from /allowed endpoint: %w", err) - } - default: - return false, "", fmt.Errorf("gitlab api is not accessible: %d", resp.StatusCode) - } - - return response.Status, response.Message, nil -} - -type preReceiveResponse struct { - ReferenceCounterIncreased bool `json:"reference_counter_increased"` -} - -// PreReceive increases the reference counter for a push for a given gl_repository through the gitlab internal API /pre_receive endpoint -func (a *gitlabAPI) PreReceive(ctx context.Context, glRepository string) (bool, error) { - resp, err := a.client.Post(ctx, "/pre_receive", map[string]string{"gl_repository": glRepository}) - if err != nil { - return false, fmt.Errorf("http post to gitlab api /pre_receive endpoint: %w", err) - } - - defer func() { - io.Copy(ioutil.Discard, resp.Body) - resp.Body.Close() - }() - - if resp.StatusCode != http.StatusOK { - return false, fmt.Errorf("pre-receive call failed with status: %d", resp.StatusCode) - } - - mtype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) - if err != nil { - return false, fmt.Errorf("/pre_receive endpoint respond with unsupported content type: %w", err) - } - - if mtype != "application/json" { - return false, fmt.Errorf("/pre_receive endpoint respond with unsupported content type: %s", mtype) - } - - var result preReceiveResponse - - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return false, fmt.Errorf("decoding response from /pre_receive endpoint: %w", err) - } - - return result.ReferenceCounterIncreased, nil -} - -// PostReceiveResponse is the response the GitLab internal api provides on a successful /post_receive call -type PostReceiveResponse struct { - ReferenceCounterDecreased bool `json:"reference_counter_decreased"` - Messages []PostReceiveMessage `json:"messages"` -} - -// PostReceiveMessage encapsulates a message from the /post_receive endpoint that gets printed to stdout -type PostReceiveMessage struct { - Message string `json:"message"` - Type string `json:"type"` -} - -// PostReceive decreases the reference counter for a push for a given gl_repository through the gitlab internal API /post_receive endpoint -func (a *gitlabAPI) PostReceive(ctx context.Context, glRepository, glID, changes string, pushOptions ...string) (bool, []PostReceiveMessage, error) { - resp, err := a.client.Post(ctx, "/post_receive", map[string]interface{}{"gl_repository": glRepository, "identifier": glID, "changes": changes, "push_options": pushOptions}) - if err != nil { - return false, nil, fmt.Errorf("http post to gitlab api /post_receive endpoint: %w", err) - } - - defer func() { - io.Copy(ioutil.Discard, resp.Body) - resp.Body.Close() - }() - - if resp.StatusCode != http.StatusOK { - return false, nil, fmt.Errorf("post-receive call failed with status: %d", resp.StatusCode) - } - - mtype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) - if err != nil { - return false, nil, fmt.Errorf("/post_receive endpoint respond with invalid content type: %w", err) - } - - if mtype != "application/json" { - return false, nil, fmt.Errorf("/post_receive endpoint respond with unsupported content type: %s", mtype) - } - - var result PostReceiveResponse - - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return false, nil, fmt.Errorf("decoding response from /post_receive endpoint: %w", err) - } - - return result.ReferenceCounterDecreased, result.Messages, nil -} - -var glIDRegex = regexp.MustCompile(`\A[0-9]+\z`) - -func (a *AllowedRequest) parseAndSetGLID(glID string) error { - var value string - - switch { - case strings.HasPrefix(glID, "username-"): - a.Username = strings.TrimPrefix(glID, "username-") - return nil - case strings.HasPrefix(glID, "key-"): - a.KeyID = strings.TrimPrefix(glID, "key-") - value = a.KeyID - case strings.HasPrefix(glID, "user-"): - a.UserID = strings.TrimPrefix(glID, "user-") - value = a.UserID - } - - if !glIDRegex.MatchString(value) { - return fmt.Errorf("gl_id='%s' is invalid", glID) - } - - return nil -} diff --git a/internal/gitlab/check.go b/internal/gitlab/check.go deleted file mode 100644 index 968e4061c..000000000 --- a/internal/gitlab/check.go +++ /dev/null @@ -1,50 +0,0 @@ -package gitlab - -import ( - "context" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" -) - -// CheckInfo represents the response of GitLabs `check` API endpoint -type CheckInfo struct { - // Version of the GitLab Rails component - Version string `json:"gitlab_version"` - // Revision of the Git object of the running GitLab - Revision string `json:"gitlab_revision"` - // APIVersion of GitLab, expected to be v4 - APIVersion string `json:"api_version"` - // RedisReachable shows if GitLab can reach Redis. This can be false - // while the check itself succeeds. Normal hook API calls will likely - // fail. - RedisReachable bool `json:"redis"` -} - -// Check performs an HTTP request to the internal/check API endpoint to verify -// the connection and tokens. It returns basic information of the installed -// GitLab -func (a *gitlabAPI) Check(ctx context.Context) (*CheckInfo, error) { - resp, err := a.client.Get(ctx, "/check") - if err != nil { - return nil, fmt.Errorf("HTTP GET to GitLab endpoint /check failed: %w", err) - } - - defer func() { - io.Copy(ioutil.Discard, resp.Body) - resp.Body.Close() - }() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("Check HTTP request failed with status: %d", resp.StatusCode) - } - - var info CheckInfo - if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { - return nil, fmt.Errorf("failed to decode response from /check endpoint: %w", err) - } - - return &info, nil -} diff --git a/internal/gitlab/http_client.go b/internal/gitlab/http_client.go index cd8508da2..4b3c33220 100644 --- a/internal/gitlab/http_client.go +++ b/internal/gitlab/http_client.go @@ -1,17 +1,31 @@ package gitlab import ( + "context" + "encoding/json" "fmt" + "io" "io/ioutil" + "mime" + "net/http" "net/url" + "regexp" + "strings" "gitlab.com/gitlab-org/gitaly/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/internal/version" "gitlab.com/gitlab-org/gitlab-shell/client" ) +var glIDRegex = regexp.MustCompile(`\A[0-9]+\z`) + +// HTTPClient is an HTTP client used to talk to the internal GitLab Rails API. +type HTTPClient struct { + *client.GitlabNetClient +} + // NewHTTPClient creates an HTTP client to talk to the Rails internal API -func NewHTTPClient(gitlabCfg config.Gitlab, tlsCfg config.TLS) (*client.GitlabNetClient, error) { +func NewHTTPClient(gitlabCfg config.Gitlab, tlsCfg config.TLS) (*HTTPClient, error) { url, err := url.PathUnescape(gitlabCfg.URL) if err != nil { return nil, err @@ -51,5 +65,220 @@ func NewHTTPClient(gitlabCfg config.Gitlab, tlsCfg config.TLS) (*client.GitlabNe gitlabnetClient.SetUserAgent("gitaly/" + version.GetVersion()) - return gitlabnetClient, nil + return &HTTPClient{gitlabnetClient}, nil +} + +// allowedRequest is a request for the internal gitlab api /allowed endpoint +type allowedRequest struct { + Action string `json:"action,omitempty"` + GLRepository string `json:"gl_repository,omitempty"` + Project string `json:"project,omitempty"` + Changes string `json:"changes,omitempty"` + Protocol string `json:"protocol,omitempty"` + Env string `json:"env,omitempty"` + Username string `json:"username,omitempty"` + KeyID string `json:"key_id,omitempty"` + UserID string `json:"user_id,omitempty"` +} + +func (a *allowedRequest) parseAndSetGLID(glID string) error { + var value string + + switch { + case strings.HasPrefix(glID, "username-"): + a.Username = strings.TrimPrefix(glID, "username-") + return nil + case strings.HasPrefix(glID, "key-"): + a.KeyID = strings.TrimPrefix(glID, "key-") + value = a.KeyID + case strings.HasPrefix(glID, "user-"): + a.UserID = strings.TrimPrefix(glID, "user-") + value = a.UserID + } + + if !glIDRegex.MatchString(value) { + return fmt.Errorf("gl_id='%s' is invalid", glID) + } + + return nil +} + +// allowedResponse is a response for the internal gitlab api's /allowed endpoint with a subset +// of fields +type allowedResponse struct { + Status bool `json:"status"` + Message string `json:"message"` +} + +// Allowed checks if a ref change for a given repository is allowed through the gitlab internal api /allowed endpoint +func (c *HTTPClient) Allowed(ctx context.Context, params AllowedParams) (bool, string, error) { + gitObjDirVars, err := marshallGitObjectDirs(params.GitObjectDirectory, params.GitAlternateObjectDirectories) + if err != nil { + return false, "", fmt.Errorf("when getting git object directories json encoded string: %w", err) + } + + req := allowedRequest{ + Action: "git-receive-pack", + GLRepository: params.GLRepository, + Changes: params.Changes, + Protocol: params.GLProtocol, + Project: strings.Replace(params.RepoPath, "'", "", -1), + Env: gitObjDirVars, + } + + if err := req.parseAndSetGLID(params.GLID); err != nil { + return false, "", fmt.Errorf("setting gl_id: %w", err) + } + + resp, err := c.Post(ctx, "/allowed", &req) + if err != nil { + return false, "", err + } + + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() + + var response allowedResponse + + switch resp.StatusCode { + case http.StatusOK, + http.StatusMultipleChoices: + + mtype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return false, "", fmt.Errorf("/allowed endpoint respond with unsupported content type: %w", err) + } + + if mtype != "application/json" { + return false, "", fmt.Errorf("/allowed endpoint respond with unsupported content type: %s", mtype) + } + + if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { + return false, "", fmt.Errorf("decoding response from /allowed endpoint: %w", err) + } + default: + return false, "", fmt.Errorf("gitlab api is not accessible: %d", resp.StatusCode) + } + + return response.Status, response.Message, nil +} + +type preReceiveResponse struct { + ReferenceCounterIncreased bool `json:"reference_counter_increased"` +} + +// PreReceive increases the reference counter for a push for a given gl_repository through the gitlab internal API /pre_receive endpoint +func (c *HTTPClient) PreReceive(ctx context.Context, glRepository string) (bool, error) { + resp, err := c.Post(ctx, "/pre_receive", map[string]string{"gl_repository": glRepository}) + if err != nil { + return false, fmt.Errorf("http post to gitlab api /pre_receive endpoint: %w", err) + } + + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("pre-receive call failed with status: %d", resp.StatusCode) + } + + mtype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return false, fmt.Errorf("/pre_receive endpoint respond with unsupported content type: %w", err) + } + + if mtype != "application/json" { + return false, fmt.Errorf("/pre_receive endpoint respond with unsupported content type: %s", mtype) + } + + var result preReceiveResponse + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return false, fmt.Errorf("decoding response from /pre_receive endpoint: %w", err) + } + + return result.ReferenceCounterIncreased, nil +} + +// postReceiveResponse is the response the GitLab internal api provides on a successful /post_receive call +type postReceiveResponse struct { + ReferenceCounterDecreased bool `json:"reference_counter_decreased"` + Messages []PostReceiveMessage `json:"messages"` +} + +// PostReceive decreases the reference counter for a push for a given gl_repository through the gitlab internal API /post_receive endpoint +func (c *HTTPClient) PostReceive(ctx context.Context, glRepository, glID, changes string, pushOptions ...string) (bool, []PostReceiveMessage, error) { + resp, err := c.Post(ctx, "/post_receive", map[string]interface{}{"gl_repository": glRepository, "identifier": glID, "changes": changes, "push_options": pushOptions}) + if err != nil { + return false, nil, fmt.Errorf("http post to gitlab api /post_receive endpoint: %w", err) + } + + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return false, nil, fmt.Errorf("post-receive call failed with status: %d", resp.StatusCode) + } + + mtype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return false, nil, fmt.Errorf("/post_receive endpoint respond with invalid content type: %w", err) + } + + if mtype != "application/json" { + return false, nil, fmt.Errorf("/post_receive endpoint respond with unsupported content type: %s", mtype) + } + + var result postReceiveResponse + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return false, nil, fmt.Errorf("decoding response from /post_receive endpoint: %w", err) + } + + return result.ReferenceCounterDecreased, result.Messages, nil +} + +// Check performs an HTTP request to the internal/check API endpoint to verify +// the connection and tokens. It returns basic information of the installed +// GitLab +func (c *HTTPClient) Check(ctx context.Context) (*CheckInfo, error) { + resp, err := c.Get(ctx, "/check") + if err != nil { + return nil, fmt.Errorf("HTTP GET to GitLab endpoint /check failed: %w", err) + } + + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Check HTTP request failed with status: %d", resp.StatusCode) + } + + var info CheckInfo + if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { + return nil, fmt.Errorf("failed to decode response from /check endpoint: %w", err) + } + + return &info, nil +} + +// marshallGitObjectDirs generates a json encoded string containing GIT_OBJECT_DIRECTORY_RELATIVE, and GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE +func marshallGitObjectDirs(gitObjectDirRel string, gitAltObjectDirsRel []string) (string, error) { + envString, err := json.Marshal(map[string]interface{}{ + "GIT_OBJECT_DIRECTORY_RELATIVE": gitObjectDirRel, + "GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE": gitAltObjectDirsRel, + }) + + if err != nil { + return "", err + } + + return string(envString), nil } diff --git a/internal/gitlab/access_test.go b/internal/gitlab/http_client_test.go index 324913d42..ab33b098a 100644 --- a/internal/gitlab/access_test.go +++ b/internal/gitlab/http_client_test.go @@ -70,7 +70,7 @@ func TestAccess_verifyParams(t *testing.T) { }) defer cleanup() - c, err := NewGitlabAPI(config.Gitlab{ + c, err := NewHTTPClient(config.Gitlab{ URL: serverURL, SecretFile: secretFilePath, HTTPSettings: config.HTTPSettings{ @@ -206,7 +206,7 @@ func TestAccess_escapedAndRelativeURLs(t *testing.T) { serverURL = url.PathEscape(serverURL) } - c, err := NewGitlabAPI(config.Gitlab{ + c, err := NewHTTPClient(config.Gitlab{ URL: serverURL, RelativeURLRoot: tc.relativeURLRoot, SecretFile: secretFilePath, @@ -357,7 +357,7 @@ func TestAccess_allowedResponseHandling(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(tc.allowedHandler)) defer server.Close() - c, err := NewGitlabAPI(config.Gitlab{ + c, err := NewHTTPClient(config.Gitlab{ URL: server.URL, SecretFile: secretFilePath, }, config.TLS{}) @@ -457,7 +457,7 @@ func TestAccess_preReceive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(tc.prereceiveHandler)) defer server.Close() - c, err := NewGitlabAPI(config.Gitlab{ + c, err := NewHTTPClient(config.Gitlab{ URL: server.URL, SecretFile: secretFilePath, }, config.TLS{}) @@ -535,7 +535,7 @@ func TestAccess_postReceive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(tc.postReceiveHandler)) defer server.Close() - c, err := NewGitlabAPI(config.Gitlab{ + c, err := NewHTTPClient(config.Gitlab{ URL: server.URL, SecretFile: secretFilePath, }, config.TLS{}) |