[GIT] Add Support for Other Git Clients (#171)

This commit is contained in:
Johannes Batzill 2023-01-10 14:35:09 -08:00 committed by GitHub
parent afd86bacb0
commit a426cdd69b
11 changed files with 98 additions and 25 deletions

View File

@ -137,7 +137,7 @@ func initSystem(ctx context.Context, config *types.Config) (*system, error) {
apiHandler := router.ProvideAPIHandler(config, authenticator, accountClient, controller, spaceController, repoController, pullreqController, webhookController, githookController) apiHandler := router.ProvideAPIHandler(config, authenticator, accountClient, controller, spaceController, repoController, pullreqController, webhookController, githookController)
gitHandler := router.ProvideGitHandler(config, provider, repoStore, authenticator, authorizer, gitrpcInterface) gitHandler := router.ProvideGitHandler(config, provider, repoStore, authenticator, authorizer, gitrpcInterface)
webHandler := router2.ProvideWebHandler(config) webHandler := router2.ProvideWebHandler(config)
routerRouter := router2.ProvideRouter(apiHandler, gitHandler, webHandler) routerRouter := router2.ProvideRouter(config, apiHandler, gitHandler, webHandler)
serverServer := server.ProvideServer(config, routerRouter) serverServer := server.ProvideServer(config, routerRouter)
serverConfig := ProvideGitRPCServerConfig(config) serverConfig := ProvideGitRPCServerConfig(config)
server3, err := server2.ProvideServer(serverConfig) server3, err := server2.ProvideServer(serverConfig)

View File

@ -100,7 +100,7 @@ func initSystem(ctx context.Context, config *types.Config) (*system, error) {
apiHandler := router.ProvideAPIHandler(config, authenticator, repoController, spaceController, pullreqController, webhookController, githookController, serviceaccountController, controller) apiHandler := router.ProvideAPIHandler(config, authenticator, repoController, spaceController, pullreqController, webhookController, githookController, serviceaccountController, controller)
gitHandler := router.ProvideGitHandler(config, provider, repoStore, authenticator, authorizer, gitrpcInterface) gitHandler := router.ProvideGitHandler(config, provider, repoStore, authenticator, authorizer, gitrpcInterface)
webHandler := router.ProvideWebHandler(config) webHandler := router.ProvideWebHandler(config)
routerRouter := router.ProvideRouter(apiHandler, gitHandler, webHandler) routerRouter := router.ProvideRouter(config, apiHandler, gitHandler, webHandler)
serverServer := server.ProvideServer(config, routerRouter) serverServer := server.ProvideServer(config, routerRouter)
serverConfig := ProvideGitRPCServerConfig(config) serverConfig := ProvideGitRPCServerConfig(config)
server3, err := server2.ProvideServer(serverConfig) server3, err := server2.ProvideServer(serverConfig)

View File

@ -66,7 +66,7 @@ func CreateRPCWriteParams(ctx context.Context, urlProvider *url.Provider,
// generate envars (add everything githook CLI needs for execution) // generate envars (add everything githook CLI needs for execution)
envVars, err := githook.GenerateEnvironmentVariables(&githook.Payload{ envVars, err := githook.GenerateEnvironmentVariables(&githook.Payload{
BaseURL: urlProvider.GetAPIBaseURLInternal(), APIBaseURL: urlProvider.GetAPIBaseURLInternal(),
RepoID: repo.ID, RepoID: repo.ID,
PrincipalID: session.Principal.ID, PrincipalID: session.Principal.ID,
RequestID: requestID, RequestID: requestID,

View File

@ -36,7 +36,7 @@ func NewCLI() (*CLI, error) {
payload: payload, payload: payload,
client: &client{ client: &client{
httpClient: http.DefaultClient, httpClient: http.DefaultClient,
baseURL: payload.BaseURL, baseURL: payload.APIBaseURL,
requestPreparation: func(r *http.Request) *http.Request { requestPreparation: func(r *http.Request) *http.Request {
// TODO: reference single constant (together with gitness middleware) // TODO: reference single constant (together with gitness middleware)
r.Header.Add("X-Request-Id", payload.RequestID) r.Header.Add("X-Request-Id", payload.RequestID)

View File

@ -53,7 +53,7 @@ func (c *client) PostReceive(ctx context.Context,
// githook executes the requested githook type using the provided input. // githook executes the requested githook type using the provided input.
func (c *client) githook(ctx context.Context, githookType string, in interface{}) (*githook.ServerHookOutput, error) { func (c *client) githook(ctx context.Context, githookType string, in interface{}) (*githook.ServerHookOutput, error) {
uri := fmt.Sprintf("%s/api/v1/internal/git-hooks/%s", c.baseURL, githookType) uri := fmt.Sprintf("%s/v1/internal/git-hooks/%s", c.baseURL, githookType)
bodyBytes, err := json.Marshal(in) bodyBytes, err := json.Marshal(in)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to serialize input: %w", err) return nil, fmt.Errorf("failed to serialize input: %w", err)

View File

@ -21,7 +21,7 @@ const (
// Payload defines the Payload the githook binary is initiated with when executing the git hooks. // Payload defines the Payload the githook binary is initiated with when executing the git hooks.
type Payload struct { type Payload struct {
BaseURL string APIBaseURL string
RepoID int64 RepoID int64
PrincipalID int64 PrincipalID int64
RequestID string RequestID string
@ -83,7 +83,7 @@ func validatePayload(payload *Payload) error {
if payload == nil { if payload == nil {
return errors.New("payload is empty") return errors.New("payload is empty")
} }
if payload.BaseURL == "" { if payload.APIBaseURL == "" {
return errors.New("payload doesn't contain a base url") return errors.New("payload doesn't contain a base url")
} }
if payload.PrincipalID <= 0 { if payload.PrincipalID <= 0 {

View File

@ -20,13 +20,17 @@ import (
const ( const (
APIMount = "/api" APIMount = "/api"
gitUserAgentPrefix = "git/" GitMount = "/git"
) )
type Router struct { type Router struct {
api APIHandler api APIHandler
git GitHandler git GitHandler
web WebHandler web WebHandler
// gitHost describes the optional host via which git traffic is identified.
// Note: always stored as lowercase.
gitHost string
} }
// NewRouter returns a new http.Handler that routes traffic // NewRouter returns a new http.Handler that routes traffic
@ -35,11 +39,14 @@ func NewRouter(
api APIHandler, api APIHandler,
git GitHandler, git GitHandler,
web WebHandler, web WebHandler,
gitHost string,
) *Router { ) *Router {
return &Router{ return &Router{
api: api, api: api,
git: git, git: git,
web: web, web: web,
gitHost: strings.ToLower(gitHost),
} }
} }
@ -57,15 +64,19 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
* 1. GIT * 1. GIT
* *
* All Git originating traffic starts with "/space1/space2/repo.git". * All Git originating traffic starts with "/space1/space2/repo.git".
* This can collide with other API endpoints and thus has to be checked first.
* To avoid any false positives, we use the user-agent header to identify git agents.
*/ */
ua := req.Header.Get("user-agent") if r.isGitTraffic(req) {
if strings.HasPrefix(ua, gitUserAgentPrefix) {
log.UpdateContext(func(c zerolog.Context) zerolog.Context { log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("http.handler", "git") return c.Str("http.handler", "git")
}) })
// remove matched prefix to simplify API handlers (only if it's there)
if err = stripPrefix(GitMount, req); err != nil {
hlog.FromRequest(req).Err(err).Msgf("Failed striping of prefix for git request.")
render.InternalError(w)
return
}
r.git.ServeHTTP(w, req) r.git.ServeHTTP(w, req)
return return
} }
@ -75,8 +86,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
* *
* All Rest API calls start with "/api/", and thus can be uniquely identified. * All Rest API calls start with "/api/", and thus can be uniquely identified.
*/ */
p := req.URL.Path if r.isAPITraffic(req) {
if strings.HasPrefix(p, APIMount) {
log.UpdateContext(func(c zerolog.Context) zerolog.Context { log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("http.handler", "api") return c.Str("http.handler", "api")
}) })
@ -104,7 +114,40 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.web.ServeHTTP(w, req) r.web.ServeHTTP(w, req)
} }
// stripPrefix removes the prefix from the request path (expected to be there). // stripPrefix removes the prefix from the request path (or noop if it's not there).
func stripPrefix(prefix string, r *http.Request) error { func stripPrefix(prefix string, req *http.Request) error {
return request.ReplacePrefix(r, r.URL.Path[:len(prefix)], "") p := req.URL.Path
if !strings.HasPrefix(p, prefix) {
return nil
}
return request.ReplacePrefix(req, req.URL.Path[:len(prefix)], "")
}
// isGitTraffic returns true iff the request is identified as part of the git http protocol.
func (r *Router) isGitTraffic(req *http.Request) bool {
// git traffic is always reachable via the git mounting path.
p := req.URL.Path
if strings.HasPrefix(p, GitMount) {
return true
}
// otherwise check if the request came in via the configured git host (if enabled)
if len(r.gitHost) > 0 {
// cut (optional) port off the host
h, _, _ := strings.Cut(req.Host, ":")
// check if request host matches the configured git host (case insensitive)
if r.gitHost == strings.ToLower(h) {
return true
}
}
// otherwise we don't treat it as git traffic
return false
}
// isAPITraffic returns true iff the request is identified as part of our rest API.
func (r *Router) isAPITraffic(req *http.Request) bool {
p := req.URL.Path
return strings.HasPrefix(p, APIMount)
} }

View File

@ -31,11 +31,13 @@ var WireSet = wire.NewSet(
) )
func ProvideRouter( func ProvideRouter(
config *types.Config,
api APIHandler, api APIHandler,
git GitHandler, git GitHandler,
web WebHandler, web WebHandler,
) *Router { ) *Router {
return NewRouter(api, git, web) return NewRouter(api, git, web,
config.Server.HTTP.GitHost)
} }
func ProvideGitHandler( func ProvideGitHandler(

View File

@ -14,9 +14,12 @@ import (
// Provider provides the URLs of the gitness system. // Provider provides the URLs of the gitness system.
type Provider struct { type Provider struct {
// apiURLRaw stores the raw URL the api endpoints are reachable at publicly. // apiURLRaw stores the raw URL the api endpoints are reachable at publicly.
// NOTE: url is guaranteed to not have any trailing '/'.
apiURLRaw string apiURLRaw string
// apiURLInternalRaw stores the raw URL the api endpoints are reachable at internally.
// NOTE: no need for internal services to go via public route. // apiURLInternalRaw stores the raw URL the api endpoints are reachable at internally
// (no need for internal services to go via public route).
// NOTE: url is guaranteed to not have any trailing '/'.
apiURLInternalRaw string apiURLInternalRaw string
// gitURL stores the URL the git endpoints are available at. // gitURL stores the URL the git endpoints are available at.
@ -25,6 +28,12 @@ type Provider struct {
} }
func NewProvider(apiURLRaw string, apiURLInternalRaw, gitURLRaw string) (*Provider, error) { func NewProvider(apiURLRaw string, apiURLInternalRaw, gitURLRaw string) (*Provider, error) {
// remove trailing '/' to make usage easier
apiURLRaw = strings.TrimRight(apiURLRaw, "/")
apiURLInternalRaw = strings.TrimRight(apiURLInternalRaw, "/")
gitURLRaw = strings.TrimRight(gitURLRaw, "/")
// parse gitURL
gitURL, err := url.Parse(gitURLRaw) gitURL, err := url.Parse(gitURLRaw)
if err != nil { if err != nil {
return nil, fmt.Errorf("provided gitURLRaw '%s' is invalid: %w", gitURLRaw, err) return nil, fmt.Errorf("provided gitURLRaw '%s' is invalid: %w", gitURLRaw, err)
@ -38,16 +47,19 @@ func NewProvider(apiURLRaw string, apiURLInternalRaw, gitURLRaw string) (*Provid
} }
// GetAPIBaseURL returns the publicly reachable base url of the api server. // GetAPIBaseURL returns the publicly reachable base url of the api server.
// NOTE: url is guaranteed to not have any trailing '/'.
func (p *Provider) GetAPIBaseURL() string { func (p *Provider) GetAPIBaseURL() string {
return p.apiURLRaw return p.apiURLRaw
} }
// GetAPIBaseURLInternal returns the internally reachable base url of the api server. // GetAPIBaseURLInternal returns the internally reachable base url of the api server.
// NOTE: url is guaranteed to not have any trailing '/'.
func (p *Provider) GetAPIBaseURLInternal() string { func (p *Provider) GetAPIBaseURLInternal() string {
return p.apiURLInternalRaw return p.apiURLInternalRaw
} }
// GenerateRepoCloneURL generates the public git clone URL for the provided repo path. // GenerateRepoCloneURL generates the public git clone URL for the provided repo path.
// NOTE: url is guaranteed to not have any trailing '/'.
func (p *Provider) GenerateRepoCloneURL(repoPath string) string { func (p *Provider) GenerateRepoCloneURL(repoPath string) string {
repoPath = path.Clean(repoPath) repoPath = path.Clean(repoPath)
if !strings.HasSuffix(repoPath, ".git") { if !strings.HasSuffix(repoPath, ".git") {

View File

@ -12,7 +12,7 @@ import (
) )
var ( var (
illegalRootSpaceNames = []string{"api"} illegalRootSpaceNames = []string{"api", "git"}
ErrRootSpaceNameNotAllowed = &ValidationError{ ErrRootSpaceNameNotAllowed = &ValidationError{
fmt.Sprintf("The following names are not allowed for a root space: %v", illegalRootSpaceNames), fmt.Sprintf("The following names are not allowed for a root space: %v", illegalRootSpaceNames),

View File

@ -18,9 +18,23 @@ type Config struct {
// URL defines the URLs via which the different parts of the service are reachable by. // URL defines the URLs via which the different parts of the service are reachable by.
URL struct { URL struct {
Git string `envconfig:"GITNESS_URL_GIT" default:"http://localhost:3000"` // Git defines the external URL via which the GIT API is reachable.
API string `envconfig:"GITNESS_URL_API" default:"http://localhost:3000"` // NOTE: for routing to work properly, the request path & hostname reaching gitness
APIInternal string `envconfig:"GITNESS_URL_API_INTERNAL" default:"http://localhost:3000"` // have to statisfy at least one of the following two conditions:
// - Path ends with `/git`
// - Hostname matches Config.Server.HTTP.GitHost
// (this could be after proxy path / header rewrite).
Git string `envconfig:"GITNESS_URL_GIT" default:"http://localhost:3000/git"`
// API defines the external URL via which the rest API is reachable.
// NOTE: for routing to work properly, the request path reaching gitness has to end with `/api`
// (this could be after proxy path rewrite).
API string `envconfig:"GITNESS_URL_API" default:"http://localhost:3000/api"`
// APIInternal defines the internal URL via which the rest API is reachable.
// NOTE: for routing to work properly, the request path reaching gitness has to end with `/api`
// (this could be after proxy path rewrite).
APIInternal string `envconfig:"GITNESS_URL_API_INTERNAL" default:"http://localhost:3000/api"`
} }
// Git defines the git configuration parameters // Git defines the git configuration parameters
@ -38,6 +52,8 @@ type Config struct {
Bind string `envconfig:"GITNESS_HTTP_BIND" default:":3000"` Bind string `envconfig:"GITNESS_HTTP_BIND" default:":3000"`
Proto string `envconfig:"GITNESS_HTTP_PROTO"` Proto string `envconfig:"GITNESS_HTTP_PROTO"`
Host string `envconfig:"GITNESS_HTTP_HOST"` Host string `envconfig:"GITNESS_HTTP_HOST"`
// GitHost is the host used to identify git traffic (OPTIONAL).
GitHost string `envconfig:"GITNESS_HTTP_GIT_HOST" default:"git.localhost"`
} }
// GRPC defines the grpc configuration parameters // GRPC defines the grpc configuration parameters