Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/references/deploy.yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Field |Type |Required |Description
`required_contexts` |*[]string* |`false` |This field allows you to specify a subset of contexts that must be success.
`payload` |*object* or *string* |`false` |This field is JSON payload with extra information about the deployment.
`production_environment` |*boolean* |`false` |This field specifies whether this runtime environment is production or not.
`review` |*[Review](#review)* |`false` |This field configures review.
`deployable_ref` |*string* |`false` |This field specifies which the ref(branch, SHA, tag) is deployable or not. It supports the regular expression, [re2](https://github.com/google/re2/wiki/Syntax) by Google, to match the ref.
`review` |*[Review](#review)* |`false` |This field configures review.

## Review

Expand Down
67 changes: 37 additions & 30 deletions internal/interactor/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,26 @@ import (
"go.uber.org/zap"
)

func (i *Interactor) IsApproved(ctx context.Context, d *ent.Deployment) bool {
rvs, _ := i.Store.ListReviews(ctx, d)

for _, r := range rvs {
if r.Status == review.StatusRejected {
return false
}
}

for _, r := range rvs {
if r.Status == review.StatusApproved {
return true
}
}

return false
}

func (i *Interactor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error) {
if locked, err := i.Store.HasLockOfRepoForEnv(ctx, r, d.Env); locked {
return nil, e.NewError(
e.ErrorCodeDeploymentLocked,
err,
)
} else if err != nil {
if ok, err := i.isDeployable(ctx, u, r, d, env); !ok {
return nil, err
}

Expand Down Expand Up @@ -101,24 +114,6 @@ func (i *Interactor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *en
return d, nil
}

func (i *Interactor) IsApproved(ctx context.Context, d *ent.Deployment) bool {
rvs, _ := i.ListReviews(ctx, d)

for _, r := range rvs {
if r.Status == review.StatusRejected {
return false
}
}

for _, r := range rvs {
if r.Status == review.StatusApproved {
return true
}
}

return false
}

// DeployToRemote create a new remote deployment after the deployment was approved.
func (i *Interactor) DeployToRemote(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error) {
if d.Status != deployment.StatusWaiting {
Expand All @@ -129,12 +124,7 @@ func (i *Interactor) DeployToRemote(ctx context.Context, u *ent.User, r *ent.Rep
)
}

if locked, err := i.Store.HasLockOfRepoForEnv(ctx, r, d.Env); locked {
return nil, e.NewError(
e.ErrorCodeDeploymentLocked,
err,
)
} else if err != nil {
if ok, err := i.isDeployable(ctx, u, r, d, env); !ok {
return nil, err
}

Expand Down Expand Up @@ -181,6 +171,23 @@ func (i *Interactor) createRemoteDeployment(ctx context.Context, u *ent.User, r
return i.SCM.CreateRemoteDeployment(ctx, u, r, d, env)
}

func (i *Interactor) isDeployable(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (bool, error) {
if ok, err := env.IsDeployableRef(d.Ref); err != nil {
return false, err
} else if !ok {
return false, e.NewErrorWithMessage(e.ErrorCodeUnprocessableEntity, "The ref is not matched with 'deployable_ref'.", nil)
}
Comment on lines +175 to +179
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate the ref is matched with deployable_ref or not.


// Check that the environment is locked.
if locked, err := i.Store.HasLockOfRepoForEnv(ctx, r, d.Env); locked {
return false, e.NewError(e.ErrorCodeDeploymentLocked, err)
} else if err != nil {
return false, e.NewError(e.ErrorCodeInternalError, err)
}

return true, nil
}

func (i *Interactor) runClosingInactiveDeployment(stop <-chan struct{}) {
ctx := context.Background()

Expand Down
77 changes: 75 additions & 2 deletions internal/interactor/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"reflect"
"testing"

"github.com/AlekSi/pointer"
"github.com/golang/mock/gomock"
"go.uber.org/zap"

Expand All @@ -24,9 +25,82 @@ func newMockInteractor(store Store, scm SCM) *Interactor {
}
}

func TestInteractor_IsApproved(t *testing.T) {
t.Run("Return false when a review is rejected.", func(t *testing.T) {
ctrl := gomock.NewController(t)
store := mock.NewMockStore(ctrl)
scm := mock.NewMockSCM(ctrl)

t.Log("Return various status reviews")
store.
EXPECT().
ListReviews(gomock.Any(), gomock.AssignableToTypeOf(&ent.Deployment{})).
Return([]*ent.Review{
{
Status: review.StatusPending,
},
{
Status: review.StatusRejected,
},
}, nil)

i := newMockInteractor(store, scm)

expected := false
if ret := i.IsApproved(context.Background(), &ent.Deployment{}); ret != expected {
t.Fatalf("IsApproved = %v, wanted %v", ret, expected)
}
})
}

func TestInteractor_Deploy(t *testing.T) {
ctx := gomock.Any()

t.Run("Return an error when the ref is not deployable", func(t *testing.T) {
input := struct {
d *ent.Deployment
e *vo.Env
}{
d: &ent.Deployment{
Type: deployment.TypeBranch,
Ref: "main",
Env: "production",
},
e: &vo.Env{
DeployableRef: pointer.ToString("releast-.*"),
},
}

ctrl := gomock.NewController(t)
store := mock.NewMockStore(ctrl)
scm := mock.NewMockSCM(ctrl)

i := newMockInteractor(store, scm)

_, err := i.Deploy(context.Background(), &ent.User{}, &ent.Repo{}, input.d, input.e)
if !e.HasErrorCode(err, e.ErrorCodeUnprocessableEntity) {
t.Fatalf("Deploy' error = %v, wanted ErrorCodeDeploymentLocked", err)
}
})

t.Run("Return an error when the environment is locked", func(t *testing.T) {
ctrl := gomock.NewController(t)
store := mock.NewMockStore(ctrl)
scm := mock.NewMockSCM(ctrl)

store.
EXPECT().
HasLockOfRepoForEnv(ctx, gomock.AssignableToTypeOf(&ent.Repo{}), "").
Return(true, nil)

i := newMockInteractor(store, scm)

_, err := i.Deploy(context.Background(), &ent.User{}, &ent.Repo{}, &ent.Deployment{}, &vo.Env{})
if !e.HasErrorCode(err, e.ErrorCodeDeploymentLocked) {
t.Fatalf("Deploy' error = %v, wanted ErrorCodeDeploymentLocked", err)
}
})

t.Run("Return a new deployment.", func(t *testing.T) {
input := struct {
d *ent.Deployment
Expand Down Expand Up @@ -89,8 +163,7 @@ func TestInteractor_Deploy(t *testing.T) {

d, err := i.Deploy(context.Background(), &ent.User{}, &ent.Repo{}, input.d, input.e)
if err != nil {
t.Errorf("Deploy returns a error: %s", err)
t.FailNow()
t.Fatalf("Deploy returns a error: %s", err)
}

expected := &ent.Deployment{
Expand Down
4 changes: 3 additions & 1 deletion pkg/e/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import (
const (
// ErrorCodeConfigParseError is that an error occurs when it parse the file.
ErrorCodeConfigParseError ErrorCode = "config_parse_error"
// ErrorCodeConfigRegexpError is the regexp(re2) is invalid.
ErrorCodeConfigRegexpError ErrorCode = "config_regexp_error"
Comment on lines +11 to +12
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a new error config_regexp_error to describe an error from the regexp.MatchString


// ErrorCodeDeploymentConflict is the deployment number is conflicted.
ErrorCodeDeploymentConflict ErrorCode = "deployment_conflict"
// ErrorCodeDeploymentInvalid is the payload is invalid.
// ErrorCodeDeploymentInvalid is the payload is invalid when it posts a remote deployment.
ErrorCodeDeploymentInvalid ErrorCode = "deployment_invalid"
// ErrorCodeDeploymentLocked is when the environment is locked.
ErrorCodeDeploymentLocked ErrorCode = "deployment_locked"
Expand Down
2 changes: 2 additions & 0 deletions pkg/e/trans.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "net/http"

var messages = map[ErrorCode]string{
ErrorCodeConfigParseError: "The configuration is invalid.",
ErrorCodeConfigRegexpError: "The regexp is invalid.",
ErrorCodeDeploymentConflict: "The conflict occurs, please retry.",
ErrorCodeDeploymentInvalid: "The validation has failed.",
ErrorCodeDeploymentLocked: "The environment is locked.",
Expand All @@ -30,6 +31,7 @@ func GetMessage(code ErrorCode) string {

var httpCodes = map[ErrorCode]int{
ErrorCodeConfigParseError: http.StatusUnprocessableEntity,
ErrorCodeConfigRegexpError: http.StatusUnprocessableEntity,
ErrorCodeDeploymentConflict: http.StatusUnprocessableEntity,
ErrorCodeDeploymentInvalid: http.StatusUnprocessableEntity,
ErrorCodeDeploymentLocked: http.StatusUnprocessableEntity,
Expand Down
26 changes: 24 additions & 2 deletions vo/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package vo

import (
"encoding/json"
"regexp"
"strconv"

"github.com/drone/envsubst"
Expand All @@ -18,14 +19,17 @@ type (
Env struct {
Name string `json:"name" yaml:"name"`

// Github parameters of deployment.
// GitHub parameters of deployment.
Task *string `json:"task" yaml:"task"`
Description *string `json:"description" yaml:"description"`
AutoMerge *bool `json:"auto_merge" yaml:"auto_merge"`
RequiredContexts *[]string `json:"required_contexts,omitempty" yaml:"required_contexts"`
Payload interface{} `json:"payload" yaml:"payload"`
ProductionEnvironment *bool `json:"production_environment" yaml:"production_environment"`

// DeployableRef validates the ref is deployable or not.
DeployableRef *string `json:"deployable_ref" yaml:"deployable_ref"`

// Review is the configuration of Review,
// It is disabled when it is empty.
Review *Review `json:"review,omitempty" yaml:"review"`
Expand All @@ -48,7 +52,9 @@ const (
)

const (
defaultDeployTask = "deploy"
// defaultDeployTask is the value of the 'GITPLOY_DEPLOY_TASK' variable.
defaultDeployTask = "deploy"
// defaultRollbackTask is the value of the 'GITPLOY_ROLLBACK_TASK' variable.
defaultRollbackTask = "rollback"
)

Expand Down Expand Up @@ -80,10 +86,26 @@ func (c *Config) GetEnv(name string) *Env {
return nil
}

// IsProductionEnvironment check whether the environment is production or not.
func (e *Env) IsProductionEnvironment() bool {
return e.ProductionEnvironment != nil && *e.ProductionEnvironment
}

// IsDeployableRef validate the ref is deployable.
func (e *Env) IsDeployableRef(ref string) (bool, error) {
if e.DeployableRef == nil {
return true, nil
}

matched, err := regexp.MatchString(*e.DeployableRef, ref)
if err != nil {
return false, eutil.NewError(eutil.ErrorCodeConfigRegexpError, err)
}

return matched, nil
}

// HasReview check whether the review is enabled or not.
func (e *Env) HasReview() bool {
return e.Review != nil && e.Review.Enabled
}
Expand Down
48 changes: 48 additions & 0 deletions vo/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,54 @@ func TestEnv_IsProductionEnvironment(t *testing.T) {
})
}

func TestEnv_IsDeployableRef(t *testing.T) {
t.Run("Return true when 'deployable_ref' is not defined.", func(t *testing.T) {
e := &Env{}

ret, err := e.IsDeployableRef("")
if err != nil {
t.Fatalf("IsDeployableRef returns an error: %s", err)
}

expected := true
if ret != expected {
t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected)
}
})

t.Run("Return true when 'deployable_ref' is matched.", func(t *testing.T) {
e := &Env{
DeployableRef: pointer.ToString("main"),
}

ret, err := e.IsDeployableRef("main")
if err != nil {
t.Fatalf("IsDeployableRef returns an error: %s", err)
}

expected := true
if ret != expected {
t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected)
}
})

t.Run("Return false when 'deployable_ref' is not matched.", func(t *testing.T) {
e := &Env{
DeployableRef: pointer.ToString("main"),
}

ret, err := e.IsDeployableRef("branch")
if err != nil {
t.Fatalf("IsDeployableRef returns an error: %s", err)
}

expected := false
if ret != expected {
t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected)
}
})
}

func TestEnv_Eval(t *testing.T) {
t.Run("eval the task.", func(t *testing.T) {
cs := []struct {
Expand Down