[Githook] Introduce InMemory Githook (#916)

This commit is contained in:
Johannes Batzill 2023-12-19 15:50:15 +00:00 committed by Harness
parent eac99c3686
commit 1d8d50a188
32 changed files with 655 additions and 393 deletions

View File

@ -22,7 +22,7 @@ import (
"github.com/harness/gitness/app/auth"
events "github.com/harness/gitness/app/events/git"
"github.com/harness/gitness/git"
"github.com/harness/gitness/githook"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
@ -44,23 +44,21 @@ const (
func (c *Controller) PostReceive(
ctx context.Context,
session *auth.Session,
repoID int64,
principalID int64,
in githook.PostReceiveInput,
) (*githook.Output, error) {
repo, err := c.getRepoCheckAccess(ctx, session, repoID, enum.PermissionRepoPush)
in types.GithookPostReceiveInput,
) (hook.Output, error) {
repo, err := c.getRepoCheckAccess(ctx, session, in.RepoID, enum.PermissionRepoPush)
if err != nil {
return nil, err
return hook.Output{}, err
}
// report ref events (best effort)
c.reportReferenceEvents(ctx, repo, principalID, in)
c.reportReferenceEvents(ctx, repo, in.PrincipalID, in.PostReceiveInput)
// create output object and have following messages fill its messages
out := &githook.Output{}
out := hook.Output{}
// handle branch updates related to PRs - best effort
c.handlePRMessaging(ctx, repo, in, out)
c.handlePRMessaging(ctx, repo, in.PostReceiveInput, &out)
return out, nil
}
@ -72,7 +70,7 @@ func (c *Controller) reportReferenceEvents(
ctx context.Context,
repo *types.Repository,
principalID int64,
in githook.PostReceiveInput,
in hook.PostReceiveInput,
) {
for _, refUpdate := range in.RefUpdates {
switch {
@ -90,7 +88,7 @@ func (c *Controller) reportBranchEvent(
ctx context.Context,
repo *types.Repository,
principalID int64,
branchUpdate githook.ReferenceUpdate,
branchUpdate hook.ReferenceUpdate,
) {
switch {
case branchUpdate.Old == types.NilSHA:
@ -137,7 +135,7 @@ func (c *Controller) reportTagEvent(
ctx context.Context,
repo *types.Repository,
principalID int64,
tagUpdate githook.ReferenceUpdate,
tagUpdate hook.ReferenceUpdate,
) {
switch {
case tagUpdate.Old == types.NilSHA:
@ -172,8 +170,8 @@ func (c *Controller) reportTagEvent(
func (c *Controller) handlePRMessaging(
ctx context.Context,
repo *types.Repository,
in githook.PostReceiveInput,
out *githook.Output,
in hook.PostReceiveInput,
out *hook.Output,
) {
// skip anything that was a batch push / isn't branch related / isn't updating/creating a branch.
if len(in.RefUpdates) != 1 ||
@ -194,7 +192,7 @@ func (c *Controller) suggestPullRequest(
ctx context.Context,
repo *types.Repository,
branchName string,
out *githook.Output,
out *hook.Output,
) {
if branchName == repo.DefaultBranch {
// Don't suggest a pull request if this is a push to the default branch.

View File

@ -23,7 +23,7 @@ import (
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/app/services/protection"
"github.com/harness/gitness/githook"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
@ -37,16 +37,13 @@ import (
func (c *Controller) PreReceive(
ctx context.Context,
session *auth.Session,
repoID int64,
principalID int64,
internal bool,
in githook.PreReceiveInput,
) (*githook.Output, error) {
output := &githook.Output{}
in types.GithookPreReceiveInput,
) (hook.Output, error) {
output := hook.Output{}
repo, err := c.getRepoCheckAccess(ctx, session, repoID, enum.PermissionRepoPush)
repo, err := c.getRepoCheckAccess(ctx, session, in.RepoID, enum.PermissionRepoPush)
if err != nil {
return nil, err
return hook.Output{}, err
}
refUpdates := groupRefsByAction(in.RefUpdates)
@ -57,7 +54,7 @@ func (c *Controller) PreReceive(
return output, nil
}
if internal {
if in.Internal {
// It's an internal call, so no need to verify protection rules.
return output, nil
}
@ -68,9 +65,9 @@ func (c *Controller) PreReceive(
}
// TODO: use store.PrincipalInfoCache once we abstracted principals.
principal, err := c.principalStore.Find(ctx, principalID)
principal, err := c.principalStore.Find(ctx, in.PrincipalID)
if err != nil {
return nil, fmt.Errorf("failed to find inner principal with id %d: %w", principalID, err)
return hook.Output{}, fmt.Errorf("failed to find inner principal with id %d: %w", in.PrincipalID, err)
}
dummySession := &auth.Session{
@ -78,9 +75,9 @@ func (c *Controller) PreReceive(
Metadata: nil,
}
err = c.checkProtectionRules(ctx, dummySession, repo, refUpdates, output)
err = c.checkProtectionRules(ctx, dummySession, repo, refUpdates, &output)
if err != nil {
return nil, fmt.Errorf("failed to check protection rules: %w", err)
return hook.Output{}, fmt.Errorf("failed to check protection rules: %w", err)
}
return output, nil
@ -101,7 +98,7 @@ func (c *Controller) checkProtectionRules(
session *auth.Session,
repo *types.Repository,
refUpdates changedRefs,
output *githook.Output,
output *hook.Output,
) error {
isRepoOwner, err := apiauth.IsRepoOwner(ctx, c.authorizer, session, repo)
if err != nil {
@ -169,7 +166,7 @@ type changes struct {
updated []string
}
func (c *changes) groupByAction(refUpdate githook.ReferenceUpdate, name string) {
func (c *changes) groupByAction(refUpdate hook.ReferenceUpdate, name string) {
switch {
case refUpdate.Old == types.NilSHA:
c.created = append(c.created, name)
@ -186,7 +183,7 @@ type changedRefs struct {
other changes
}
func groupRefsByAction(refUpdates []githook.ReferenceUpdate) (c changedRefs) {
func groupRefsByAction(refUpdates []hook.ReferenceUpdate) (c changedRefs) {
for _, refUpdate := range refUpdates {
switch {
case strings.HasPrefix(refUpdate.Ref, gitReferenceNamePrefixBranch):

View File

@ -16,10 +16,10 @@ package githook
import (
"context"
"fmt"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/githook"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/types"
)
// Update executes the update hook for a git repository.
@ -28,15 +28,8 @@ import (
func (c *Controller) Update(
ctx context.Context,
session *auth.Session,
repoID int64,
principalID int64,
in *githook.UpdateInput,
) (*githook.Output, error) {
if in == nil {
return nil, fmt.Errorf("input is nil")
}
in types.GithookUpdateInput,
) (hook.Output, error) {
// We currently don't have any update action (nothing planned as of now)
return &githook.Output{}, nil
return hook.Output{}, nil
}

View File

@ -14,39 +14,4 @@
package githook
import (
"github.com/harness/gitness/app/auth/authz"
eventsgit "github.com/harness/gitness/app/events/git"
"github.com/harness/gitness/app/services/protection"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/app/url"
"github.com/harness/gitness/git"
"github.com/google/wire"
)
// WireSet provides a wire set for this package.
var WireSet = wire.NewSet(
ProvideController,
)
func ProvideController(
authorizer authz.Authorizer,
principalStore store.PrincipalStore,
repoStore store.RepoStore,
gitReporter *eventsgit.Reporter,
git git.Interface,
pullreqStore store.PullReqStore,
urlProvider url.Provider,
protectionManager *protection.Manager,
) *Controller {
return NewController(
authorizer,
principalStore,
repoStore,
gitReporter,
git,
pullreqStore,
urlProvider,
protectionManager)
}
// Due to cyclic injection dependencies, wiring can be found at app/githook/wire.go

View File

@ -21,7 +21,7 @@ import (
controllergithook "github.com/harness/gitness/app/api/controller/githook"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/githook"
"github.com/harness/gitness/types"
)
// HandlePostReceive returns a handler function that handles post-receive git hooks.
@ -30,26 +30,14 @@ func HandlePostReceive(githookCtrl *controllergithook.Controller) http.HandlerFu
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
repoID, err := request.GetRepoIDFromQuery(r)
if err != nil {
render.TranslatedUserError(w, err)
return
}
principalID, err := request.GetPrincipalIDFromQuery(r)
if err != nil {
render.TranslatedUserError(w, err)
return
}
in := githook.PostReceiveInput{}
err = json.NewDecoder(r.Body).Decode(&in)
in := types.GithookPostReceiveInput{}
err := json.NewDecoder(r.Body).Decode(&in)
if err != nil {
render.BadRequestf(w, "Invalid Request Body: %s.", err)
return
}
out, err := githookCtrl.PostReceive(ctx, session, repoID, principalID, in)
out, err := githookCtrl.PostReceive(ctx, session, in)
if err != nil {
render.TranslatedUserError(w, err)
return

View File

@ -21,7 +21,7 @@ import (
controllergithook "github.com/harness/gitness/app/api/controller/githook"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/githook"
"github.com/harness/gitness/types"
)
// HandlePreReceive returns a handler function that handles pre-receive git hooks.
@ -30,32 +30,14 @@ func HandlePreReceive(githookCtrl *controllergithook.Controller) http.HandlerFun
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
repoID, err := request.GetRepoIDFromQuery(r)
if err != nil {
render.TranslatedUserError(w, err)
return
}
principalID, err := request.GetPrincipalIDFromQuery(r)
if err != nil {
render.TranslatedUserError(w, err)
return
}
internal, err := request.GetInternalFromQueryOrDefault(r, false)
if err != nil {
render.TranslatedUserError(w, err)
return
}
in := githook.PreReceiveInput{}
err = json.NewDecoder(r.Body).Decode(&in)
in := types.GithookPreReceiveInput{}
err := json.NewDecoder(r.Body).Decode(&in)
if err != nil {
render.BadRequestf(w, "Invalid Request Body: %s.", err)
return
}
out, err := githookCtrl.PreReceive(ctx, session, repoID, principalID, internal, in)
out, err := githookCtrl.PreReceive(ctx, session, in)
if err != nil {
render.TranslatedUserError(w, err)
return

View File

@ -21,7 +21,7 @@ import (
githookcontroller "github.com/harness/gitness/app/api/controller/githook"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/githook"
"github.com/harness/gitness/types"
)
// HandleUpdate returns a handler function that handles update git hooks.
@ -30,26 +30,14 @@ func HandleUpdate(githookCtrl *githookcontroller.Controller) http.HandlerFunc {
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
repoID, err := request.GetRepoIDFromQuery(r)
if err != nil {
render.TranslatedUserError(w, err)
return
}
principalID, err := request.GetPrincipalIDFromQuery(r)
if err != nil {
render.TranslatedUserError(w, err)
return
}
in := new(githook.UpdateInput)
err = json.NewDecoder(r.Body).Decode(in)
in := types.GithookUpdateInput{}
err := json.NewDecoder(r.Body).Decode(&in)
if err != nil {
render.BadRequestf(w, "Invalid Request Body: %s.", err)
return
}
out, err := githookCtrl.Update(ctx, session, repoID, principalID, in)
out, err := githookCtrl.Update(ctx, session, in)
if err != nil {
render.TranslatedUserError(w, err)
return

View File

@ -112,11 +112,6 @@ func ParseCommitFilter(r *http.Request) (*types.CommitFilter, error) {
}, nil
}
// GetInternalFromQueryOrDefault returns the internal flag from the request query.
func GetInternalFromQueryOrDefault(r *http.Request, dflt bool) (bool, error) {
return QueryParamAsBoolOrDefault(r, QueryParamInternal, dflt)
}
// GetGitProtocolFromHeadersOrDefault returns the git protocol from the request headers.
func GetGitProtocolFromHeadersOrDefault(r *http.Request, deflt string) string {
return GetHeaderOrDefault(r, HeaderParamGitProtocol, deflt)

View File

@ -39,11 +39,6 @@ func GetPrincipalUIDFromPath(r *http.Request) (string, error) {
return PathParamOrError(r, PathParamPrincipalUID)
}
// GetPrincipalIDFromQuery returns the principal id from the request query.
func GetPrincipalIDFromQuery(r *http.Request) (int64, error) {
return QueryParamAsPositiveInt64(r, QueryParamPrincipalID)
}
func GetUserUIDFromPath(r *http.Request) (string, error) {
return PathParamOrError(r, PathParamUserUID)
}

View File

@ -37,11 +37,6 @@ func GetRepoRefFromPath(r *http.Request) (string, error) {
return url.PathUnescape(rawRef)
}
// GetRepoIDFromQuery returns the repo id from the request query.
func GetRepoIDFromQuery(r *http.Request) (int64, error) {
return QueryParamAsPositiveInt64(r, QueryParamRepoID)
}
// ParseSortRepo extracts the repo sort parameter from the url.
func ParseSortRepo(r *http.Request) enum.RepoAttr {
return enum.ParseRepoAttr(

View File

@ -0,0 +1,131 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package githook
import (
"context"
"errors"
"fmt"
"github.com/harness/gitness/app/api/controller/githook"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/store"
"github.com/harness/gitness/types"
"github.com/rs/zerolog/log"
)
// ControllerClientFactory creates clients that directly call the controller to execute githooks.
type ControllerClientFactory struct {
githookCtrl *githook.Controller
}
func (f *ControllerClientFactory) NewClient(_ context.Context, envVars map[string]string) (hook.Client, error) {
payload, err := hook.LoadPayloadFromMap[Payload](envVars)
if err != nil {
return nil, fmt.Errorf("failed to load payload from provided map of environment variables: %w", err)
}
// ensure we return disabled message in case it's explicitly disabled
if payload.Disabled {
return hook.NewNoopClient([]string{"hook disabled"}), nil
}
if err := payload.Validate(); err != nil {
return nil, fmt.Errorf("payload validation failed: %w", err)
}
return &ControllerClient{
baseInput: getInputBaseFromPayload(payload),
githookCtrl: f.githookCtrl,
}, nil
}
// ControllerClient directly calls the controller to execute githooks.
type ControllerClient struct {
baseInput types.GithookInputBase
githookCtrl *githook.Controller
}
func (c *ControllerClient) PreReceive(
ctx context.Context,
in hook.PreReceiveInput,
) (hook.Output, error) {
log.Ctx(ctx).Debug().Int64("repo_id", c.baseInput.RepoID).Msg("calling pre-receive")
out, err := c.githookCtrl.PreReceive(
ctx,
nil, // TODO: update once githooks are auth protected
types.GithookPreReceiveInput{
GithookInputBase: c.baseInput,
PreReceiveInput: in,
},
)
if err != nil {
return hook.Output{}, translateControllerError(err)
}
return out, nil
}
func (c *ControllerClient) Update(
ctx context.Context,
in hook.UpdateInput,
) (hook.Output, error) {
log.Ctx(ctx).Debug().Int64("repo_id", c.baseInput.RepoID).Msg("calling update")
out, err := c.githookCtrl.Update(
ctx,
nil, // TODO: update once githooks are auth protected
types.GithookUpdateInput{
GithookInputBase: c.baseInput,
UpdateInput: in,
},
)
if err != nil {
return hook.Output{}, translateControllerError(err)
}
return out, nil
}
func (c *ControllerClient) PostReceive(
ctx context.Context,
in hook.PostReceiveInput,
) (hook.Output, error) {
log.Ctx(ctx).Debug().Int64("repo_id", c.baseInput.RepoID).Msg("calling post-receive")
out, err := c.githookCtrl.PostReceive(
ctx,
nil, // TODO: update once githooks are auth protected
types.GithookPostReceiveInput{
GithookInputBase: c.baseInput,
PostReceiveInput: in,
},
)
if err != nil {
return hook.Output{}, translateControllerError(err)
}
return out, nil
}
func translateControllerError(err error) error {
if errors.Is(err, store.ErrResourceNotFound) {
return hook.ErrNotFound
}
return err
}

181
app/githook/client_rest.go Normal file
View File

@ -0,0 +1,181 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package githook
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/types"
"github.com/harness/gitness/version"
)
const (
// HTTPRequestPathPreReceive is the subpath under the provided base url the client uses to call pre-receive.
HTTPRequestPathPreReceive = "pre-receive"
// HTTPRequestPathPostReceive is the subpath under the provided base url the client uses to call post-receive.
HTTPRequestPathPostReceive = "post-receive"
// HTTPRequestPathUpdate is the subpath under the provided base url the client uses to call update.
HTTPRequestPathUpdate = "update"
)
// RestClientFactory creates clients that make rest api calls to gitness to execute githooks.
type RestClientFactory struct{}
func (f *RestClientFactory) NewClient(_ context.Context, envVars map[string]string) (hook.Client, error) {
payload, err := hook.LoadPayloadFromMap[Payload](envVars)
if err != nil {
return nil, fmt.Errorf("failed to load payload from provided map of environment variables: %w", err)
}
// ensure we return disabled message in case it's explicitly disabled
if payload.Disabled {
return hook.NewNoopClient([]string{"hook disabled"}), nil
}
if err := payload.Validate(); err != nil {
return nil, fmt.Errorf("payload validation failed: %w", err)
}
return NewRestClient(payload), nil
}
// RestClient is the hook.Client used to call the githooks api of gitness api server.
type RestClient struct {
httpClient *http.Client
baseURL string
requestID string
baseInput types.GithookInputBase
}
func NewRestClient(
payload Payload,
) hook.Client {
return &RestClient{
httpClient: http.DefaultClient,
baseURL: strings.TrimRight(payload.BaseURL, "/"),
requestID: payload.RequestID,
baseInput: getInputBaseFromPayload(payload),
}
}
// PreReceive calls the pre-receive githook api of the gitness api server.
func (c *RestClient) PreReceive(
ctx context.Context,
in hook.PreReceiveInput,
) (hook.Output, error) {
return c.githook(ctx, HTTPRequestPathPreReceive, types.GithookPreReceiveInput{
GithookInputBase: c.baseInput,
PreReceiveInput: in,
})
}
// Update calls the update githook api of the gitness api server.
func (c *RestClient) Update(
ctx context.Context,
in hook.UpdateInput,
) (hook.Output, error) {
return c.githook(ctx, HTTPRequestPathUpdate, types.GithookUpdateInput{
GithookInputBase: c.baseInput,
UpdateInput: in,
})
}
// PostReceive calls the post-receive githook api of the gitness api server.
func (c *RestClient) PostReceive(
ctx context.Context,
in hook.PostReceiveInput,
) (hook.Output, error) {
return c.githook(ctx, HTTPRequestPathPostReceive, types.GithookPostReceiveInput{
GithookInputBase: c.baseInput,
PostReceiveInput: in,
})
}
// githook executes the requested githook type using the provided input.
func (c *RestClient) githook(ctx context.Context, githookType string, payload interface{}) (hook.Output, error) {
uri := c.baseURL + "/" + githookType
bodyBytes, err := json.Marshal(payload)
if err != nil {
return hook.Output{}, fmt.Errorf("failed to serialize input: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, bytes.NewBuffer(bodyBytes))
if err != nil {
return hook.Output{}, fmt.Errorf("failed to create new http request: %w", err)
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add(request.HeaderUserAgent, fmt.Sprintf("Gitness/%s", version.Version))
req.Header.Add(request.HeaderRequestID, c.requestID)
// Execute the request
resp, err := c.httpClient.Do(req)
// ensure the body is closed after we read (independent of status code or error)
if resp != nil && resp.Body != nil {
// Use function to satisfy the linter which complains about unhandled errors otherwise
defer func() { _ = resp.Body.Close() }()
}
if err != nil {
return hook.Output{}, fmt.Errorf("request execution failed: %w", err)
}
return unmarshalResponse[hook.Output](resp)
}
// unmarshalResponse reads the response body and if there are no errors marshall's it into
// the data struct.
func unmarshalResponse[T any](resp *http.Response) (T, error) {
var body T
if resp == nil {
return body, errors.New("http response is empty")
}
if resp.StatusCode == http.StatusNotFound {
return body, hook.ErrNotFound
}
if resp.StatusCode != http.StatusOK {
return body, fmt.Errorf("expected response code 200 but got: %s", resp.Status)
}
// ensure we actually got a body returned.
if resp.Body == nil {
return body, errors.New("http response body is empty")
}
rawBody, err := io.ReadAll(resp.Body)
if err != nil {
return body, fmt.Errorf("error reading response body : %w", err)
}
err = json.Unmarshal(rawBody, &body)
if err != nil {
return body, fmt.Errorf("error deserializing response body: %w", err)
}
return body, nil
}

View File

@ -17,14 +17,11 @@ package githook
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/githook"
"github.com/harness/gitness/types"
"github.com/harness/gitness/version"
"github.com/harness/gitness/git/hook"
"github.com/rs/zerolog/log"
)
@ -55,7 +52,7 @@ func GenerateEnvironmentVariables(
// generate githook base url
baseURL := strings.TrimLeft(apiBaseURL, "/") + "/v1/internal/git-hooks"
payload := &types.GithookPayload{
payload := Payload{
BaseURL: baseURL,
RepoID: repoID,
PrincipalID: principalID,
@ -68,48 +65,27 @@ func GenerateEnvironmentVariables(
return nil, fmt.Errorf("generated payload is invalid: %w", err)
}
return githook.GenerateEnvironmentVariables(payload)
return hook.GenerateEnvironmentVariables(payload)
}
// LoadFromEnvironment returns a new githook.CLICore created by loading the payload from the environment variable.
func LoadFromEnvironment() (*githook.CLICore, error) {
payload, err := githook.LoadPayloadFromEnvironment[*types.GithookPayload]()
func LoadFromEnvironment() (*hook.CLICore, error) {
payload, err := hook.LoadPayloadFromEnvironment[Payload]()
if err != nil {
return nil, fmt.Errorf("failed to load payload from environment: %w", err)
}
// ensure we return disabled error in case it's explicitly disabled (will result in no-op)
if payload.Disabled {
return nil, githook.ErrDisabled
return nil, hook.ErrDisabled
}
if err := payload.Validate(); err != nil {
return nil, fmt.Errorf("payload validation failed: %w", err)
}
return githook.NewCLICore(
githook.NewClient(
http.DefaultClient,
payload.BaseURL,
func(r *http.Request) *http.Request {
// add query params
query := r.URL.Query()
query.Add(request.QueryParamRepoID, fmt.Sprint(payload.RepoID))
query.Add(request.QueryParamPrincipalID, fmt.Sprint(payload.PrincipalID))
if payload.Internal {
query.Add(request.QueryParamInternal, "true")
}
r.URL.RawQuery = query.Encode()
// add headers
if len(payload.RequestID) > 0 {
r.Header.Add(request.HeaderRequestID, payload.RequestID)
}
r.Header.Add(request.HeaderUserAgent, fmt.Sprintf("Gitness/%s", version.Version))
return r
},
),
return hook.NewCLICore(
NewRestClient(payload),
ExecutionTimeout,
), nil
}

58
app/githook/types.go Normal file
View File

@ -0,0 +1,58 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package githook
import (
"errors"
"github.com/harness/gitness/types"
)
// Payload defines the payload that's send to git via environment variables.
type Payload struct {
BaseURL string
RepoID int64
PrincipalID int64
RequestID string
Disabled bool
Internal bool // Internal calls originate from Gitness, and external calls are direct git pushes.
}
func (p Payload) Validate() error {
// skip further validation if githook is disabled
if p.Disabled {
return nil
}
if p.BaseURL == "" {
return errors.New("payload doesn't contain a base url")
}
if p.PrincipalID <= 0 {
return errors.New("payload doesn't contain a principal id")
}
if p.RepoID <= 0 {
return errors.New("payload doesn't contain a repo id")
}
return nil
}
func getInputBaseFromPayload(p Payload) types.GithookInputBase {
return types.GithookInputBase{
RepoID: p.RepoID,
PrincipalID: p.PrincipalID,
Internal: p.Internal,
}
}

70
app/githook/wire.go Normal file
View File

@ -0,0 +1,70 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package githook
import (
"github.com/harness/gitness/app/api/controller/githook"
"github.com/harness/gitness/app/auth/authz"
eventsgit "github.com/harness/gitness/app/events/git"
"github.com/harness/gitness/app/services/protection"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/app/url"
"github.com/harness/gitness/git"
"github.com/harness/gitness/git/hook"
"github.com/google/wire"
)
// WireSet provides a wire set for this package.
var WireSet = wire.NewSet(
ProvideController,
ProvideFactory,
)
func ProvideFactory() hook.ClientFactory {
return &ControllerClientFactory{
// will be set in ProvideController (to break cyclic dependency during wiring)
githookCtrl: nil,
}
}
func ProvideController(
authorizer authz.Authorizer,
principalStore store.PrincipalStore,
repoStore store.RepoStore,
gitReporter *eventsgit.Reporter,
git git.Interface,
pullreqStore store.PullReqStore,
urlProvider url.Provider,
protectionManager *protection.Manager,
githookFactory hook.ClientFactory,
) *githook.Controller {
ctrl := githook.NewController(
authorizer,
principalStore,
repoStore,
gitReporter,
git,
pullreqStore,
urlProvider,
protectionManager)
// TODO: improve wiring if possible
if fct, ok := githookFactory.(*ControllerClientFactory); ok {
fct.githookCtrl = ctrl
}
return ctrl
}

View File

@ -69,7 +69,7 @@ import (
middlewareprincipal "github.com/harness/gitness/app/api/middleware/principal"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/app/auth/authn"
"github.com/harness/gitness/githook"
"github.com/harness/gitness/app/githook"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"

View File

@ -18,7 +18,7 @@ import (
"os"
"github.com/harness/gitness/cli/operations/hooks"
"github.com/harness/gitness/githook"
"github.com/harness/gitness/git/hook"
)
func GetArguments() []string {
@ -26,7 +26,7 @@ func GetArguments() []string {
args := os.Args[1:]
// in case of githooks, translate the arguments coming from git to work with gitness.
if gitArgs, fromGit := githook.SanitizeArgsForGit(command, args); fromGit {
if gitArgs, fromGit := hook.SanitizeArgsForGit(command, args); fromGit {
return append([]string{hooks.ParamHooks}, gitArgs...)
}

View File

@ -16,7 +16,7 @@ package hooks
import (
gitnessgithook "github.com/harness/gitness/app/githook"
"github.com/harness/gitness/githook"
"github.com/harness/gitness/git/hook"
"gopkg.in/alecthomas/kingpin.v2"
)
@ -28,5 +28,5 @@ const (
func Register(app *kingpin.Application) {
subCmd := app.Command(ParamHooks, "manage git server hooks")
githook.RegisterAll(subCmd, gitnessgithook.LoadFromEnvironment)
hook.RegisterAll(subCmd, gitnessgithook.LoadFromEnvironment)
}

View File

@ -13,7 +13,6 @@ import (
checkcontroller "github.com/harness/gitness/app/api/controller/check"
"github.com/harness/gitness/app/api/controller/connector"
"github.com/harness/gitness/app/api/controller/execution"
"github.com/harness/gitness/app/api/controller/githook"
controllerkeywordsearch "github.com/harness/gitness/app/api/controller/keywordsearch"
controllerlogs "github.com/harness/gitness/app/api/controller/logs"
"github.com/harness/gitness/app/api/controller/pipeline"
@ -37,6 +36,7 @@ import (
gitevents "github.com/harness/gitness/app/events/git"
pullreqevents "github.com/harness/gitness/app/events/pullreq"
repoevents "github.com/harness/gitness/app/events/repo"
"github.com/harness/gitness/app/githook"
"github.com/harness/gitness/app/pipeline/canceler"
"github.com/harness/gitness/app/pipeline/commit"
"github.com/harness/gitness/app/pipeline/file"

View File

@ -12,7 +12,6 @@ import (
check2 "github.com/harness/gitness/app/api/controller/check"
"github.com/harness/gitness/app/api/controller/connector"
"github.com/harness/gitness/app/api/controller/execution"
"github.com/harness/gitness/app/api/controller/githook"
keywordsearch2 "github.com/harness/gitness/app/api/controller/keywordsearch"
logs2 "github.com/harness/gitness/app/api/controller/logs"
"github.com/harness/gitness/app/api/controller/pipeline"
@ -36,6 +35,7 @@ import (
events4 "github.com/harness/gitness/app/events/git"
events3 "github.com/harness/gitness/app/events/pullreq"
events2 "github.com/harness/gitness/app/events/repo"
"github.com/harness/gitness/app/githook"
"github.com/harness/gitness/app/pipeline/canceler"
"github.com/harness/gitness/app/pipeline/commit"
"github.com/harness/gitness/app/pipeline/file"
@ -135,7 +135,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
if err != nil {
return nil, err
}
gitAdapter, err := git.ProvideGITAdapter(typesConfig, cacheCache)
clientFactory := githook.ProvideFactory()
gitAdapter, err := git.ProvideGITAdapter(typesConfig, cacheCache, clientFactory)
if err != nil {
return nil, err
}
@ -249,7 +250,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
if err != nil {
return nil, err
}
githookController := githook.ProvideController(authorizer, principalStore, repoStore, reporter2, gitInterface, pullReqStore, provider, protectionManager)
githookController := githook.ProvideController(authorizer, principalStore, repoStore, reporter2, gitInterface, pullReqStore, provider, protectionManager, clientFactory)
serviceaccountController := serviceaccount.NewController(principalUID, authorizer, principalStore, spaceStore, repoStore, tokenStore)
principalController := principal.ProvideController(principalStore)
v := check2.ProvideCheckSanitizers()

View File

@ -18,6 +18,7 @@ import (
"context"
"github.com/harness/gitness/cache"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
@ -27,11 +28,13 @@ import (
type Adapter struct {
traceGit bool
lastCommitCache cache.Cache[CommitEntryKey, *types.Commit]
githookFactory hook.ClientFactory
}
func New(
config types.Config,
lastCommitCache cache.Cache[CommitEntryKey, *types.Commit],
githookFactory hook.ClientFactory,
) (Adapter, error) {
// TODO: should be subdir of gitRoot? What is it being used for?
setting.Git.HomePath = "home"
@ -44,5 +47,6 @@ func New(
return Adapter{
traceGit: config.Trace,
lastCommitCache: lastCommitCache,
githookFactory: githookFactory,
}, nil
}

View File

@ -23,6 +23,7 @@ import (
"time"
"github.com/harness/gitness/git/adapter"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
@ -42,11 +43,18 @@ var (
}
)
type mockClientFactory struct{}
func (f *mockClientFactory) NewClient(_ context.Context, _ map[string]string) (hook.Client, error) {
return hook.NewNoopClient([]string{"mocked client"}), nil
}
func setupGit(t *testing.T) adapter.Adapter {
t.Helper()
git, err := adapter.New(
types.Config{Trace: true},
adapter.NewInMemoryLastCommitCache(5*time.Minute),
&mockClientFactory{},
)
if err != nil {
t.Fatalf("error initializing repository: %v", err)

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package githook
package hook
import (
"context"

54
git/hook/client.go Normal file
View File

@ -0,0 +1,54 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hook
import (
"context"
"fmt"
"strings"
)
var (
// ErrNotFound is returned in case resources related to a githook call aren't found.
ErrNotFound = fmt.Errorf("not found")
)
// Client is an abstraction of a githook client that can be used to trigger githook calls.
type Client interface {
PreReceive(ctx context.Context, in PreReceiveInput) (Output, error)
Update(ctx context.Context, in UpdateInput) (Output, error)
PostReceive(ctx context.Context, in PostReceiveInput) (Output, error)
}
// ClientFactory is an abstraction of a factory that creates a new client based on the provided environment variables.
type ClientFactory interface {
NewClient(ctx context.Context, envVars map[string]string) (Client, error)
}
// TODO: move to single representation once we have our custom Git CLI wrapper.
func EnvVarsToMap(in []string) (map[string]string, error) {
out := map[string]string{}
for _, entry := range in {
key, value, ok := strings.Cut(entry, "=")
if !ok {
return nil, fmt.Errorf("unexpected entry in input: %q", entry)
}
key = strings.TrimSpace(key)
out[key] = value
}
return out, nil
}

40
git/hook/client_ noop.go Normal file
View File

@ -0,0 +1,40 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hook
import (
"context"
)
// NoopClient directly returns success with the provided messages, without any other impact.
type NoopClient struct {
messages []string
}
func NewNoopClient(messages []string) Client {
return &NoopClient{messages: messages}
}
func (c *NoopClient) PreReceive(_ context.Context, _ PreReceiveInput) (Output, error) {
return Output{Messages: c.messages}, nil
}
func (c *NoopClient) Update(_ context.Context, _ UpdateInput) (Output, error) {
return Output{Messages: c.messages}, nil
}
func (c *NoopClient) PostReceive(_ context.Context, _ PostReceiveInput) (Output, error) {
return Output{Messages: c.messages}, nil
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package githook
package hook
import (
"bufio"
@ -28,12 +28,12 @@ import (
// CLICore implements the core of a githook cli. It uses the client and execution timeout
// to perform githook operations as part of a cli.
type CLICore struct {
client *Client
client Client
executionTimeout time.Duration
}
// NewCLICore returns a new CLICore using the provided client and execution timeout.
func NewCLICore(client *Client, executionTimeout time.Duration) *CLICore {
func NewCLICore(client Client, executionTimeout time.Duration) *CLICore {
return &CLICore{
client: client,
executionTimeout: executionTimeout,
@ -47,7 +47,7 @@ func (c *CLICore) PreReceive(ctx context.Context) error {
return fmt.Errorf("failed to read updated references from std in: %w", err)
}
in := &PreReceiveInput{
in := PreReceiveInput{
RefUpdates: refUpdates,
}
@ -58,7 +58,7 @@ func (c *CLICore) PreReceive(ctx context.Context) error {
// Update executes the update git hook.
func (c *CLICore) Update(ctx context.Context, ref string, oldSHA string, newSHA string) error {
in := &UpdateInput{
in := UpdateInput{
RefUpdate: ReferenceUpdate{
Ref: ref,
Old: oldSHA,
@ -78,7 +78,7 @@ func (c *CLICore) PostReceive(ctx context.Context) error {
return fmt.Errorf("failed to read updated references from std in: %w", err)
}
in := &PostReceiveInput{
in := PostReceiveInput{
RefUpdates: refUpdates,
}
@ -88,15 +88,11 @@ func (c *CLICore) PostReceive(ctx context.Context) error {
}
//nolint:forbidigo // outputing to CMD as that's where git reads the data
func handleServerHookOutput(out *Output, err error) error {
func handleServerHookOutput(out Output, err error) error {
if err != nil {
return fmt.Errorf("an error occurred when calling the server: %w", err)
}
if out == nil {
return errors.New("the server returned an empty output")
}
// print messages before any error
if len(out.Messages) > 0 {
// add empty line before and after to make it easier readable

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package githook
package hook
import (
"bytes"

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package githook
package hook
// Output represents the output of server hook api calls.
type Output struct {

View File

@ -20,6 +20,7 @@ import (
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git/storage"
"github.com/harness/gitness/git/types"
)
const (
@ -37,14 +38,12 @@ type Service struct {
}
func New(
gitRoot string,
tmpDir string,
config types.Config,
adapter Adapter,
storage storage.Store,
gitHookPath string,
) (*Service, error) {
// Create repos folder
reposRoot := filepath.Join(gitRoot, repoSubdirName)
reposRoot := filepath.Join(config.Root, repoSubdirName)
if _, err := os.Stat(reposRoot); errors.Is(err, os.ErrNotExist) {
if err = os.MkdirAll(reposRoot, 0o700); err != nil {
return nil, err
@ -61,10 +60,10 @@ func New(
}
return &Service{
reposRoot: reposRoot,
tmpDir: tmpDir,
tmpDir: config.TmpDir,
reposGraveyard: reposGraveyard,
adapter: adapter,
store: storage,
gitHookPath: gitHookPath,
reposGraveyard: reposGraveyard,
gitHookPath: config.HookPath,
}, nil
}

View File

@ -17,6 +17,7 @@ package git
import (
"github.com/harness/gitness/cache"
"github.com/harness/gitness/git/adapter"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/git/storage"
"github.com/harness/gitness/git/types"
@ -32,16 +33,19 @@ var WireSet = wire.NewSet(
func ProvideGITAdapter(
config types.Config,
lastCommitCache cache.Cache[adapter.CommitEntryKey, *types.Commit],
githookFactory hook.ClientFactory,
) (Adapter, error) {
return adapter.New(config, lastCommitCache)
return adapter.New(config, lastCommitCache, githookFactory)
}
func ProvideService(config types.Config, adapter Adapter, storage storage.Store) (Interface, error) {
func ProvideService(
config types.Config,
adapter Adapter,
storage storage.Store,
) (Interface, error) {
return New(
config.Root,
config.TmpDir,
config,
adapter,
storage,
config.HookPath,
)
}

View File

@ -1,148 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package githook
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
)
const (
// HTTPRequestPathPreReceive is the subpath under the provided base url the client uses to call pre-receive.
HTTPRequestPathPreReceive = "pre-receive"
// HTTPRequestPathPostReceive is the subpath under the provided base url the client uses to call post-receive.
HTTPRequestPathPostReceive = "post-receive"
// HTTPRequestPathUpdate is the subpath under the provided base url the client uses to call update.
HTTPRequestPathUpdate = "update"
)
var (
ErrNotFound = fmt.Errorf("not found")
)
// Client is the Client used to call the githooks api of gitness api server.
type Client struct {
httpClient *http.Client
// baseURL is the base url of the gitness api server.
baseURL string
// requestPreparation is used to prepare the request before sending.
// This can be used to inject required headers.
requestPreparation func(*http.Request) *http.Request
}
func NewClient(httpClient *http.Client, baseURL string, requestPreparation func(*http.Request) *http.Request) *Client {
return &Client{
httpClient: httpClient,
baseURL: strings.TrimRight(baseURL, "/"),
requestPreparation: requestPreparation,
}
}
// PreReceive calls the pre-receive githook api of the gitness api server.
func (c *Client) PreReceive(ctx context.Context,
in *PreReceiveInput) (*Output, error) {
return c.githook(ctx, HTTPRequestPathPreReceive, in)
}
// Update calls the update githook api of the gitness api server.
func (c *Client) Update(ctx context.Context,
in *UpdateInput) (*Output, error) {
return c.githook(ctx, HTTPRequestPathUpdate, in)
}
// PostReceive calls the post-receive githook api of the gitness api server.
func (c *Client) PostReceive(ctx context.Context,
in *PostReceiveInput) (*Output, error) {
return c.githook(ctx, HTTPRequestPathPostReceive, in)
}
// githook executes the requested githook type using the provided input.
func (c *Client) githook(ctx context.Context, githookType string, in interface{}) (*Output, error) {
uri := c.baseURL + "/" + githookType
bodyBytes, err := json.Marshal(in)
if err != nil {
return nil, fmt.Errorf("failed to serialize input: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, bytes.NewBuffer(bodyBytes))
if err != nil {
return nil, fmt.Errorf("failed to create new http request: %w", err)
}
req.Header.Add("Content-Type", "application/json")
// prepare request if configured
if c.requestPreparation != nil {
req = c.requestPreparation(req)
}
// Execute the request
resp, err := c.httpClient.Do(req)
// ensure the body is closed after we read (independent of status code or error)
if resp != nil && resp.Body != nil {
// Use function to satisfy the linter which complains about unhandled errors otherwise
defer func() { _ = resp.Body.Close() }()
}
if err != nil {
return nil, fmt.Errorf("request execution failed: %w", err)
}
return unmarshalResponse[Output](resp)
}
// unmarshalResponse reads the response body and if there are no errors marshall's it into
// the data struct.
func unmarshalResponse[T any](resp *http.Response) (*T, error) {
if resp == nil {
return nil, errors.New("http response is empty")
}
if resp.StatusCode == http.StatusNotFound {
return nil, ErrNotFound
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected response code 200 but got: %s", resp.Status)
}
// ensure we actually got a body returned.
if resp.Body == nil {
return nil, errors.New("http response body is empty")
}
rawBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body : %w", err)
}
body := new(T)
err = json.Unmarshal(rawBody, body)
if err != nil {
return nil, fmt.Errorf("error deserializing response body: %w", err)
}
return body, nil
}

View File

@ -15,38 +15,30 @@
package types
import (
"errors"
"github.com/harness/gitness/git/hook"
)
// GithookPayload defines the GithookPayload the githook binary is initiated with when executing the git hooks.
type GithookPayload struct {
BaseURL string
// GithookInputBase contains the base input of the githook apis.
type GithookInputBase struct {
RepoID int64
PrincipalID int64
RequestID string
Disabled bool
Internal bool // Internal calls comer for the Gitness, and external calls are direct git pushes.
Internal bool // Internal calls originate from Gitness, and external calls are direct git pushes.
}
func (p *GithookPayload) Validate() error {
if p == nil {
return errors.New("payload is empty")
}
// skip further validation if githook is disabled
if p.Disabled {
return nil
}
if p.BaseURL == "" {
return errors.New("payload doesn't contain a base url")
}
if p.PrincipalID <= 0 {
return errors.New("payload doesn't contain a principal id")
}
if p.RepoID <= 0 {
return errors.New("payload doesn't contain a repo id")
}
return nil
// GithookPreReceiveInput is the input for the pre-receive githook api call.
type GithookPreReceiveInput struct {
GithookInputBase
hook.PreReceiveInput
}
// GithookUpdateInput is the input for the update githook api call.
type GithookUpdateInput struct {
GithookInputBase
hook.UpdateInput
}
// GithookPostReceiveInput is the input for the post-receive githook api call.
type GithookPostReceiveInput struct {
GithookInputBase
hook.PostReceiveInput
}