mirror of https://github.com/harness/drone.git
302 lines
7.5 KiB
Go
302 lines
7.5 KiB
Go
// Copyright 2023 Harness, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package hook
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/harness/gitness/errors"
|
|
"github.com/harness/gitness/git/command"
|
|
"github.com/harness/gitness/git/sha"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// CreateRefUpdater creates new RefUpdater object using the provided git hook ClientFactory.
|
|
func CreateRefUpdater(
|
|
hookClientFactory ClientFactory,
|
|
envVars map[string]string,
|
|
repoPath string,
|
|
ref string,
|
|
) (*RefUpdater, error) {
|
|
if repoPath == "" {
|
|
return nil, errors.Internal(nil, "repo path can't be empty")
|
|
}
|
|
|
|
client, err := hookClientFactory.NewClient(envVars)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create hook.Client: %w", err)
|
|
}
|
|
|
|
return &RefUpdater{
|
|
state: stateInitOld,
|
|
hookClient: client,
|
|
envVars: envVars,
|
|
repoPath: repoPath,
|
|
ref: ref,
|
|
oldValue: sha.None,
|
|
newValue: sha.None,
|
|
}, nil
|
|
}
|
|
|
|
// RefUpdater is an entity that is responsible for update of a reference.
|
|
// It will call pre-receive hook prior to the update and post-receive hook after the update.
|
|
// It has a state machine to guarantee that methods are called in the correct order (Init, Pre, Update, Post).
|
|
type RefUpdater struct {
|
|
state refUpdaterState
|
|
hookClient Client
|
|
envVars map[string]string
|
|
repoPath string
|
|
ref string
|
|
oldValue sha.SHA
|
|
newValue sha.SHA
|
|
}
|
|
|
|
// refUpdaterState represents state of the ref updater internal state machine.
|
|
type refUpdaterState byte
|
|
|
|
func (t refUpdaterState) String() string {
|
|
switch t {
|
|
case stateInitOld:
|
|
return "INIT_OLD"
|
|
case stateInitNew:
|
|
return "INIT_NEW"
|
|
case statePre:
|
|
return "PRE"
|
|
case stateUpdate:
|
|
return "UPDATE"
|
|
case statePost:
|
|
return "POST"
|
|
case stateDone:
|
|
return "DONE"
|
|
}
|
|
return "INVALID"
|
|
}
|
|
|
|
const (
|
|
stateInitOld refUpdaterState = iota
|
|
stateInitNew
|
|
statePre
|
|
stateUpdate
|
|
statePost
|
|
stateDone
|
|
)
|
|
|
|
// Do runs full ref update by executing all methods in the correct order.
|
|
func (u *RefUpdater) Do(ctx context.Context, oldValue, newValue sha.SHA) error {
|
|
if err := u.Init(ctx, oldValue, newValue); err != nil {
|
|
return fmt.Errorf("init failed: %w", err)
|
|
}
|
|
|
|
if err := u.Pre(ctx); err != nil {
|
|
return fmt.Errorf("pre failed: %w", err)
|
|
}
|
|
|
|
if err := u.UpdateRef(ctx); err != nil {
|
|
return fmt.Errorf("update failed: %w", err)
|
|
}
|
|
|
|
if err := u.Post(ctx); err != nil {
|
|
return fmt.Errorf("post failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *RefUpdater) Init(ctx context.Context, oldValue, newValue sha.SHA) error {
|
|
if err := u.InitOld(ctx, oldValue); err != nil {
|
|
return fmt.Errorf("init old failed: %w", err)
|
|
}
|
|
if err := u.InitNew(ctx, newValue); err != nil {
|
|
return fmt.Errorf("init new failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *RefUpdater) InitOld(ctx context.Context, oldValue sha.SHA) error {
|
|
if u == nil {
|
|
return nil
|
|
}
|
|
|
|
if u.state != stateInitOld {
|
|
return fmt.Errorf("invalid operation order: init old requires state=%s, current state=%s",
|
|
stateInitOld, u.state)
|
|
}
|
|
|
|
if oldValue.IsEmpty() {
|
|
// if no old value was provided, use current value (as required for hooks)
|
|
val, err := u.getRef(ctx)
|
|
if errors.IsNotFound(err) { //nolint:gocritic
|
|
oldValue = sha.Nil
|
|
} else if err != nil {
|
|
return fmt.Errorf("failed to get current value of reference: %w", err)
|
|
} else {
|
|
oldValue = val
|
|
}
|
|
}
|
|
|
|
u.state = stateInitNew
|
|
u.oldValue = oldValue
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *RefUpdater) InitNew(_ context.Context, newValue sha.SHA) error {
|
|
if u == nil {
|
|
return nil
|
|
}
|
|
|
|
if u.state != stateInitNew {
|
|
return fmt.Errorf("invalid operation order: init new requires state=%s, current state=%s",
|
|
stateInitNew, u.state)
|
|
}
|
|
|
|
if newValue.IsEmpty() {
|
|
// don't break existing interface - user calls with empty value to delete the ref.
|
|
newValue = sha.Nil
|
|
}
|
|
|
|
u.state = statePre
|
|
u.newValue = newValue
|
|
|
|
return nil
|
|
}
|
|
|
|
// Pre runs the pre-receive git hook.
|
|
func (u *RefUpdater) Pre(ctx context.Context, alternateDirs ...string) error {
|
|
if u.state != statePre {
|
|
return fmt.Errorf("invalid operation order: pre-receive hook requires state=%s, current state=%s",
|
|
statePre, u.state)
|
|
}
|
|
|
|
// fail in case someone tries to delete a reference that doesn't exist.
|
|
if u.oldValue.IsEmpty() && u.newValue.IsNil() {
|
|
return errors.NotFound("reference %q not found", u.ref)
|
|
}
|
|
|
|
if u.oldValue.IsNil() && u.newValue.IsNil() {
|
|
return fmt.Errorf("provided values cannot be both empty")
|
|
}
|
|
|
|
out, err := u.hookClient.PreReceive(ctx, PreReceiveInput{
|
|
RefUpdates: []ReferenceUpdate{
|
|
{
|
|
Ref: u.ref,
|
|
Old: u.oldValue,
|
|
New: u.newValue,
|
|
},
|
|
},
|
|
Environment: Environment{
|
|
AlternateObjectDirs: alternateDirs,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("pre-receive call failed with: %w", err)
|
|
}
|
|
if out.Error != nil {
|
|
log.Ctx(ctx).Debug().
|
|
Str("err", *out.Error).
|
|
Msgf("Pre-receive blocked ref update\nMessages\n%v", out.Messages)
|
|
return errors.PreconditionFailed("pre-receive hook blocked reference update: %q", *out.Error)
|
|
}
|
|
|
|
u.state = stateUpdate
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateRef updates the git reference.
|
|
func (u *RefUpdater) UpdateRef(ctx context.Context) error {
|
|
if u.state != stateUpdate {
|
|
return fmt.Errorf("invalid operation order: ref update requires state=%s, current state=%s",
|
|
stateUpdate, u.state)
|
|
}
|
|
|
|
cmd := command.New("update-ref")
|
|
if u.newValue.IsNil() {
|
|
cmd.Add(command.WithFlag("-d", u.ref))
|
|
} else {
|
|
cmd.Add(command.WithArg(u.ref, u.newValue.String()))
|
|
}
|
|
|
|
cmd.Add(command.WithArg(u.oldValue.String()))
|
|
|
|
if err := cmd.Run(ctx, command.WithDir(u.repoPath)); err != nil {
|
|
msg := err.Error()
|
|
if strings.Contains(msg, "reference already exists") {
|
|
return errors.Conflict("reference already exists")
|
|
}
|
|
|
|
return fmt.Errorf("update of ref %q from %q to %q failed: %w", u.ref, u.oldValue, u.newValue, err)
|
|
}
|
|
|
|
u.state = statePost
|
|
|
|
return nil
|
|
}
|
|
|
|
// Post runs the pre-receive git hook.
|
|
func (u *RefUpdater) Post(ctx context.Context, alternateDirs ...string) error {
|
|
if u.state != statePost {
|
|
return fmt.Errorf("invalid operation order: post-receive hook requires state=%s, current state=%s",
|
|
statePost, u.state)
|
|
}
|
|
|
|
out, err := u.hookClient.PostReceive(ctx, PostReceiveInput{
|
|
RefUpdates: []ReferenceUpdate{
|
|
{
|
|
Ref: u.ref,
|
|
Old: u.oldValue,
|
|
New: u.newValue,
|
|
},
|
|
},
|
|
Environment: Environment{
|
|
AlternateObjectDirs: alternateDirs,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("post-receive call failed with: %w", err)
|
|
}
|
|
if out.Error != nil {
|
|
return fmt.Errorf("post-receive call returned error: %q", *out.Error)
|
|
}
|
|
|
|
u.state = stateDone
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *RefUpdater) getRef(ctx context.Context) (sha.SHA, error) {
|
|
cmd := command.New("show-ref",
|
|
command.WithFlag("--verify"),
|
|
command.WithFlag("-s"),
|
|
command.WithArg(u.ref),
|
|
)
|
|
output := &bytes.Buffer{}
|
|
err := cmd.Run(ctx, command.WithDir(u.repoPath), command.WithStdout(output))
|
|
if err != nil {
|
|
if command.AsError(err).IsExitCode(128) && strings.Contains(err.Error(), "not a valid ref") {
|
|
return sha.None, errors.NotFound("reference %q not found", u.ref)
|
|
}
|
|
return sha.None, err
|
|
}
|
|
|
|
return sha.New(output.String())
|
|
}
|