Commit cd60a5bf authored by Vasilii Iakliushin's avatar Vasilii Iakliushin

Merge branch 'id-clean-up-gitaly-unavailable-err' into 'master'

Improve Git HTTP err message given when Gitaly unavailable

See merge request gitlab-org/gitlab!71120
parents f600d5fb 772df282
...@@ -11,6 +11,9 @@ module Repositories ...@@ -11,6 +11,9 @@ module Repositories
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception
rescue_from Gitlab::GitAccessProject::CreationError, with: :render_422_with_exception rescue_from Gitlab::GitAccessProject::CreationError, with: :render_422_with_exception
rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception
rescue_from GRPC::Unavailable do |e|
render_503_with_exception(e, message: 'The git server, Gitaly, is not available at this time. Please contact your administrator.')
end
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
...@@ -71,8 +74,8 @@ module Repositories ...@@ -71,8 +74,8 @@ module Repositories
render plain: exception.message, status: :unprocessable_entity render plain: exception.message, status: :unprocessable_entity
end end
def render_503_with_exception(exception) def render_503_with_exception(exception, message: nil)
render plain: exception.message, status: :service_unavailable render plain: message || exception.message, status: :service_unavailable
end end
def update_fetch_statistics def update_fetch_statistics
......
...@@ -7,12 +7,33 @@ RSpec.describe Repositories::GitHttpController do ...@@ -7,12 +7,33 @@ RSpec.describe Repositories::GitHttpController do
let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) } let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) } let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) }
shared_examples 'handles unavailable Gitaly' do
let(:params) { super().merge(service: 'git-upload-pack') }
before do
request.headers.merge! auth_env(user.username, user.password, nil)
end
context 'when Gitaly is unavailable' do
it 'responds with a 503 message' do
expect(Gitlab::GitalyClient).to receive(:call).and_raise(GRPC::Unavailable)
get :info_refs, params: params
expect(response).to have_gitlab_http_status(:service_unavailable)
expect(response.body).to eq('The git server, Gitaly, is not available at this time. Please contact your administrator.')
end
end
end
context 'when repository container is a project' do context 'when repository container is a project' do
it_behaves_like Repositories::GitHttpController do it_behaves_like Repositories::GitHttpController do
let(:container) { project } let(:container) { project }
let(:user) { project.owner } let(:user) { project.owner }
let(:access_checker_class) { Gitlab::GitAccess } let(:access_checker_class) { Gitlab::GitAccess }
it_behaves_like 'handles unavailable Gitaly'
describe 'POST #git_upload_pack' do describe 'POST #git_upload_pack' do
before do before do
allow(controller).to receive(:verify_workhorse_api!).and_return(true) allow(controller).to receive(:verify_workhorse_api!).and_return(true)
...@@ -84,6 +105,8 @@ RSpec.describe Repositories::GitHttpController do ...@@ -84,6 +105,8 @@ RSpec.describe Repositories::GitHttpController do
let(:container) { personal_snippet } let(:container) { personal_snippet }
let(:user) { personal_snippet.author } let(:user) { personal_snippet.author }
let(:access_checker_class) { Gitlab::GitAccessSnippet } let(:access_checker_class) { Gitlab::GitAccessSnippet }
it_behaves_like 'handles unavailable Gitaly'
end end
end end
...@@ -92,6 +115,8 @@ RSpec.describe Repositories::GitHttpController do ...@@ -92,6 +115,8 @@ RSpec.describe Repositories::GitHttpController do
let(:container) { project_snippet } let(:container) { project_snippet }
let(:user) { project_snippet.author } let(:user) { project_snippet.author }
let(:access_checker_class) { Gitlab::GitAccessSnippet } let(:access_checker_class) { Gitlab::GitAccessSnippet }
it_behaves_like 'handles unavailable Gitaly'
end end
end end
end end
...@@ -8,8 +8,8 @@ import ( ...@@ -8,8 +8,8 @@ import (
"net/http" "net/http"
"github.com/golang/gddo/httputil" "github.com/golang/gddo/httputil"
grpccodes "google.golang.org/grpc/codes"
"gitlab.com/gitlab-org/labkit/log" grpcstatus "google.golang.org/grpc/status"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api" "gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/gitaly" "gitlab.com/gitlab-org/gitlab/workhorse/internal/gitaly"
...@@ -26,6 +26,7 @@ func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response) ...@@ -26,6 +26,7 @@ func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response)
defer responseWriter.Log(r, 0) defer responseWriter.Log(r, 0)
rpc := getService(r) rpc := getService(r)
if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") { if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") {
// The 'dumb' Git HTTP protocol is not supported // The 'dumb' Git HTTP protocol is not supported
http.Error(responseWriter, "Not Found", 404) http.Error(responseWriter, "Not Found", 404)
...@@ -41,19 +42,26 @@ func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response) ...@@ -41,19 +42,26 @@ func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response)
encoding := httputil.NegotiateContentEncoding(r, offers) encoding := httputil.NegotiateContentEncoding(r, offers)
if err := handleGetInfoRefsWithGitaly(r.Context(), responseWriter, a, rpc, gitProtocol, encoding); err != nil { if err := handleGetInfoRefsWithGitaly(r.Context(), responseWriter, a, rpc, gitProtocol, encoding); err != nil {
helper.Fail500(responseWriter, r, fmt.Errorf("handleGetInfoRefs: %v", err)) status := grpcstatus.Convert(err)
err = fmt.Errorf("handleGetInfoRefs: %v", err)
if status != nil && status.Code() == grpccodes.Unavailable {
helper.CaptureAndFail(responseWriter, r, err, "The git server, Gitaly, is not available at this time. Please contact your administrator.", http.StatusServiceUnavailable)
} else {
helper.Fail500(responseWriter, r, err)
}
} }
} }
func handleGetInfoRefsWithGitaly(ctx context.Context, responseWriter *HttpResponseWriter, a *api.Response, rpc, gitProtocol, encoding string) error { func handleGetInfoRefsWithGitaly(ctx context.Context, responseWriter *HttpResponseWriter, a *api.Response, rpc, gitProtocol, encoding string) error {
ctx, smarthttp, err := gitaly.NewSmartHTTPClient(ctx, a.GitalyServer) ctx, smarthttp, err := gitaly.NewSmartHTTPClient(ctx, a.GitalyServer)
if err != nil { if err != nil {
return fmt.Errorf("GetInfoRefsHandler: %v", err) return err
} }
infoRefsResponseReader, err := smarthttp.InfoRefsResponseReader(ctx, &a.Repository, rpc, gitConfigOptions(a), gitProtocol) infoRefsResponseReader, err := smarthttp.InfoRefsResponseReader(ctx, &a.Repository, rpc, gitConfigOptions(a), gitProtocol)
if err != nil { if err != nil {
return fmt.Errorf("GetInfoRefsHandler: %v", err) return err
} }
var w io.Writer var w io.Writer
...@@ -69,7 +77,7 @@ func handleGetInfoRefsWithGitaly(ctx context.Context, responseWriter *HttpRespon ...@@ -69,7 +77,7 @@ func handleGetInfoRefsWithGitaly(ctx context.Context, responseWriter *HttpRespon
} }
if _, err = io.Copy(w, infoRefsResponseReader); err != nil { if _, err = io.Copy(w, infoRefsResponseReader); err != nil {
log.WithError(err).Error("GetInfoRefsHandler: error copying gitaly response") return err
} }
return nil return nil
......
package git
import (
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
grpccodes "google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
"gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/gitaly"
)
type smartHTTPServiceServerWithInfoRefs struct {
gitalypb.UnimplementedSmartHTTPServiceServer
InfoRefsUploadPackFunc func(*gitalypb.InfoRefsRequest, gitalypb.SmartHTTPService_InfoRefsUploadPackServer) error
}
func (srv *smartHTTPServiceServerWithInfoRefs) InfoRefsUploadPack(r *gitalypb.InfoRefsRequest, s gitalypb.SmartHTTPService_InfoRefsUploadPackServer) error {
return srv.InfoRefsUploadPackFunc(r, s)
}
func TestGetInfoRefsHandler(t *testing.T) {
addr := startSmartHTTPServer(t, &smartHTTPServiceServerWithInfoRefs{
InfoRefsUploadPackFunc: func(r *gitalypb.InfoRefsRequest, s gitalypb.SmartHTTPService_InfoRefsUploadPackServer) error {
return grpcstatus.Error(grpccodes.Unavailable, "error")
},
})
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/?service=git-upload-pack", nil)
a := &api.Response{GitalyServer: gitaly.Server{Address: addr}}
handleGetInfoRefs(NewHttpResponseWriter(w), r, a)
require.Equal(t, 503, w.Code)
msg := "The git server, Gitaly, is not available at this time. Please contact your administrator.\n"
require.Equal(t, msg, w.Body.String())
}
...@@ -45,14 +45,13 @@ func TestUploadPackTimesOut(t *testing.T) { ...@@ -45,14 +45,13 @@ func TestUploadPackTimesOut(t *testing.T) {
uploadPackTimeout = time.Millisecond uploadPackTimeout = time.Millisecond
defer func() { uploadPackTimeout = originalUploadPackTimeout }() defer func() { uploadPackTimeout = originalUploadPackTimeout }()
addr, cleanUp := startSmartHTTPServer(t, &smartHTTPServiceServer{ addr := startSmartHTTPServer(t, &smartHTTPServiceServer{
PostUploadPackFunc: func(stream gitalypb.SmartHTTPService_PostUploadPackServer) error { PostUploadPackFunc: func(stream gitalypb.SmartHTTPService_PostUploadPackServer) error {
_, err := stream.Recv() // trigger a read on the client request body _, err := stream.Recv() // trigger a read on the client request body
require.NoError(t, err) require.NoError(t, err)
return nil return nil
}, },
}) })
defer cleanUp()
body := &fakeReader{n: 0, err: nil} body := &fakeReader{n: 0, err: nil}
...@@ -64,7 +63,9 @@ func TestUploadPackTimesOut(t *testing.T) { ...@@ -64,7 +63,9 @@ func TestUploadPackTimesOut(t *testing.T) {
require.EqualError(t, err, "smarthttp.UploadPack: busyReader: context deadline exceeded") require.EqualError(t, err, "smarthttp.UploadPack: busyReader: context deadline exceeded")
} }
func startSmartHTTPServer(t testing.TB, s gitalypb.SmartHTTPServiceServer) (string, func()) { func startSmartHTTPServer(t testing.TB, s gitalypb.SmartHTTPServiceServer) string {
t.Helper()
tmp, err := ioutil.TempDir("", "") tmp, err := ioutil.TempDir("", "")
require.NoError(t, err) require.NoError(t, err)
...@@ -78,8 +79,10 @@ func startSmartHTTPServer(t testing.TB, s gitalypb.SmartHTTPServiceServer) (stri ...@@ -78,8 +79,10 @@ func startSmartHTTPServer(t testing.TB, s gitalypb.SmartHTTPServiceServer) (stri
require.NoError(t, srv.Serve(ln)) require.NoError(t, srv.Serve(ln))
}() }()
return fmt.Sprintf("%s://%s", ln.Addr().Network(), ln.Addr().String()), func() { t.Cleanup(func() {
srv.GracefulStop() srv.GracefulStop()
require.NoError(t, os.RemoveAll(tmp), "error removing temp dir %q", tmp) require.NoError(t, os.RemoveAll(tmp), "error removing temp dir %q", tmp)
} })
return fmt.Sprintf("%s://%s", ln.Addr().Network(), ln.Addr().String())
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment