Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit fb63d42

Browse files
author
Noah Hanjun Lee
authored
Add 'deployable_ref' field for the validation of ref (#222)
* Add 'deployable_ref' field * Add the validation for 'deployable_ref' * Add the documentation
1 parent bb48e1a commit fb63d42

File tree

7 files changed

+191
-36
lines changed

7 files changed

+191
-36
lines changed

docs/references/deploy.yml.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ Field |Type |Required |Description
1717
`required_contexts` |*[]string* |`false` |This field allows you to specify a subset of contexts that must be success.
1818
`payload` |*object* or *string* |`false` |This field is JSON payload with extra information about the deployment.
1919
`production_environment` |*boolean* |`false` |This field specifies whether this runtime environment is production or not.
20-
`review` |*[Review](#review)* |`false` |This field configures review.
20+
`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.
21+
`review` |*[Review](#review)* |`false` |This field configures review.
2122

2223
## Review
2324

internal/interactor/deployment.go

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,26 @@ import (
1414
"go.uber.org/zap"
1515
)
1616

17+
func (i *Interactor) IsApproved(ctx context.Context, d *ent.Deployment) bool {
18+
rvs, _ := i.Store.ListReviews(ctx, d)
19+
20+
for _, r := range rvs {
21+
if r.Status == review.StatusRejected {
22+
return false
23+
}
24+
}
25+
26+
for _, r := range rvs {
27+
if r.Status == review.StatusApproved {
28+
return true
29+
}
30+
}
31+
32+
return false
33+
}
34+
1735
func (i *Interactor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error) {
18-
if locked, err := i.Store.HasLockOfRepoForEnv(ctx, r, d.Env); locked {
19-
return nil, e.NewError(
20-
e.ErrorCodeDeploymentLocked,
21-
err,
22-
)
23-
} else if err != nil {
36+
if ok, err := i.isDeployable(ctx, u, r, d, env); !ok {
2437
return nil, err
2538
}
2639

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

104-
func (i *Interactor) IsApproved(ctx context.Context, d *ent.Deployment) bool {
105-
rvs, _ := i.ListReviews(ctx, d)
106-
107-
for _, r := range rvs {
108-
if r.Status == review.StatusRejected {
109-
return false
110-
}
111-
}
112-
113-
for _, r := range rvs {
114-
if r.Status == review.StatusApproved {
115-
return true
116-
}
117-
}
118-
119-
return false
120-
}
121-
122117
// DeployToRemote create a new remote deployment after the deployment was approved.
123118
func (i *Interactor) DeployToRemote(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error) {
124119
if d.Status != deployment.StatusWaiting {
@@ -129,12 +124,7 @@ func (i *Interactor) DeployToRemote(ctx context.Context, u *ent.User, r *ent.Rep
129124
)
130125
}
131126

132-
if locked, err := i.Store.HasLockOfRepoForEnv(ctx, r, d.Env); locked {
133-
return nil, e.NewError(
134-
e.ErrorCodeDeploymentLocked,
135-
err,
136-
)
137-
} else if err != nil {
127+
if ok, err := i.isDeployable(ctx, u, r, d, env); !ok {
138128
return nil, err
139129
}
140130

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

174+
func (i *Interactor) isDeployable(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (bool, error) {
175+
if ok, err := env.IsDeployableRef(d.Ref); err != nil {
176+
return false, err
177+
} else if !ok {
178+
return false, e.NewErrorWithMessage(e.ErrorCodeUnprocessableEntity, "The ref is not matched with 'deployable_ref'.", nil)
179+
}
180+
181+
// Check that the environment is locked.
182+
if locked, err := i.Store.HasLockOfRepoForEnv(ctx, r, d.Env); locked {
183+
return false, e.NewError(e.ErrorCodeDeploymentLocked, err)
184+
} else if err != nil {
185+
return false, e.NewError(e.ErrorCodeInternalError, err)
186+
}
187+
188+
return true, nil
189+
}
190+
184191
func (i *Interactor) runClosingInactiveDeployment(stop <-chan struct{}) {
185192
ctx := context.Background()
186193

internal/interactor/deployment_test.go

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"reflect"
66
"testing"
77

8+
"github.com/AlekSi/pointer"
89
"github.com/golang/mock/gomock"
910
"go.uber.org/zap"
1011

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

28+
func TestInteractor_IsApproved(t *testing.T) {
29+
t.Run("Return false when a review is rejected.", func(t *testing.T) {
30+
ctrl := gomock.NewController(t)
31+
store := mock.NewMockStore(ctrl)
32+
scm := mock.NewMockSCM(ctrl)
33+
34+
t.Log("Return various status reviews")
35+
store.
36+
EXPECT().
37+
ListReviews(gomock.Any(), gomock.AssignableToTypeOf(&ent.Deployment{})).
38+
Return([]*ent.Review{
39+
{
40+
Status: review.StatusPending,
41+
},
42+
{
43+
Status: review.StatusRejected,
44+
},
45+
}, nil)
46+
47+
i := newMockInteractor(store, scm)
48+
49+
expected := false
50+
if ret := i.IsApproved(context.Background(), &ent.Deployment{}); ret != expected {
51+
t.Fatalf("IsApproved = %v, wanted %v", ret, expected)
52+
}
53+
})
54+
}
55+
2756
func TestInteractor_Deploy(t *testing.T) {
2857
ctx := gomock.Any()
2958

59+
t.Run("Return an error when the ref is not deployable", func(t *testing.T) {
60+
input := struct {
61+
d *ent.Deployment
62+
e *vo.Env
63+
}{
64+
d: &ent.Deployment{
65+
Type: deployment.TypeBranch,
66+
Ref: "main",
67+
Env: "production",
68+
},
69+
e: &vo.Env{
70+
DeployableRef: pointer.ToString("releast-.*"),
71+
},
72+
}
73+
74+
ctrl := gomock.NewController(t)
75+
store := mock.NewMockStore(ctrl)
76+
scm := mock.NewMockSCM(ctrl)
77+
78+
i := newMockInteractor(store, scm)
79+
80+
_, err := i.Deploy(context.Background(), &ent.User{}, &ent.Repo{}, input.d, input.e)
81+
if !e.HasErrorCode(err, e.ErrorCodeUnprocessableEntity) {
82+
t.Fatalf("Deploy' error = %v, wanted ErrorCodeDeploymentLocked", err)
83+
}
84+
})
85+
86+
t.Run("Return an error when the environment is locked", func(t *testing.T) {
87+
ctrl := gomock.NewController(t)
88+
store := mock.NewMockStore(ctrl)
89+
scm := mock.NewMockSCM(ctrl)
90+
91+
store.
92+
EXPECT().
93+
HasLockOfRepoForEnv(ctx, gomock.AssignableToTypeOf(&ent.Repo{}), "").
94+
Return(true, nil)
95+
96+
i := newMockInteractor(store, scm)
97+
98+
_, err := i.Deploy(context.Background(), &ent.User{}, &ent.Repo{}, &ent.Deployment{}, &vo.Env{})
99+
if !e.HasErrorCode(err, e.ErrorCodeDeploymentLocked) {
100+
t.Fatalf("Deploy' error = %v, wanted ErrorCodeDeploymentLocked", err)
101+
}
102+
})
103+
30104
t.Run("Return a new deployment.", func(t *testing.T) {
31105
input := struct {
32106
d *ent.Deployment
@@ -89,8 +163,7 @@ func TestInteractor_Deploy(t *testing.T) {
89163

90164
d, err := i.Deploy(context.Background(), &ent.User{}, &ent.Repo{}, input.d, input.e)
91165
if err != nil {
92-
t.Errorf("Deploy returns a error: %s", err)
93-
t.FailNow()
166+
t.Fatalf("Deploy returns a error: %s", err)
94167
}
95168

96169
expected := &ent.Deployment{

pkg/e/code.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import (
88
const (
99
// ErrorCodeConfigParseError is that an error occurs when it parse the file.
1010
ErrorCodeConfigParseError ErrorCode = "config_parse_error"
11+
// ErrorCodeConfigRegexpError is the regexp(re2) is invalid.
12+
ErrorCodeConfigRegexpError ErrorCode = "config_regexp_error"
1113

1214
// ErrorCodeDeploymentConflict is the deployment number is conflicted.
1315
ErrorCodeDeploymentConflict ErrorCode = "deployment_conflict"
14-
// ErrorCodeDeploymentInvalid is the payload is invalid.
16+
// ErrorCodeDeploymentInvalid is the payload is invalid when it posts a remote deployment.
1517
ErrorCodeDeploymentInvalid ErrorCode = "deployment_invalid"
1618
// ErrorCodeDeploymentLocked is when the environment is locked.
1719
ErrorCodeDeploymentLocked ErrorCode = "deployment_locked"

pkg/e/trans.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "net/http"
44

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

3132
var httpCodes = map[ErrorCode]int{
3233
ErrorCodeConfigParseError: http.StatusUnprocessableEntity,
34+
ErrorCodeConfigRegexpError: http.StatusUnprocessableEntity,
3335
ErrorCodeDeploymentConflict: http.StatusUnprocessableEntity,
3436
ErrorCodeDeploymentInvalid: http.StatusUnprocessableEntity,
3537
ErrorCodeDeploymentLocked: http.StatusUnprocessableEntity,

vo/config.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package vo
22

33
import (
44
"encoding/json"
5+
"regexp"
56
"strconv"
67

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

21-
// Github parameters of deployment.
22+
// GitHub parameters of deployment.
2223
Task *string `json:"task" yaml:"task"`
2324
Description *string `json:"description" yaml:"description"`
2425
AutoMerge *bool `json:"auto_merge" yaml:"auto_merge"`
2526
RequiredContexts *[]string `json:"required_contexts,omitempty" yaml:"required_contexts"`
2627
Payload interface{} `json:"payload" yaml:"payload"`
2728
ProductionEnvironment *bool `json:"production_environment" yaml:"production_environment"`
2829

30+
// DeployableRef validates the ref is deployable or not.
31+
DeployableRef *string `json:"deployable_ref" yaml:"deployable_ref"`
32+
2933
// Review is the configuration of Review,
3034
// It is disabled when it is empty.
3135
Review *Review `json:"review,omitempty" yaml:"review"`
@@ -48,7 +52,9 @@ const (
4852
)
4953

5054
const (
51-
defaultDeployTask = "deploy"
55+
// defaultDeployTask is the value of the 'GITPLOY_DEPLOY_TASK' variable.
56+
defaultDeployTask = "deploy"
57+
// defaultRollbackTask is the value of the 'GITPLOY_ROLLBACK_TASK' variable.
5258
defaultRollbackTask = "rollback"
5359
)
5460

@@ -80,10 +86,26 @@ func (c *Config) GetEnv(name string) *Env {
8086
return nil
8187
}
8288

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

94+
// IsDeployableRef validate the ref is deployable.
95+
func (e *Env) IsDeployableRef(ref string) (bool, error) {
96+
if e.DeployableRef == nil {
97+
return true, nil
98+
}
99+
100+
matched, err := regexp.MatchString(*e.DeployableRef, ref)
101+
if err != nil {
102+
return false, eutil.NewError(eutil.ErrorCodeConfigRegexpError, err)
103+
}
104+
105+
return matched, nil
106+
}
107+
108+
// HasReview check whether the review is enabled or not.
87109
func (e *Env) HasReview() bool {
88110
return e.Review != nil && e.Review.Enabled
89111
}

vo/config_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,54 @@ func TestEnv_IsProductionEnvironment(t *testing.T) {
112112
})
113113
}
114114

115+
func TestEnv_IsDeployableRef(t *testing.T) {
116+
t.Run("Return true when 'deployable_ref' is not defined.", func(t *testing.T) {
117+
e := &Env{}
118+
119+
ret, err := e.IsDeployableRef("")
120+
if err != nil {
121+
t.Fatalf("IsDeployableRef returns an error: %s", err)
122+
}
123+
124+
expected := true
125+
if ret != expected {
126+
t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected)
127+
}
128+
})
129+
130+
t.Run("Return true when 'deployable_ref' is matched.", func(t *testing.T) {
131+
e := &Env{
132+
DeployableRef: pointer.ToString("main"),
133+
}
134+
135+
ret, err := e.IsDeployableRef("main")
136+
if err != nil {
137+
t.Fatalf("IsDeployableRef returns an error: %s", err)
138+
}
139+
140+
expected := true
141+
if ret != expected {
142+
t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected)
143+
}
144+
})
145+
146+
t.Run("Return false when 'deployable_ref' is not matched.", func(t *testing.T) {
147+
e := &Env{
148+
DeployableRef: pointer.ToString("main"),
149+
}
150+
151+
ret, err := e.IsDeployableRef("branch")
152+
if err != nil {
153+
t.Fatalf("IsDeployableRef returns an error: %s", err)
154+
}
155+
156+
expected := false
157+
if ret != expected {
158+
t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected)
159+
}
160+
})
161+
}
162+
115163
func TestEnv_Eval(t *testing.T) {
116164
t.Run("eval the task.", func(t *testing.T) {
117165
cs := []struct {

0 commit comments

Comments
 (0)