mirror of https://github.com/harness/drone.git
[GIHA] Introduce `githa` custom githook binary (#186)
parent
99b3ac1c79
commit
5543fba0b4
|
@ -15,4 +15,5 @@ gitness.session.sql
|
|||
# ignore any executables we build
|
||||
/gitness
|
||||
/gitrpcserver
|
||||
/githook
|
||||
/gitness-githook
|
||||
/githa-githook
|
10
Makefile
10
Makefile
|
@ -60,9 +60,13 @@ build-gitrpc: generate ## Build the gitrpc binary
|
|||
@echo "Building GitRPC Server"
|
||||
go build -tags=${BUILD_TAGS} -ldflags=${LDFLAGS} -o ./gitrpcserver ./cmd/gitrpcserver
|
||||
|
||||
build-githook: generate ## Build the githook binary
|
||||
@echo "Building GitHook Binary"
|
||||
go build -tags=${BUILD_TAGS} -ldflags=${LDFLAGS} -o ./githook ./cmd/githook
|
||||
build-githook: generate ## Build the githook binary for gitness
|
||||
@echo "Building gitness GitHook Binary"
|
||||
go build -tags=${BUILD_TAGS} -ldflags=${LDFLAGS} -o ./gitness-githook ./cmd/gitness-githook
|
||||
|
||||
build-githa-githook: generate ## Build the githook binary for githa
|
||||
@echo "Building githa GitHook Binary"
|
||||
go build -tags=${BUILD_TAGS} -ldflags=${LDFLAGS} -o ./githa-githook ./cmd/githa-githook
|
||||
|
||||
test: generate ## Run the go tests
|
||||
@echo "Running tests"
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/harness/gitness/cli/operations/hooks"
|
||||
"github.com/harness/gitness/internal/githook"
|
||||
"github.com/harness/gitness/githook"
|
||||
)
|
||||
|
||||
func GetArguments() []string {
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
package hooks
|
||||
|
||||
import (
|
||||
"github.com/harness/gitness/internal/githook"
|
||||
"github.com/harness/gitness/githook"
|
||||
gitnessgithook "github.com/harness/gitness/internal/githook"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
@ -17,5 +18,5 @@ const (
|
|||
|
||||
func Register(app *kingpin.Application) {
|
||||
subCmd := app.Command(ParamHooks, "manage git server hooks")
|
||||
githook.RegisterAll(subCmd)
|
||||
githook.RegisterAll(subCmd, gitnessgithook.LoadFromEnvironment)
|
||||
}
|
||||
|
|
|
@ -1,34 +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 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))
|
||||
}
|
|
@ -9,7 +9,6 @@ import (
|
|||
"errors"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
@ -33,11 +32,6 @@ const (
|
|||
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 coming 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) {
|
||||
|
@ -59,24 +53,38 @@ type KingpinRegister interface {
|
|||
Command(name, help string) *kingpin.CmdClause
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrDisabled can be returned by the loading function to indicate the githook has been disabled.
|
||||
// Returning the error will cause the githook execution to be skipped (githook is noop and returns success).
|
||||
ErrDisabled = errors.New("githook disabled")
|
||||
)
|
||||
|
||||
// LoadCLICoreFunc is a function that creates a new CLI core that's used for githook cli execution.
|
||||
// This allows users to initialize their own CLI core with custom Client and configuration.
|
||||
type LoadCLICoreFunc func() (*CLICore, error)
|
||||
|
||||
// RegisterAll registers all githook commands.
|
||||
func RegisterAll(cmd KingpinRegister) {
|
||||
RegisterPreReceive(cmd)
|
||||
RegisterUpdate(cmd)
|
||||
RegisterPostReceive(cmd)
|
||||
func RegisterAll(cmd KingpinRegister, loadCoreFn LoadCLICoreFunc) {
|
||||
RegisterPreReceive(cmd, loadCoreFn)
|
||||
RegisterUpdate(cmd, loadCoreFn)
|
||||
RegisterPostReceive(cmd, loadCoreFn)
|
||||
}
|
||||
|
||||
// RegisterPreReceive registers the pre-receive githook command.
|
||||
func RegisterPreReceive(cmd KingpinRegister) {
|
||||
c := &preReceiveCommand{}
|
||||
func RegisterPreReceive(cmd KingpinRegister, loadCoreFn LoadCLICoreFunc) {
|
||||
c := &preReceiveCommand{
|
||||
loadCoreFn: loadCoreFn,
|
||||
}
|
||||
|
||||
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{}
|
||||
func RegisterUpdate(cmd KingpinRegister, loadCoreFn LoadCLICoreFunc) {
|
||||
c := &updateCommand{
|
||||
loadCoreFn: loadCoreFn,
|
||||
}
|
||||
|
||||
subCmd := cmd.Command(ParamUpdate, "hook that is executed before the specific reference gets updated").
|
||||
Action(c.run)
|
||||
|
@ -95,56 +103,64 @@ func RegisterUpdate(cmd KingpinRegister) {
|
|||
}
|
||||
|
||||
// RegisterPostReceive registers the post-receive githook command.
|
||||
func RegisterPostReceive(cmd KingpinRegister) {
|
||||
c := &postReceiveCommand{}
|
||||
func RegisterPostReceive(cmd KingpinRegister, loadCoreFn LoadCLICoreFunc) {
|
||||
c := &postReceiveCommand{
|
||||
loadCoreFn: loadCoreFn,
|
||||
}
|
||||
|
||||
cmd.Command(ParamPostReceive, "hook that is executed after all references of the push got updated").
|
||||
Action(c.run)
|
||||
}
|
||||
|
||||
type preReceiveCommand struct{}
|
||||
type preReceiveCommand struct {
|
||||
loadCoreFn LoadCLICoreFunc
|
||||
}
|
||||
|
||||
func (c *preReceiveCommand) run(*kingpin.ParseContext) error {
|
||||
return run(func(ctx context.Context, hook *GitHook) error {
|
||||
return hook.PreReceive(ctx)
|
||||
return run(c.loadCoreFn, func(ctx context.Context, core *CLICore) error {
|
||||
return core.PreReceive(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
type updateCommand struct {
|
||||
loadCoreFn LoadCLICoreFunc
|
||||
|
||||
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)
|
||||
return run(c.loadCoreFn, func(ctx context.Context, core *CLICore) error {
|
||||
return core.Update(ctx, c.ref, c.oldSHA, c.newSHA)
|
||||
})
|
||||
}
|
||||
|
||||
type postReceiveCommand struct{}
|
||||
type postReceiveCommand struct {
|
||||
loadCoreFn LoadCLICoreFunc
|
||||
}
|
||||
|
||||
func (c *postReceiveCommand) run(*kingpin.ParseContext) error {
|
||||
return run(func(ctx context.Context, hook *GitHook) error {
|
||||
return hook.PostReceive(ctx)
|
||||
return run(c.loadCoreFn, func(ctx context.Context, core *CLICore) error {
|
||||
return core.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()
|
||||
func run(loadCoreFn LoadCLICoreFunc, fn func(ctx context.Context, core *CLICore) error) error {
|
||||
core, err := loadCoreFn()
|
||||
if errors.Is(err, ErrDisabled) {
|
||||
// complete operation successfully without making any calls to the server.
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrHookDisabled) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 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)
|
||||
ctx, cancel := context.WithTimeout(ctx, core.executionTimeout)
|
||||
defer cancel()
|
||||
|
||||
return fn(ctx, hook)
|
||||
return fn(ctx, core)
|
||||
}
|
|
@ -12,17 +12,26 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = fmt.Errorf("not found")
|
||||
)
|
||||
|
||||
// client is the client used to call the githooks api of gitness api server.
|
||||
type client struct {
|
||||
// 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.
|
||||
|
@ -33,27 +42,35 @@ type client struct {
|
|||
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 *types.PreReceiveInput) (*types.ServerHookOutput, error) {
|
||||
return c.githook(ctx, "pre-receive", in)
|
||||
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 *types.UpdateInput) (*types.ServerHookOutput, error) {
|
||||
return c.githook(ctx, "update", in)
|
||||
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 *types.PostReceiveInput) (*types.ServerHookOutput, error) {
|
||||
return c.githook(ctx, "post-receive", in)
|
||||
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{}) (*types.ServerHookOutput, error) {
|
||||
uri := fmt.Sprintf("%s/v1/internal/git-hooks/%s", c.baseURL, githookType)
|
||||
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)
|
||||
|
@ -64,7 +81,6 @@ func (c *client) githook(ctx context.Context, githookType string, in interface{}
|
|||
return nil, fmt.Errorf("failed to create new http request: %w", err)
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("User-Agent", fmt.Sprintf("Gitness/%s", version.Version)) //TODO: change once it's separate CLI.
|
||||
|
||||
// prepare request if configured
|
||||
if c.requestPreparation != nil {
|
||||
|
@ -84,7 +100,7 @@ func (c *client) githook(ctx context.Context, githookType string, in interface{}
|
|||
return nil, fmt.Errorf("request execution failed: %w", err)
|
||||
}
|
||||
|
||||
return unmarshalResponse[types.ServerHookOutput](resp)
|
||||
return unmarshalResponse[Output](resp)
|
||||
}
|
||||
|
||||
// unmarshalResponse reads the response body and if there are no errors marshall's it into
|
|
@ -0,0 +1,132 @@
|
|||
// 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"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
|
||||
executionTimeout time.Duration
|
||||
}
|
||||
|
||||
// NewCLICore returns a new CLICore using the provided client and execution timeout.
|
||||
func NewCLICore(client *Client, executionTimeout time.Duration) *CLICore {
|
||||
return &CLICore{
|
||||
client: client,
|
||||
executionTimeout: executionTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// PreReceive executes the pre-receive git hook.
|
||||
func (c *CLICore) 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 := &PreReceiveInput{
|
||||
RefUpdates: refUpdates,
|
||||
}
|
||||
|
||||
out, err := c.client.PreReceive(ctx, in)
|
||||
|
||||
return handleServerHookOutput(out, err)
|
||||
}
|
||||
|
||||
// Update executes the update git hook.
|
||||
func (c *CLICore) Update(ctx context.Context, ref string, oldSHA string, newSHA string) error {
|
||||
in := &UpdateInput{
|
||||
RefUpdate: 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 *CLICore) 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 := &PostReceiveInput{
|
||||
RefUpdates: refUpdates,
|
||||
}
|
||||
|
||||
out, err := c.client.PostReceive(ctx, in)
|
||||
|
||||
return handleServerHookOutput(out, err)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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() ([]ReferenceUpdate, error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
updatedRefs := []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, ReferenceUpdate{
|
||||
Old: splitGitHookData[0],
|
||||
New: splitGitHookData[1],
|
||||
Ref: splitGitHookData[2],
|
||||
})
|
||||
}
|
||||
|
||||
return updatedRefs, nil
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
// 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 (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
// envNamePayload defines the environment variable name used to send the payload to githook binary.
|
||||
envNamePayload = "GIT_HOOK_PAYLOAD"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrEnvVarNotFound is an error that is returned in case the environment variable isn't found.
|
||||
ErrEnvVarNotFound = errors.New("environment variable not found")
|
||||
)
|
||||
|
||||
// GenerateEnvironmentVariables generates the environment variables that should be used when calling git
|
||||
// to ensure the payload will be available to the githook cli.
|
||||
func GenerateEnvironmentVariables(payload any) (map[string]string, error) {
|
||||
// serialize the payload
|
||||
payloadBuff := &bytes.Buffer{}
|
||||
encoder := gob.NewEncoder(payloadBuff)
|
||||
if err := encoder.Encode(payload); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode payload: %w", err)
|
||||
}
|
||||
|
||||
// send it as base64 to avoid issues with any problematic characters
|
||||
// NOTE: this will blow up the payload by ~33%, though it's not expected to be too big.
|
||||
// On the other hand, we save a lot of size by only needing one environment variable name.
|
||||
payloadBase64 := base64.StdEncoding.EncodeToString(payloadBuff.Bytes())
|
||||
|
||||
return map[string]string{
|
||||
envNamePayload: payloadBase64,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadPayloadFromMap loads the payload from a map containing environment variables in a map format.
|
||||
func LoadPayloadFromMap[T any](envVars map[string]string) (T, error) {
|
||||
var payload T
|
||||
|
||||
// retrieve payload from environment variables
|
||||
payloadBase64, ok := envVars[envNamePayload]
|
||||
if !ok {
|
||||
return payload, ErrEnvVarNotFound
|
||||
}
|
||||
|
||||
return decodePayload[T](payloadBase64)
|
||||
}
|
||||
|
||||
// LoadPayloadFromEnvironment loads the githook payload from the environment.
|
||||
func LoadPayloadFromEnvironment[T any]() (T, error) {
|
||||
var payload T
|
||||
|
||||
// retrieve payload from environment variables
|
||||
payloadBase64, err := getEnvironmentVariable(envNamePayload)
|
||||
if err != nil {
|
||||
return payload, fmt.Errorf("failed to load payload from environment variables: %w", err)
|
||||
}
|
||||
|
||||
return decodePayload[T](payloadBase64)
|
||||
}
|
||||
|
||||
func decodePayload[T any](encodedPayload string) (T, error) {
|
||||
var payload T
|
||||
// decode base64
|
||||
payloadBytes, err := base64.StdEncoding.DecodeString(encodedPayload)
|
||||
if err != nil {
|
||||
return payload, fmt.Errorf("failed to base64 decode payload: %w", err)
|
||||
}
|
||||
|
||||
// deserialize the payload
|
||||
decoder := gob.NewDecoder(bytes.NewReader(payloadBytes))
|
||||
err = decoder.Decode(&payload)
|
||||
if err != nil {
|
||||
return payload, fmt.Errorf("failed to deserialize payload: %w", err)
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func getEnvironmentVariable(name string) (string, error) {
|
||||
val, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
return "", ErrEnvVarNotFound
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
return "", fmt.Errorf("'%s' found in env but it's empty", name)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// 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
|
||||
|
||||
// Output represents the output of server hook api calls.
|
||||
// TODO: support non-error messages (once we need it).
|
||||
type Output 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"`
|
||||
}
|
||||
|
||||
// PostReceiveInput represents the input of the post-receive git hook.
|
||||
type PostReceiveInput struct {
|
||||
// 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 {
|
||||
// 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 {
|
||||
// RefUpdate contains information about the reference that is being updated.
|
||||
RefUpdate ReferenceUpdate `json:"ref_update"`
|
||||
}
|
|
@ -9,6 +9,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/githook"
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
events "github.com/harness/gitness/internal/events/git"
|
||||
"github.com/harness/gitness/types"
|
||||
|
@ -26,36 +27,47 @@ const (
|
|||
func (c *Controller) PostReceive(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
in *types.PostReceiveInput,
|
||||
) (*types.ServerHookOutput, error) {
|
||||
repoID int64,
|
||||
principalID int64,
|
||||
in *githook.PostReceiveInput,
|
||||
) (*githook.Output, error) {
|
||||
if in == nil {
|
||||
return nil, fmt.Errorf("input is nil")
|
||||
}
|
||||
|
||||
// report ref events (best effort)
|
||||
c.reportReferenceEvents(ctx, in)
|
||||
c.reportReferenceEvents(ctx, repoID, principalID, in)
|
||||
|
||||
return &types.ServerHookOutput{}, nil
|
||||
return &githook.Output{}, 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 *types.PostReceiveInput) {
|
||||
func (c *Controller) reportReferenceEvents(
|
||||
ctx context.Context,
|
||||
repoID int64,
|
||||
principalID int64,
|
||||
in *githook.PostReceiveInput,
|
||||
) {
|
||||
for _, refUpdate := range in.RefUpdates {
|
||||
switch {
|
||||
case strings.HasPrefix(refUpdate.Ref, gitReferenceNamePrefixBranch):
|
||||
c.reportBranchEvent(ctx, in.PrincipalID, in.RepoID, refUpdate)
|
||||
c.reportBranchEvent(ctx, repoID, principalID, refUpdate)
|
||||
case strings.HasPrefix(refUpdate.Ref, gitReferenceNamePrefixTag):
|
||||
c.reportTagEvent(ctx, in.PrincipalID, in.RepoID, refUpdate)
|
||||
c.reportTagEvent(ctx, repoID, principalID, refUpdate)
|
||||
default:
|
||||
// Ignore any other references in post-receive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) reportBranchEvent(ctx context.Context,
|
||||
principalID int64, repoID int64, branchUpdate types.ReferenceUpdate) {
|
||||
func (c *Controller) reportBranchEvent(
|
||||
ctx context.Context,
|
||||
repoID int64,
|
||||
principalID int64,
|
||||
branchUpdate githook.ReferenceUpdate,
|
||||
) {
|
||||
switch {
|
||||
case branchUpdate.Old == types.NilSHA:
|
||||
c.gitReporter.BranchCreated(ctx, &events.BranchCreatedPayload{
|
||||
|
@ -83,8 +95,12 @@ func (c *Controller) reportBranchEvent(ctx context.Context,
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Controller) reportTagEvent(ctx context.Context,
|
||||
principalID int64, repoID int64, tagUpdate types.ReferenceUpdate) {
|
||||
func (c *Controller) reportTagEvent(
|
||||
ctx context.Context,
|
||||
repoID int64,
|
||||
principalID int64,
|
||||
tagUpdate githook.ReferenceUpdate,
|
||||
) {
|
||||
switch {
|
||||
case tagUpdate.Old == types.NilSHA:
|
||||
c.gitReporter.TagCreated(ctx, &events.TagCreatedPayload{
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/githook"
|
||||
"github.com/harness/gitness/internal/api/usererror"
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/types"
|
||||
|
@ -20,13 +21,15 @@ import (
|
|||
func (c *Controller) PreReceive(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
in *types.PreReceiveInput,
|
||||
) (*types.ServerHookOutput, error) {
|
||||
repoID int64,
|
||||
principalID int64,
|
||||
in *githook.PreReceiveInput,
|
||||
) (*githook.Output, error) {
|
||||
if in == nil {
|
||||
return nil, fmt.Errorf("input is nil")
|
||||
}
|
||||
|
||||
repo, err := c.getRepoCheckAccess(ctx, session, in.RepoID, enum.PermissionRepoEdit)
|
||||
repo, err := c.getRepoCheckAccess(ctx, session, repoID, enum.PermissionRepoEdit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -38,16 +41,16 @@ func (c *Controller) PreReceive(
|
|||
|
||||
// TODO: Branch Protection, Block non-brach/tag refs (?), ...
|
||||
|
||||
return &types.ServerHookOutput{}, nil
|
||||
return &githook.Output{}, nil
|
||||
}
|
||||
|
||||
func (c *Controller) blockDefaultBranchDeletion(repo *types.Repository,
|
||||
in *types.PreReceiveInput) *types.ServerHookOutput {
|
||||
in *githook.PreReceiveInput) *githook.Output {
|
||||
repoDefaultBranchRef := gitReferenceNamePrefixBranch + repo.DefaultBranch
|
||||
|
||||
for _, refUpdate := range in.RefUpdates {
|
||||
if refUpdate.New == types.NilSHA && refUpdate.Ref == repoDefaultBranchRef {
|
||||
return &types.ServerHookOutput{
|
||||
return &githook.Output{
|
||||
Error: ptr.String(usererror.ErrDefaultBranchCantBeDeleted.Error()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,21 +8,23 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/githook"
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
// Update executes the update hook for a git repository.
|
||||
func (c *Controller) Update(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
in *types.UpdateInput,
|
||||
) (*types.ServerHookOutput, error) {
|
||||
repoID int64,
|
||||
principalID int64,
|
||||
in *githook.UpdateInput,
|
||||
) (*githook.Output, 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 &types.ServerHookOutput{}, nil
|
||||
return &githook.Output{}, nil
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/gitrpc"
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/internal/auth/authz"
|
||||
"github.com/harness/gitness/internal/githook"
|
||||
|
@ -19,7 +18,6 @@ import (
|
|||
"github.com/harness/gitness/types/check"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
|
@ -65,19 +63,14 @@ func NewController(
|
|||
// IMPORTANT: session & repo are assumed to be not nil!
|
||||
func CreateRPCWriteParams(ctx context.Context, urlProvider *url.Provider,
|
||||
session *auth.Session, repo *types.Repository) (gitrpc.WriteParams, error) {
|
||||
requestID, ok := request.RequestIDFrom(ctx)
|
||||
if !ok {
|
||||
// best effort retrieving of requestID - log in case we can't find it but don't fail operation.
|
||||
log.Ctx(ctx).Warn().Msg("operation doesn't have a requestID in the context.")
|
||||
}
|
||||
|
||||
// generate envars (add everything githook CLI needs for execution)
|
||||
envVars, err := githook.GenerateEnvironmentVariables(&githook.Payload{
|
||||
APIBaseURL: urlProvider.GetAPIBaseURLInternal(),
|
||||
RepoID: repo.ID,
|
||||
PrincipalID: session.Principal.ID,
|
||||
RequestID: requestID,
|
||||
})
|
||||
envVars, err := githook.GenerateEnvironmentVariables(
|
||||
ctx,
|
||||
urlProvider.GetAPIBaseURLInternal(),
|
||||
repo.ID,
|
||||
session.Principal.ID,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return gitrpc.WriteParams{}, fmt.Errorf("failed to generate git hook environment variables: %w", err)
|
||||
}
|
||||
|
|
|
@ -201,10 +201,13 @@ func (c *Controller) createGitRPCRepository(ctx context.Context, session *auth.S
|
|||
}
|
||||
|
||||
// generate envars (add everything githook CLI needs for execution)
|
||||
envVars, err := githook.GenerateEnvironmentVariables(&githook.Payload{
|
||||
APIBaseURL: c.urlProvider.GetAPIBaseURLInternal(),
|
||||
Disabled: true,
|
||||
})
|
||||
envVars, err := githook.GenerateEnvironmentVariables(
|
||||
ctx,
|
||||
c.urlProvider.GetAPIBaseURLInternal(),
|
||||
0,
|
||||
session.Principal.ID,
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate git hook environment variables: %w", err)
|
||||
}
|
||||
|
|
|
@ -9,13 +9,10 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/gitrpc"
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/internal/githook"
|
||||
"github.com/harness/gitness/internal/url"
|
||||
"github.com/harness/gitness/types"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// TODO: this file should be in gitrpc package and should accept
|
||||
|
@ -27,19 +24,14 @@ import (
|
|||
// function will be best fit.
|
||||
func CreateRPCWriteParams(ctx context.Context, urlProvider *url.Provider,
|
||||
session *auth.Session, repo *types.Repository) (gitrpc.WriteParams, error) {
|
||||
requestID, ok := request.RequestIDFrom(ctx)
|
||||
if !ok {
|
||||
// best effort retrieving of requestID - log in case we can't find it but don't fail operation.
|
||||
log.Ctx(ctx).Warn().Msg("operation doesn't have a requestID in the context.")
|
||||
}
|
||||
|
||||
// generate envars (add everything githook CLI needs for execution)
|
||||
envVars, err := githook.GenerateEnvironmentVariables(&githook.Payload{
|
||||
APIBaseURL: urlProvider.GetAPIBaseURLInternal(),
|
||||
RepoID: repo.ID,
|
||||
PrincipalID: session.Principal.ID,
|
||||
RequestID: requestID,
|
||||
})
|
||||
envVars, err := githook.GenerateEnvironmentVariables(
|
||||
ctx,
|
||||
urlProvider.GetAPIBaseURLInternal(),
|
||||
repo.ID,
|
||||
session.Principal.ID,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return gitrpc.WriteParams{}, fmt.Errorf("failed to generate git hook environment variables: %w", err)
|
||||
}
|
||||
|
|
|
@ -8,26 +8,38 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/internal/api/controller/githook"
|
||||
"github.com/harness/gitness/githook"
|
||||
controllergithook "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.
|
||||
func HandlePostReceive(githookCtrl *githook.Controller) http.HandlerFunc {
|
||||
func HandlePostReceive(githookCtrl *controllergithook.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
in := new(types.PostReceiveInput)
|
||||
err := json.NewDecoder(r.Body).Decode(in)
|
||||
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.PostReceiveInput)
|
||||
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, in)
|
||||
out, err := githookCtrl.PostReceive(ctx, session, repoID, principalID, in)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
|
|
|
@ -8,26 +8,38 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/internal/api/controller/githook"
|
||||
"github.com/harness/gitness/githook"
|
||||
controllergithook "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.
|
||||
func HandlePreReceive(githookCtrl *githook.Controller) http.HandlerFunc {
|
||||
func HandlePreReceive(githookCtrl *controllergithook.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
in := new(types.PreReceiveInput)
|
||||
err := json.NewDecoder(r.Body).Decode(in)
|
||||
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.PreReceiveInput)
|
||||
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, in)
|
||||
out, err := githookCtrl.PreReceive(ctx, session, repoID, principalID, in)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
|
|
|
@ -8,26 +8,38 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/internal/api/controller/githook"
|
||||
"github.com/harness/gitness/githook"
|
||||
githookcontroller "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.
|
||||
func HandleUpdate(githookCtrl *githook.Controller) http.HandlerFunc {
|
||||
func HandleUpdate(githookCtrl *githookcontroller.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
in := new(types.UpdateInput)
|
||||
err := json.NewDecoder(r.Body).Decode(in)
|
||||
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)
|
||||
if err != nil {
|
||||
render.BadRequestf(w, "Invalid Request Body: %s.", err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := githookCtrl.Update(ctx, session, in)
|
||||
out, err := githookCtrl.Update(ctx, session, repoID, principalID, in)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/harness/gitness/internal/api/controller/user"
|
||||
"github.com/harness/gitness/internal/api/usererror"
|
||||
"github.com/harness/gitness/types"
|
||||
|
||||
"github.com/swaggest/openapi-go/openapi3"
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
// 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 request
|
||||
|
||||
const (
|
||||
// TODO: have shared constants across all services?
|
||||
HeaderRequestID = "X-Request-Id"
|
||||
HeaderUserAgent = "User-Agent"
|
||||
)
|
|
@ -15,12 +15,19 @@ const (
|
|||
PathParamPrincipalUID = "principal_uid"
|
||||
PathParamUserUID = "user_uid"
|
||||
PathParamServiceAccountUID = "sa_uid"
|
||||
|
||||
QueryParamPrincipalID = "principal_id"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ func parsePullReqStates(r *http.Request) []enum.PullReqState {
|
|||
|
||||
// ParsePullReqFilter extracts the pull request query parameter from the url.
|
||||
func ParsePullReqFilter(r *http.Request) (*types.PullReqFilter, error) {
|
||||
createdBy, err := QueryParamAsID(r, QueryParamCreatedBy)
|
||||
createdBy, err := QueryParamAsPositiveInt64(r, QueryParamCreatedBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
const (
|
||||
PathParamRepoRef = "repo_ref"
|
||||
QueryParamRepoID = "repo_id"
|
||||
)
|
||||
|
||||
func GetRepoRefFromPath(r *http.Request) (string, error) {
|
||||
|
@ -26,6 +27,11 @@ 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.ParseRepoAtrr(
|
||||
|
|
|
@ -91,9 +91,9 @@ func QueryParamAsID(r *http.Request, paramName string) (int64, error) {
|
|||
|
||||
// QueryParamAsPositiveInt64 extracts an integer parameter from the request query.
|
||||
func QueryParamAsPositiveInt64(r *http.Request, paramName string) (int64, error) {
|
||||
value, ok := QueryParam(r, paramName)
|
||||
if !ok {
|
||||
return 0, nil
|
||||
value, err := QueryParamOrError(r, paramName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
valueInt, err := strconv.ParseInt(value, 10, 64)
|
||||
|
|
|
@ -1,118 +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 githook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
// envPayload defines the environment variable name used to send the payload to githook binary.
|
||||
// NOTE: Since the variable is not meant for gitness itself, don't prefix with 'GITNESS'.
|
||||
envPayload = "GIT_HOOK_GITNESS_PAYLOAD"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrHookDisabled = errors.New("hook disabled")
|
||||
)
|
||||
|
||||
// Payload defines the Payload the githook binary is initiated with when executing the git hooks.
|
||||
type Payload struct {
|
||||
APIBaseURL string
|
||||
RepoID int64
|
||||
PrincipalID int64
|
||||
RequestID string
|
||||
Disabled bool // this will stop processing server hooks
|
||||
}
|
||||
|
||||
// GenerateEnvironmentVariables generates the environment variables that are sent to the githook binary.
|
||||
// NOTE: for now we use a single environment variable to reduce the overal number of environment variables.
|
||||
func GenerateEnvironmentVariables(payload *Payload) (map[string]string, error) {
|
||||
// serialize the payload
|
||||
payloadBuff := &bytes.Buffer{}
|
||||
encoder := gob.NewEncoder(payloadBuff)
|
||||
if err := encoder.Encode(payload); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode payload: %w", err)
|
||||
}
|
||||
|
||||
// send it as base64 to avoid issues with any problematic characters
|
||||
// NOTE: this will blow up the payload by ~33%, though it's not expected to be too big.
|
||||
// On the other hand, we save a lot of size by only needing one environment variable name.
|
||||
payloadBase64 := base64.StdEncoding.EncodeToString(payloadBuff.Bytes())
|
||||
|
||||
return map[string]string{
|
||||
envPayload: payloadBase64,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// loadPayloadFromEnvironment loads the githook payload from the environment.
|
||||
func loadPayloadFromEnvironment() (*Payload, error) {
|
||||
// retrieve payload from environment variables
|
||||
payloadBase64, err := getEnvironmentVariable(envPayload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load payload from environment variables: %w", err)
|
||||
}
|
||||
|
||||
// decode base64
|
||||
payloadBytes, err := base64.StdEncoding.DecodeString(payloadBase64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to base64 decode payload: %w", err)
|
||||
}
|
||||
|
||||
// deserialize the payload
|
||||
var payload Payload
|
||||
decoder := gob.NewDecoder(bytes.NewReader(payloadBytes))
|
||||
err = decoder.Decode(&payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to deserialize payload: %w", err)
|
||||
}
|
||||
|
||||
// ensure payload is valid
|
||||
err = validatePayload(&payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("payload contains invalid data: %w", err)
|
||||
}
|
||||
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
// validatePayload performs a BASIC validation of the payload.
|
||||
func validatePayload(payload *Payload) error {
|
||||
if payload == nil {
|
||||
return errors.New("payload is empty")
|
||||
}
|
||||
if payload.Disabled {
|
||||
return ErrHookDisabled
|
||||
}
|
||||
if payload.APIBaseURL == "" {
|
||||
return errors.New("payload doesn't contain a base url")
|
||||
}
|
||||
if payload.PrincipalID <= 0 {
|
||||
return errors.New("payload doesn't contain a principal id")
|
||||
}
|
||||
if payload.RepoID <= 0 {
|
||||
return errors.New("payload doesn't contain a repo id")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEnvironmentVariable(name string) (string, error) {
|
||||
val, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("'%s' not found in env", name)
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
return "", fmt.Errorf("'%s' found in env but it's empty", name)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
|
@ -5,154 +5,95 @@
|
|||
package githook
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/githook"
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/version"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// GitHook represents the githook implementation.
|
||||
type GitHook struct {
|
||||
payload *Payload
|
||||
client *client
|
||||
}
|
||||
var (
|
||||
// ExecutionTimeout is the timeout used for githook CLI runs.
|
||||
ExecutionTimeout = 3 * time.Minute
|
||||
)
|
||||
|
||||
// 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)
|
||||
// GenerateEnvironmentVariables generates the required environment variables for a payload
|
||||
// constructed from the provided parameters.
|
||||
func GenerateEnvironmentVariables(
|
||||
ctx context.Context,
|
||||
apiBaseURL string,
|
||||
repoID int64,
|
||||
principalID int64,
|
||||
disabled bool,
|
||||
) (map[string]string, error) {
|
||||
// best effort retrieving of requestID - log in case we can't find it but don't fail operation.
|
||||
requestID, ok := request.RequestIDFrom(ctx)
|
||||
if !ok {
|
||||
log.Ctx(ctx).Warn().Msg("operation doesn't have a requestID in the context - generate githook payload without")
|
||||
}
|
||||
|
||||
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)
|
||||
// generate githook base url
|
||||
baseURL := strings.TrimLeft(apiBaseURL, "/") + "/v1/internal/git-hooks"
|
||||
|
||||
payload := &types.GithookPayload{
|
||||
BaseURL: baseURL,
|
||||
RepoID: repoID,
|
||||
PrincipalID: principalID,
|
||||
RequestID: requestID,
|
||||
Disabled: disabled,
|
||||
}
|
||||
|
||||
if err := payload.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("generated payload is invalid: %w", err)
|
||||
}
|
||||
|
||||
return githook.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]()
|
||||
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
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
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
|
||||
},
|
||||
},
|
||||
}, 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 occurred 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
|
||||
),
|
||||
ExecutionTimeout,
|
||||
), nil
|
||||
}
|
||||
|
|
|
@ -8,8 +8,9 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/githook"
|
||||
"github.com/harness/gitness/internal/api/controller/check"
|
||||
"github.com/harness/gitness/internal/api/controller/githook"
|
||||
controllergithook "github.com/harness/gitness/internal/api/controller/githook"
|
||||
"github.com/harness/gitness/internal/api/controller/principal"
|
||||
"github.com/harness/gitness/internal/api/controller/pullreq"
|
||||
"github.com/harness/gitness/internal/api/controller/repo"
|
||||
|
@ -63,7 +64,7 @@ func NewAPIHandler(
|
|||
spaceCtrl *space.Controller,
|
||||
pullreqCtrl *pullreq.Controller,
|
||||
webhookCtrl *webhook.Controller,
|
||||
githookCtrl *githook.Controller,
|
||||
githookCtrl *controllergithook.Controller,
|
||||
saCtrl *serviceaccount.Controller,
|
||||
userCtrl *user.Controller,
|
||||
principalCtrl principal.Controller,
|
||||
|
@ -115,7 +116,7 @@ func setupRoutesV1(r chi.Router,
|
|||
spaceCtrl *space.Controller,
|
||||
pullreqCtrl *pullreq.Controller,
|
||||
webhookCtrl *webhook.Controller,
|
||||
githookCtrl *githook.Controller,
|
||||
githookCtrl *controllergithook.Controller,
|
||||
saCtrl *serviceaccount.Controller,
|
||||
userCtrl *user.Controller,
|
||||
principalCtrl principal.Controller,
|
||||
|
@ -257,17 +258,17 @@ func setupRepos(r chi.Router,
|
|||
})
|
||||
}
|
||||
|
||||
func setupInternal(r chi.Router, githookCtrl *githook.Controller) {
|
||||
func setupInternal(r chi.Router, githookCtrl *controllergithook.Controller) {
|
||||
r.Route("/internal", func(r chi.Router) {
|
||||
SetupGitHooks(r, githookCtrl)
|
||||
})
|
||||
}
|
||||
|
||||
func SetupGitHooks(r chi.Router, githookCtrl *githook.Controller) {
|
||||
func SetupGitHooks(r chi.Router, githookCtrl *controllergithook.Controller) {
|
||||
r.Route("/git-hooks", func(r chi.Router) {
|
||||
r.Post("/pre-receive", handlergithook.HandlePreReceive(githookCtrl))
|
||||
r.Post("/update", handlergithook.HandleUpdate(githookCtrl))
|
||||
r.Post("/post-receive", handlergithook.HandlePostReceive(githookCtrl))
|
||||
r.Post("/"+githook.HTTPRequestPathPreReceive, handlergithook.HandlePreReceive(githookCtrl))
|
||||
r.Post("/"+githook.HTTPRequestPathUpdate, handlergithook.HandleUpdate(githookCtrl))
|
||||
r.Post("/"+githook.HTTPRequestPathPostReceive, handlergithook.HandlePostReceive(githookCtrl))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
|
||||
"github.com/harness/gitness/events"
|
||||
"github.com/harness/gitness/gitrpc"
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
"github.com/harness/gitness/internal/bootstrap"
|
||||
gitevents "github.com/harness/gitness/internal/events/git"
|
||||
pullreqevents "github.com/harness/gitness/internal/events/pullreq"
|
||||
|
@ -25,7 +24,6 @@ import (
|
|||
"github.com/harness/gitness/types"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
|
@ -217,24 +215,22 @@ func New(ctx context.Context,
|
|||
}
|
||||
|
||||
// createSystemRPCWriteParams creates base write parameters for gitrpc write operations.
|
||||
func createSystemRPCWriteParams(ctx context.Context, urlProvider *url.Provider,
|
||||
repoID int64, repoGITUID string) (gitrpc.WriteParams, error) {
|
||||
requestID, ok := request.RequestIDFrom(ctx)
|
||||
if !ok {
|
||||
// best effort retrieving of requestID - log in case we can't find it but don't fail operation.
|
||||
log.Ctx(ctx).Warn().Msg("operation doesn't have a requestID in the context.")
|
||||
}
|
||||
|
||||
func createSystemRPCWriteParams(
|
||||
ctx context.Context,
|
||||
urlProvider *url.Provider,
|
||||
repoID int64,
|
||||
repoGITUID string,
|
||||
) (gitrpc.WriteParams, error) {
|
||||
principal := bootstrap.NewSystemServiceSession().Principal
|
||||
|
||||
// generate envars (add everything githook CLI needs for execution)
|
||||
envVars, err := githook.GenerateEnvironmentVariables(&githook.Payload{
|
||||
APIBaseURL: urlProvider.GetAPIBaseURLInternal(),
|
||||
RepoID: repoID,
|
||||
PrincipalID: principal.ID,
|
||||
RequestID: requestID,
|
||||
Disabled: false,
|
||||
})
|
||||
envVars, err := githook.GenerateEnvironmentVariables(
|
||||
ctx,
|
||||
urlProvider.GetAPIBaseURLInternal(),
|
||||
repoID,
|
||||
principal.ID,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return gitrpc.WriteParams{}, fmt.Errorf("failed to generate git hook environment variables: %w", err)
|
||||
}
|
||||
|
|
|
@ -4,46 +4,38 @@
|
|||
|
||||
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"`
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// GithookPayload defines the GithookPayload the githook binary is initiated with when executing the git hooks.
|
||||
type GithookPayload struct {
|
||||
BaseURL string
|
||||
RepoID int64
|
||||
PrincipalID int64
|
||||
RequestID string
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
func (p *GithookPayload) Validate() error {
|
||||
if p == nil {
|
||||
return errors.New("payload is empty")
|
||||
}
|
||||
|
||||
// BaseInput contains the base input for any githook api call.
|
||||
type BaseInput struct {
|
||||
RepoID int64 `json:"repo_id"`
|
||||
PrincipalID int64 `json:"principal_id"`
|
||||
}
|
||||
// skip further validation if githook is disabled
|
||||
if p.Disabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
// 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"`
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue