mirror of
https://github.com/harness/drone.git
synced 2025-05-05 15:32:56 +00:00
[Githook] Introduce InMemory Githook (#916)
This commit is contained in:
parent
eac99c3686
commit
1d8d50a188
@ -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.
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(
|
||||
|
131
app/githook/client_controller.go
Normal file
131
app/githook/client_controller.go
Normal 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
181
app/githook/client_rest.go
Normal 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
|
||||
}
|
@ -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
58
app/githook/types.go
Normal 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
70
app/githook/wire.go
Normal 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
|
||||
}
|
@ -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"
|
||||
|
||||
|
@ -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...)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
54
git/hook/client.go
Normal 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
40
git/hook/client_ noop.go
Normal 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
|
||||
}
|
@ -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
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package githook
|
||||
package hook
|
||||
|
||||
import (
|
||||
"bytes"
|
@ -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 {
|
@ -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
|
||||
}
|
||||
|
14
git/wire.go
14
git/wire.go
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user