mirror of https://github.com/harness/drone.git
[Githook] Add GitHook binary (#259)
This PR adds a githook sub-package to our cmd packages, introducing the githook binary. The binary is linked as pre-receie/update/post-receive by gitrpc if configured. This is required to have a gitrpc deployment without requiring the one-in-all gitness binary on the same machine.jobatzil/rename
parent
114cdb34e0
commit
7da9bce7c1
|
@ -8,8 +8,11 @@ _research
|
|||
web/node_modules
|
||||
web/dist/files
|
||||
release
|
||||
/gitness
|
||||
/gitrpcserver
|
||||
.idea
|
||||
coverage.out
|
||||
gitness.session.sql
|
||||
|
||||
# ignore any executables we build
|
||||
/gitness
|
||||
/gitrpcserver
|
||||
/githook
|
4
Makefile
4
Makefile
|
@ -57,6 +57,10 @@ build-gitrpc: generate ## Build the gitrpc binary
|
|||
@echo "Building GitRPC Server"
|
||||
go build -ldflags="-X github.com/harness/gitness/version.GitCommit=${GIT_COMMIT} -X github.com/harness/gitness/version.Version.Major=${GITNESS_VERSION}" -o ./gitrpcserver ./cmd/gitrpcserver
|
||||
|
||||
build-githook: generate ## Build the githook binary
|
||||
@echo "Building GitHook Binary"
|
||||
go build -ldflags="-X github.com/harness/gitness/version.GitCommit=${GIT_COMMIT} -X github.com/harness/gitness/version.Version.Major=${GITNESS_VERSION}" -o ./githook ./cmd/githook
|
||||
|
||||
test: generate ## Run the go tests
|
||||
@echo "Running tests"
|
||||
go test -v -coverprofile=coverage.out ./internal/...
|
||||
|
|
14
cli/cli.go
14
cli/cli.go
|
@ -8,7 +8,6 @@ import (
|
|||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/harness/gitness/cli/operations/account"
|
||||
|
@ -18,6 +17,7 @@ import (
|
|||
"github.com/harness/gitness/cli/server"
|
||||
"github.com/harness/gitness/cli/session"
|
||||
"github.com/harness/gitness/client"
|
||||
"github.com/harness/gitness/internal/githook"
|
||||
"github.com/harness/gitness/version"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
|
@ -98,9 +98,13 @@ func initialize(ss *session.Session, httpClient *client.HTTPClient) error {
|
|||
}
|
||||
|
||||
func getArguments() []string {
|
||||
// for git operations, the first argument is "hooks", followed by other relevant arguments
|
||||
if os.Args[0] == "hooks/update" || os.Args[0] == "hooks/pre-receive" || os.Args[0] == "hooks/post-receive" {
|
||||
return append([]string{"hooks", path.Base(os.Args[0])}, os.Args[1:]...)
|
||||
command := os.Args[0]
|
||||
args := os.Args[1:]
|
||||
|
||||
// in case of githooks, translate the arguments comming from git to work with gitness.
|
||||
if gitArgs, fromGit := githook.SanitizeArgsForGit(command, args); fromGit {
|
||||
return append([]string{hooks.ParamHooks}, gitArgs...)
|
||||
}
|
||||
return os.Args[1:]
|
||||
|
||||
return args
|
||||
}
|
||||
|
|
|
@ -5,12 +5,17 @@
|
|||
package hooks
|
||||
|
||||
import (
|
||||
"github.com/harness/gitness/internal/githook"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
// ParamHooks defines the parameter for the git hooks sub-commands.
|
||||
ParamHooks = "hooks"
|
||||
)
|
||||
|
||||
func Register(app *kingpin.Application) {
|
||||
cmd := app.Command("hooks", "manage git server hooks")
|
||||
registerUpdate(cmd)
|
||||
registerPostReceive(cmd)
|
||||
registerPreReceive(cmd)
|
||||
subCmd := app.Command(ParamHooks, "manage git server hooks")
|
||||
githook.RegisterAll(subCmd)
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/internal/githook"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type postReceiveCommand struct{}
|
||||
|
||||
func (c *postReceiveCommand) run(*kingpin.ParseContext) error {
|
||||
cli, err := githook.NewCLI()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create githook cli: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
return cli.PostReceive(ctx)
|
||||
}
|
||||
|
||||
func registerPostReceive(app *kingpin.CmdClause) {
|
||||
c := &postReceiveCommand{}
|
||||
|
||||
app.Command("post-receive", "hook that is executed after all references of the push got updated").
|
||||
Action(c.run)
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/internal/githook"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type preReceiveCommand struct{}
|
||||
|
||||
func (c *preReceiveCommand) run(*kingpin.ParseContext) error {
|
||||
cli, err := githook.NewCLI()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create githook cli: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
return cli.PreReceive(ctx)
|
||||
}
|
||||
|
||||
func registerPreReceive(app *kingpin.CmdClause) {
|
||||
c := &preReceiveCommand{}
|
||||
|
||||
app.Command("pre-receive", "hook that is executed before any reference of the push is updated").
|
||||
Action(c.run)
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/internal/githook"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type updateCommand struct {
|
||||
ref string
|
||||
old string
|
||||
new string
|
||||
}
|
||||
|
||||
func (c *updateCommand) run(*kingpin.ParseContext) error {
|
||||
cli, err := githook.NewCLI()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create githook cli: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
return cli.Update(ctx, c.ref, c.old, c.new)
|
||||
}
|
||||
|
||||
func registerUpdate(app *kingpin.CmdClause) {
|
||||
c := &updateCommand{}
|
||||
|
||||
cmd := app.Command("update", "hook that is executed before the specific reference gets updated").
|
||||
Action(c.run)
|
||||
|
||||
cmd.Arg("ref", "reference for which the hook is executed").
|
||||
Required().
|
||||
StringVar(&c.ref)
|
||||
|
||||
cmd.Arg("old", "old commit sha").
|
||||
Required().
|
||||
StringVar(&c.old)
|
||||
|
||||
cmd.Arg("new", "new commit sha").
|
||||
Required().
|
||||
StringVar(&c.new)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/harness/gitness/internal/githook"
|
||||
"github.com/harness/gitness/version"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
application = "githook"
|
||||
description = "A lightweight executable that forwards git server hooks to the gitness API server."
|
||||
)
|
||||
|
||||
func main() {
|
||||
// ensure args are properly sanitized if called by git
|
||||
command := os.Args[0]
|
||||
args := os.Args[1:]
|
||||
args, _ = githook.SanitizeArgsForGit(command, args)
|
||||
|
||||
// define new kingpin application and register githooks globally
|
||||
app := kingpin.New(application, description)
|
||||
app.Version(version.Version.String())
|
||||
githook.RegisterAll(app)
|
||||
|
||||
// trigger execution
|
||||
kingpin.MustParse(app.Parse(args))
|
||||
}
|
|
@ -22,19 +22,12 @@ const (
|
|||
gitReferenceNamePrefixTag = "refs/tags/"
|
||||
)
|
||||
|
||||
// PostReceiveInput represents the input of the post-receive git hook.
|
||||
type PostReceiveInput struct {
|
||||
BaseInput
|
||||
// RefUpdates contains all references that got updated as part of the git operation.
|
||||
RefUpdates []ReferenceUpdate `json:"ref_updates"`
|
||||
}
|
||||
|
||||
// PostReceive executes the post-receive hook for a git repository.
|
||||
func (c *Controller) PostReceive(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
in *PostReceiveInput,
|
||||
) (*ServerHookOutput, error) {
|
||||
in *types.PostReceiveInput,
|
||||
) (*types.ServerHookOutput, error) {
|
||||
if in == nil {
|
||||
return nil, fmt.Errorf("input is nil")
|
||||
}
|
||||
|
@ -42,13 +35,13 @@ func (c *Controller) PostReceive(
|
|||
// report ref events (best effort)
|
||||
c.reportReferenceEvents(ctx, in)
|
||||
|
||||
return &ServerHookOutput{}, nil
|
||||
return &types.ServerHookOutput{}, nil
|
||||
}
|
||||
|
||||
// reportReferenceEvents is reporting reference events to the event system.
|
||||
// NOTE: keep best effort for now as it doesn't change the outcome of the git operation.
|
||||
// TODO: in the future we might want to think about propagating errors so user is aware of events not being triggered.
|
||||
func (c *Controller) reportReferenceEvents(ctx context.Context, in *PostReceiveInput) {
|
||||
func (c *Controller) reportReferenceEvents(ctx context.Context, in *types.PostReceiveInput) {
|
||||
for _, refUpdate := range in.RefUpdates {
|
||||
switch {
|
||||
case strings.HasPrefix(refUpdate.Ref, gitReferenceNamePrefixBranch):
|
||||
|
@ -62,7 +55,7 @@ func (c *Controller) reportReferenceEvents(ctx context.Context, in *PostReceiveI
|
|||
}
|
||||
|
||||
func (c *Controller) reportBranchEvent(ctx context.Context,
|
||||
principalID int64, repoID int64, branchUpdate ReferenceUpdate) {
|
||||
principalID int64, repoID int64, branchUpdate types.ReferenceUpdate) {
|
||||
switch {
|
||||
case branchUpdate.Old == types.NilSHA:
|
||||
c.gitReporter.BranchCreated(ctx, &events.BranchCreatedPayload{
|
||||
|
@ -91,7 +84,7 @@ func (c *Controller) reportBranchEvent(ctx context.Context,
|
|||
}
|
||||
|
||||
func (c *Controller) reportTagEvent(ctx context.Context,
|
||||
principalID int64, repoID int64, tagUpdate ReferenceUpdate) {
|
||||
principalID int64, repoID int64, tagUpdate types.ReferenceUpdate) {
|
||||
switch {
|
||||
case tagUpdate.Old == types.NilSHA:
|
||||
c.gitReporter.TagCreated(ctx, &events.TagCreatedPayload{
|
||||
|
|
|
@ -16,19 +16,12 @@ import (
|
|||
"github.com/gotidy/ptr"
|
||||
)
|
||||
|
||||
// PreReceiveInput represents the input of the pre-receive git hook.
|
||||
type PreReceiveInput struct {
|
||||
BaseInput
|
||||
// RefUpdates contains all references that are being updated as part of the git operation.
|
||||
RefUpdates []ReferenceUpdate `json:"ref_updates"`
|
||||
}
|
||||
|
||||
// PreReceive executes the pre-receive hook for a git repository.
|
||||
func (c *Controller) PreReceive(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
in *PreReceiveInput,
|
||||
) (*ServerHookOutput, error) {
|
||||
in *types.PreReceiveInput,
|
||||
) (*types.ServerHookOutput, error) {
|
||||
if in == nil {
|
||||
return nil, fmt.Errorf("input is nil")
|
||||
}
|
||||
|
@ -45,15 +38,16 @@ func (c *Controller) PreReceive(
|
|||
|
||||
// TODO: Branch Protection, Block non-brach/tag refs (?), ...
|
||||
|
||||
return &ServerHookOutput{}, nil
|
||||
return &types.ServerHookOutput{}, nil
|
||||
}
|
||||
|
||||
func (c *Controller) blockDefaultBranchDeletion(repo *types.Repository, in *PreReceiveInput) *ServerHookOutput {
|
||||
func (c *Controller) blockDefaultBranchDeletion(repo *types.Repository,
|
||||
in *types.PreReceiveInput) *types.ServerHookOutput {
|
||||
repoDefaultBranchRef := gitReferenceNamePrefixBranch + repo.DefaultBranch
|
||||
|
||||
for _, refUpdate := range in.RefUpdates {
|
||||
if refUpdate.New == types.NilSHA && refUpdate.Ref == repoDefaultBranchRef {
|
||||
return &ServerHookOutput{
|
||||
return &types.ServerHookOutput{
|
||||
Error: ptr.String(usererror.ErrDefaultBranchCantBeDeleted.Error()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,26 +9,20 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
// UpdateInput represents the input of the update git hook.
|
||||
type UpdateInput struct {
|
||||
BaseInput
|
||||
// RefUpdate contains information about the reference that is being updated.
|
||||
RefUpdate ReferenceUpdate `json:"ref_update"`
|
||||
}
|
||||
|
||||
// Update executes the update hook for a git repository.
|
||||
func (c *Controller) Update(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
in *UpdateInput,
|
||||
) (*ServerHookOutput, error) {
|
||||
in *types.UpdateInput,
|
||||
) (*types.ServerHookOutput, error) {
|
||||
if in == nil {
|
||||
return nil, fmt.Errorf("input is nil")
|
||||
}
|
||||
|
||||
// We currently don't have any update action (nothing planned as of now)
|
||||
|
||||
return &ServerHookOutput{}, nil
|
||||
return &types.ServerHookOutput{}, nil
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/harness/gitness/internal/api/controller/githook"
|
||||
"github.com/harness/gitness/internal/api/render"
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
// HandlePostReceive returns a handler function that handles post-receive git hooks.
|
||||
|
@ -19,7 +20,7 @@ func HandlePostReceive(githookCtrl *githook.Controller) http.HandlerFunc {
|
|||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
in := new(githook.PostReceiveInput)
|
||||
in := new(types.PostReceiveInput)
|
||||
err := json.NewDecoder(r.Body).Decode(in)
|
||||
if err != nil {
|
||||
render.BadRequestf(w, "Invalid Request Body: %s.", err)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/harness/gitness/internal/api/controller/githook"
|
||||
"github.com/harness/gitness/internal/api/render"
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
// HandlePreReceive returns a handler function that handles pre-receive git hooks.
|
||||
|
@ -19,7 +20,7 @@ func HandlePreReceive(githookCtrl *githook.Controller) http.HandlerFunc {
|
|||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
in := new(githook.PreReceiveInput)
|
||||
in := new(types.PreReceiveInput)
|
||||
err := json.NewDecoder(r.Body).Decode(in)
|
||||
if err != nil {
|
||||
render.BadRequestf(w, "Invalid Request Body: %s.", err)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/harness/gitness/internal/api/controller/githook"
|
||||
"github.com/harness/gitness/internal/api/render"
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
// HandleUpdate returns a handler function that handles update git hooks.
|
||||
|
@ -19,7 +20,7 @@ func HandleUpdate(githookCtrl *githook.Controller) http.HandlerFunc {
|
|||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
in := new(githook.UpdateInput)
|
||||
in := new(types.UpdateInput)
|
||||
err := json.NewDecoder(r.Body).Decode(in)
|
||||
if err != nil {
|
||||
render.BadRequestf(w, "Invalid Request Body: %s.", err)
|
||||
|
|
|
@ -5,156 +5,142 @@
|
|||
package githook
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/internal/api/controller/githook"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// CLI represents the githook cli implementation.
|
||||
type CLI struct {
|
||||
payload *Payload
|
||||
client *client
|
||||
const (
|
||||
// ParamPreReceive is the parameter under which the pre-receive operation is registered.
|
||||
ParamPreReceive = "pre-receive"
|
||||
// ParamUpdate is the parameter under which the update operation is registered.
|
||||
ParamUpdate = "update"
|
||||
// ParamPostReceive is the parameter under which the post-receive operation is registered.
|
||||
ParamPostReceive = "post-receive"
|
||||
|
||||
// CommandNamePreReceive is the command used by git for the pre-receive hook
|
||||
// (os.args[0] == "hooks/pre-receive").
|
||||
CommandNamePreReceive = "hooks/pre-receive"
|
||||
// CommandNameUpdate is the command used by git for the update hook
|
||||
// (os.args[0] == "hooks/update").
|
||||
CommandNameUpdate = "hooks/update"
|
||||
// CommandNamePostReceive is the command used by git for the post-receive hook
|
||||
// (os.args[0] == "hooks/post-receive").
|
||||
CommandNamePostReceive = "hooks/post-receive"
|
||||
)
|
||||
|
||||
var (
|
||||
// ExecutionTimeout is the timeout used for githook CLI runs.
|
||||
ExecutionTimeout = 3 * time.Minute
|
||||
)
|
||||
|
||||
// SanitizeArgsForGit sanitizes the command line arguments (os.Args) if the command indicates they are comming from git.
|
||||
// Returns the santized args and true if the call comes from git, otherwise the original args are returned with false.
|
||||
func SanitizeArgsForGit(command string, args []string) ([]string, bool) {
|
||||
switch command {
|
||||
case CommandNamePreReceive:
|
||||
return append([]string{ParamPreReceive}, args...), true
|
||||
case CommandNameUpdate:
|
||||
return append([]string{ParamUpdate}, args...), true
|
||||
case CommandNamePostReceive:
|
||||
return append([]string{ParamPostReceive}, args...), true
|
||||
default:
|
||||
return args, false
|
||||
}
|
||||
}
|
||||
|
||||
// NewCLI creates a new CLI instance from environment variables for githook execution.
|
||||
func NewCLI() (*CLI, error) {
|
||||
payload, err := loadPayloadFromEnvironment()
|
||||
// KingpinRegister is an abstraction of an entity that allows to register commands.
|
||||
// This is required to allow registering hook commands both on application and sub command level.
|
||||
type KingpinRegister interface {
|
||||
Command(name, help string) *kingpin.CmdClause
|
||||
}
|
||||
|
||||
// RegisterAll registers all githook commands.
|
||||
func RegisterAll(cmd KingpinRegister) {
|
||||
RegisterPreReceive(cmd)
|
||||
RegisterUpdate(cmd)
|
||||
RegisterPostReceive(cmd)
|
||||
}
|
||||
|
||||
// RegisterPreReceive registers the pre-receive githook command.
|
||||
func RegisterPreReceive(cmd KingpinRegister) {
|
||||
c := &preReceiveCommand{}
|
||||
|
||||
cmd.Command(ParamPreReceive, "hook that is executed before any reference of the push is updated").
|
||||
Action(c.run)
|
||||
}
|
||||
|
||||
// RegisterUpdate registers the update githook command.
|
||||
func RegisterUpdate(cmd KingpinRegister) {
|
||||
c := &updateCommand{}
|
||||
|
||||
subCmd := cmd.Command(ParamUpdate, "hook that is executed before the specific reference gets updated").
|
||||
Action(c.run)
|
||||
|
||||
subCmd.Arg("ref", "reference for which the hook is executed").
|
||||
Required().
|
||||
StringVar(&c.ref)
|
||||
|
||||
subCmd.Arg("old", "old commit sha").
|
||||
Required().
|
||||
StringVar(&c.oldSHA)
|
||||
|
||||
subCmd.Arg("new", "new commit sha").
|
||||
Required().
|
||||
StringVar(&c.newSHA)
|
||||
}
|
||||
|
||||
// RegisterPostReceive registers the post-receive githook command.
|
||||
func RegisterPostReceive(cmd KingpinRegister) {
|
||||
c := &postReceiveCommand{}
|
||||
|
||||
cmd.Command(ParamPostReceive, "hook that is executed after all references of the push got updated").
|
||||
Action(c.run)
|
||||
}
|
||||
|
||||
type preReceiveCommand struct{}
|
||||
|
||||
func (c *preReceiveCommand) run(*kingpin.ParseContext) error {
|
||||
return run(func(ctx context.Context, hook *GitHook) error {
|
||||
return hook.PreReceive(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
type updateCommand struct {
|
||||
ref string
|
||||
oldSHA string
|
||||
newSHA string
|
||||
}
|
||||
|
||||
func (c *updateCommand) run(*kingpin.ParseContext) error {
|
||||
return run(func(ctx context.Context, hook *GitHook) error {
|
||||
return hook.Update(ctx, c.ref, c.oldSHA, c.newSHA)
|
||||
})
|
||||
}
|
||||
|
||||
type postReceiveCommand struct{}
|
||||
|
||||
func (c *postReceiveCommand) run(*kingpin.ParseContext) error {
|
||||
return run(func(ctx context.Context, hook *GitHook) error {
|
||||
return hook.PostReceive(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func run(fn func(ctx context.Context, hook *GitHook) error) error {
|
||||
// load hook here (as it loads environment variables, has to be done at time of execution, not register)
|
||||
hook, err := NewFromEnvironment()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load payload: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return &CLI{
|
||||
payload: payload,
|
||||
client: &client{
|
||||
httpClient: http.DefaultClient,
|
||||
baseURL: payload.APIBaseURL,
|
||||
requestPreparation: func(r *http.Request) *http.Request {
|
||||
// TODO: reference single constant (together with gitness middleware)
|
||||
r.Header.Add("X-Request-Id", payload.RequestID)
|
||||
return r
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PreReceive executes the pre-receive git hook.
|
||||
func (c *CLI) PreReceive(ctx context.Context) error {
|
||||
refUpdates, err := getUpdatedReferencesFromStdIn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read updated references from std in: %w", err)
|
||||
}
|
||||
|
||||
in := &githook.PreReceiveInput{
|
||||
BaseInput: githook.BaseInput{
|
||||
RepoID: c.payload.RepoID,
|
||||
PrincipalID: c.payload.PrincipalID,
|
||||
},
|
||||
RefUpdates: refUpdates,
|
||||
}
|
||||
|
||||
out, err := c.client.PreReceive(ctx, in)
|
||||
|
||||
return handleServerHookOutput(out, err)
|
||||
}
|
||||
|
||||
// Update executes the update git hook.
|
||||
func (c *CLI) Update(ctx context.Context, ref string, oldSHA string, newSHA string) error {
|
||||
in := &githook.UpdateInput{
|
||||
BaseInput: githook.BaseInput{
|
||||
RepoID: c.payload.RepoID,
|
||||
PrincipalID: c.payload.PrincipalID,
|
||||
},
|
||||
RefUpdate: githook.ReferenceUpdate{
|
||||
Ref: ref,
|
||||
Old: oldSHA,
|
||||
New: newSHA,
|
||||
},
|
||||
}
|
||||
|
||||
out, err := c.client.Update(ctx, in)
|
||||
|
||||
return handleServerHookOutput(out, err)
|
||||
}
|
||||
|
||||
// PostReceive executes the post-receive git hook.
|
||||
func (c *CLI) PostReceive(ctx context.Context) error {
|
||||
refUpdates, err := getUpdatedReferencesFromStdIn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read updated references from std in: %w", err)
|
||||
}
|
||||
|
||||
in := &githook.PostReceiveInput{
|
||||
BaseInput: githook.BaseInput{
|
||||
RepoID: c.payload.RepoID,
|
||||
PrincipalID: c.payload.PrincipalID,
|
||||
},
|
||||
RefUpdates: refUpdates,
|
||||
}
|
||||
|
||||
out, err := c.client.PostReceive(ctx, in)
|
||||
|
||||
return handleServerHookOutput(out, err)
|
||||
}
|
||||
|
||||
func handleServerHookOutput(out *githook.ServerHookOutput, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occured when calling the server: %w", err)
|
||||
}
|
||||
|
||||
if out == nil {
|
||||
return errors.New("the server returned an empty output")
|
||||
}
|
||||
|
||||
if out.Error != nil {
|
||||
return errors.New(*out.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUpdatedReferencesFromStdIn reads the updated references provided by git from stdin.
|
||||
// The expected format is "<old-value> SP <new-value> SP <ref-name> LF"
|
||||
// For more details see https://git-scm.com/docs/githooks#pre-receive
|
||||
func getUpdatedReferencesFromStdIn() ([]githook.ReferenceUpdate, error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
updatedRefs := []githook.ReferenceUpdate{}
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
// if end of file is reached, break the loop
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Msgf("Error when reading from standard input - %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(line) == 0 {
|
||||
return nil, errors.New("ref data from stdin contains empty line - not expected")
|
||||
}
|
||||
|
||||
// splitting line of expected form "<old-value> SP <new-value> SP <ref-name> LF"
|
||||
splitGitHookData := strings.Split(line[:len(line)-1], " ")
|
||||
if len(splitGitHookData) != 3 {
|
||||
return nil, fmt.Errorf("received invalid data format or didn't receive enough parameters - %v",
|
||||
splitGitHookData)
|
||||
}
|
||||
|
||||
updatedRefs = append(updatedRefs, githook.ReferenceUpdate{
|
||||
Old: splitGitHookData[0],
|
||||
New: splitGitHookData[1],
|
||||
Ref: splitGitHookData[2],
|
||||
})
|
||||
}
|
||||
|
||||
return updatedRefs, nil
|
||||
// Create context that listens for the interrupt signal from the OS and has a timeout.
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
ctx, cancel := context.WithTimeout(ctx, ExecutionTimeout)
|
||||
defer cancel()
|
||||
|
||||
return fn(ctx, hook)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/internal/api/controller/githook"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/version"
|
||||
)
|
||||
|
||||
|
@ -35,24 +35,24 @@ type client struct {
|
|||
|
||||
// PreReceive calls the pre-receive githook api of the gitness api server.
|
||||
func (c *client) PreReceive(ctx context.Context,
|
||||
in *githook.PreReceiveInput) (*githook.ServerHookOutput, error) {
|
||||
in *types.PreReceiveInput) (*types.ServerHookOutput, error) {
|
||||
return c.githook(ctx, "pre-receive", in)
|
||||
}
|
||||
|
||||
// Update calls the update githook api of the gitness api server.
|
||||
func (c *client) Update(ctx context.Context,
|
||||
in *githook.UpdateInput) (*githook.ServerHookOutput, error) {
|
||||
in *types.UpdateInput) (*types.ServerHookOutput, error) {
|
||||
return c.githook(ctx, "update", in)
|
||||
}
|
||||
|
||||
// PostReceive calls the post-receive githook api of the gitness api server.
|
||||
func (c *client) PostReceive(ctx context.Context,
|
||||
in *githook.PostReceiveInput) (*githook.ServerHookOutput, error) {
|
||||
in *types.PostReceiveInput) (*types.ServerHookOutput, error) {
|
||||
return c.githook(ctx, "post-receive", in)
|
||||
}
|
||||
|
||||
// githook executes the requested githook type using the provided input.
|
||||
func (c *client) githook(ctx context.Context, githookType string, in interface{}) (*githook.ServerHookOutput, error) {
|
||||
func (c *client) githook(ctx context.Context, githookType string, in interface{}) (*types.ServerHookOutput, error) {
|
||||
uri := fmt.Sprintf("%s/v1/internal/git-hooks/%s", c.baseURL, githookType)
|
||||
bodyBytes, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
|
@ -84,7 +84,7 @@ func (c *client) githook(ctx context.Context, githookType string, in interface{}
|
|||
return nil, fmt.Errorf("request execution failed: %w", err)
|
||||
}
|
||||
|
||||
return unmarshalResponse[githook.ServerHookOutput](resp)
|
||||
return unmarshalResponse[types.ServerHookOutput](resp)
|
||||
}
|
||||
|
||||
// unmarshalResponse reads the response body and if there are no errors marshall's it into
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package githook
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
// GitHook represents the githook implementation.
|
||||
type GitHook struct {
|
||||
payload *Payload
|
||||
client *client
|
||||
}
|
||||
|
||||
// NewFromEnvironment creates a new githook app from environment variables for githook execution.
|
||||
func NewFromEnvironment() (*GitHook, error) {
|
||||
payload, err := loadPayloadFromEnvironment()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load payload: %w", err)
|
||||
}
|
||||
|
||||
return &GitHook{
|
||||
payload: payload,
|
||||
client: &client{
|
||||
httpClient: http.DefaultClient,
|
||||
baseURL: payload.APIBaseURL,
|
||||
requestPreparation: func(r *http.Request) *http.Request {
|
||||
// TODO: reference single constant (together with gitness middleware)
|
||||
r.Header.Add("X-Request-Id", payload.RequestID)
|
||||
return r
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PreReceive executes the pre-receive git hook.
|
||||
func (c *GitHook) PreReceive(ctx context.Context) error {
|
||||
refUpdates, err := getUpdatedReferencesFromStdIn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read updated references from std in: %w", err)
|
||||
}
|
||||
|
||||
in := &types.PreReceiveInput{
|
||||
BaseInput: types.BaseInput{
|
||||
RepoID: c.payload.RepoID,
|
||||
PrincipalID: c.payload.PrincipalID,
|
||||
},
|
||||
RefUpdates: refUpdates,
|
||||
}
|
||||
|
||||
out, err := c.client.PreReceive(ctx, in)
|
||||
|
||||
return handleServerHookOutput(out, err)
|
||||
}
|
||||
|
||||
// Update executes the update git hook.
|
||||
func (c *GitHook) Update(ctx context.Context, ref string, oldSHA string, newSHA string) error {
|
||||
in := &types.UpdateInput{
|
||||
BaseInput: types.BaseInput{
|
||||
RepoID: c.payload.RepoID,
|
||||
PrincipalID: c.payload.PrincipalID,
|
||||
},
|
||||
RefUpdate: types.ReferenceUpdate{
|
||||
Ref: ref,
|
||||
Old: oldSHA,
|
||||
New: newSHA,
|
||||
},
|
||||
}
|
||||
|
||||
out, err := c.client.Update(ctx, in)
|
||||
|
||||
return handleServerHookOutput(out, err)
|
||||
}
|
||||
|
||||
// PostReceive executes the post-receive git hook.
|
||||
func (c *GitHook) PostReceive(ctx context.Context) error {
|
||||
refUpdates, err := getUpdatedReferencesFromStdIn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read updated references from std in: %w", err)
|
||||
}
|
||||
|
||||
in := &types.PostReceiveInput{
|
||||
BaseInput: types.BaseInput{
|
||||
RepoID: c.payload.RepoID,
|
||||
PrincipalID: c.payload.PrincipalID,
|
||||
},
|
||||
RefUpdates: refUpdates,
|
||||
}
|
||||
|
||||
out, err := c.client.PostReceive(ctx, in)
|
||||
|
||||
return handleServerHookOutput(out, err)
|
||||
}
|
||||
|
||||
func handleServerHookOutput(out *types.ServerHookOutput, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occured when calling the server: %w", err)
|
||||
}
|
||||
|
||||
if out == nil {
|
||||
return errors.New("the server returned an empty output")
|
||||
}
|
||||
|
||||
if out.Error != nil {
|
||||
return errors.New(*out.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUpdatedReferencesFromStdIn reads the updated references provided by git from stdin.
|
||||
// The expected format is "<old-value> SP <new-value> SP <ref-name> LF"
|
||||
// For more details see https://git-scm.com/docs/githooks#pre-receive
|
||||
func getUpdatedReferencesFromStdIn() ([]types.ReferenceUpdate, error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
updatedRefs := []types.ReferenceUpdate{}
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
// if end of file is reached, break the loop
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("Error when reading from standard input - %s\n", err) //nolint:forbidigo // executes as cli.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(line) == 0 {
|
||||
return nil, errors.New("ref data from stdin contains empty line - not expected")
|
||||
}
|
||||
|
||||
// splitting line of expected form "<old-value> SP <new-value> SP <ref-name> LF"
|
||||
splitGitHookData := strings.Split(line[:len(line)-1], " ")
|
||||
if len(splitGitHookData) != 3 {
|
||||
return nil, fmt.Errorf("received invalid data format or didn't receive enough parameters - %v",
|
||||
splitGitHookData)
|
||||
}
|
||||
|
||||
updatedRefs = append(updatedRefs, types.ReferenceUpdate{
|
||||
Old: splitGitHookData[0],
|
||||
New: splitGitHookData[1],
|
||||
Ref: splitGitHookData[2],
|
||||
})
|
||||
}
|
||||
|
||||
return updatedRefs, nil
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package types
|
||||
|
||||
// ServerHookOutput represents the output of server hook api calls.
|
||||
// TODO: support non-error messages (once we need it).
|
||||
type ServerHookOutput struct {
|
||||
// Error contains the user facing error (like "branch is protected", ...).
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ReferenceUpdate represents an update of a git reference.
|
||||
type ReferenceUpdate struct {
|
||||
// Ref is the full name of the reference that got updated.
|
||||
Ref string `json:"ref"`
|
||||
// Old is the old commmit hash (before the update).
|
||||
Old string `json:"old"`
|
||||
// New is the new commit hash (after the update).
|
||||
New string `json:"new"`
|
||||
}
|
||||
|
||||
// BaseInput contains the base input for any githook api call.
|
||||
type BaseInput struct {
|
||||
RepoID int64 `json:"repo_id"`
|
||||
PrincipalID int64 `json:"principal_id"`
|
||||
}
|
||||
|
||||
// PostReceiveInput represents the input of the post-receive git hook.
|
||||
type PostReceiveInput struct {
|
||||
BaseInput
|
||||
// RefUpdates contains all references that got updated as part of the git operation.
|
||||
RefUpdates []ReferenceUpdate `json:"ref_updates"`
|
||||
}
|
||||
|
||||
// PreReceiveInput represents the input of the pre-receive git hook.
|
||||
type PreReceiveInput struct {
|
||||
BaseInput
|
||||
// RefUpdates contains all references that are being updated as part of the git operation.
|
||||
RefUpdates []ReferenceUpdate `json:"ref_updates"`
|
||||
}
|
||||
|
||||
// UpdateInput represents the input of the update git hook.
|
||||
type UpdateInput struct {
|
||||
BaseInput
|
||||
// RefUpdate contains information about the reference that is being updated.
|
||||
RefUpdate ReferenceUpdate `json:"ref_update"`
|
||||
}
|
Loading…
Reference in New Issue