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

Commit e929347

Browse files
author
Noah Hanjun Lee
authored
Migrate the logic of DeployToRemote into the interactor package (#191)
* Fix to handle errors of 'GetConfig' * Migrate validation into the interactor package
1 parent 2b2bdf5 commit e929347

File tree

13 files changed

+213
-136
lines changed

13 files changed

+213
-136
lines changed

internal/interactor/deployment.go

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import (
1414
"go.uber.org/zap"
1515
)
1616

17+
// Deploy creates a new remote deployment basically.
18+
// But if the approval flag is enabled, it saves the deployment only.
1719
func (i *Interactor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error) {
18-
// Verify the payload is deployable.
1920
if locked, err := i.Store.HasLockOfRepoForEnv(ctx, r, d.Env); locked {
2021
return nil, e.NewError(
2122
e.ErrorCodeDeploymentLocked,
@@ -53,7 +54,7 @@ func (i *Interactor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *en
5354
}
5455

5556
i.log.Debug("Create a new remote deployment.")
56-
rd, err := i.SCM.CreateRemoteDeployment(ctx, u, r, d, env)
57+
rd, err := i.createRemoteDeployment(ctx, u, r, d, env)
5758
if err != nil {
5859
return nil, err
5960
}
@@ -103,16 +104,25 @@ func (i *Interactor) IsApproved(ctx context.Context, d *ent.Deployment) bool {
103104
return approved >= d.RequiredApprovalCount
104105
}
105106

106-
func (i *Interactor) CreateRemoteDeployment(ctx context.Context, u *ent.User, re *ent.Repo, d *ent.Deployment, e *vo.Env) (*ent.Deployment, error) {
107-
// Rollback configures it can deploy the ref without any constraints.
108-
// 1) Set auto_merge false to avoid the merge conflict.
109-
// 2) Set required_contexts empty to skip the verfication.
110-
if d.IsRollback {
111-
e.AutoMerge = pointer.ToBool(false)
112-
e.RequiredContexts = &[]string{}
107+
// DeployToRemote create a new remote deployment after the deployment was approved.
108+
func (i *Interactor) DeployToRemote(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error) {
109+
if locked, err := i.Store.HasLockOfRepoForEnv(ctx, r, d.Env); locked {
110+
return nil, e.NewError(
111+
e.ErrorCodeDeploymentLocked,
112+
err,
113+
)
114+
} else if err != nil {
115+
return nil, err
113116
}
114117

115-
rd, err := i.SCM.CreateRemoteDeployment(ctx, u, re, d, e)
118+
if d.IsApprovalEnabled && !i.IsApproved(ctx, d) {
119+
return nil, e.NewError(
120+
e.ErrorCodeDeploymentUnapproved,
121+
nil,
122+
)
123+
}
124+
125+
rd, err := i.createRemoteDeployment(ctx, u, r, d, env)
116126
if err != nil {
117127
return nil, err
118128
}
@@ -136,6 +146,18 @@ func (i *Interactor) CreateRemoteDeployment(ctx context.Context, u *ent.User, re
136146
return d, nil
137147
}
138148

149+
func (i *Interactor) createRemoteDeployment(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (*vo.RemoteDeployment, error) {
150+
// Rollback configures it can deploy the ref without any constraints.
151+
// 1) Set auto_merge false to avoid the merge conflict.
152+
// 2) Set required_contexts empty to skip the verfication.
153+
if d.IsRollback {
154+
env.AutoMerge = pointer.ToBool(false)
155+
env.RequiredContexts = &[]string{}
156+
}
157+
158+
return i.SCM.CreateRemoteDeployment(ctx, u, r, d, env)
159+
}
160+
139161
func (i *Interactor) runClosingInactiveDeployment(stop <-chan struct{}) {
140162
ctx := context.Background()
141163

internal/interactor/deployment_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ func TestInteractor_Deploy(t *testing.T) {
212212
})
213213
}
214214

215-
func TestInteractor_CreateRemoteDeployment(t *testing.T) {
215+
func TestInteractor_DeployToRemote(t *testing.T) {
216216
ctx := gomock.Any()
217217

218218
t.Run("create a new remote deployment and update the deployment.", func(t *testing.T) {
@@ -238,19 +238,26 @@ func TestInteractor_CreateRemoteDeployment(t *testing.T) {
238238
UID = 1000
239239
)
240240

241-
t.Logf("Returns a new remote deployment with UID = %d", UID)
241+
t.Log("MOCK - Check the environment is locked.")
242+
store.
243+
EXPECT().
244+
HasLockOfRepoForEnv(ctx, gomock.AssignableToTypeOf(&ent.Repo{}), gomock.AssignableToTypeOf("")).
245+
Return(false, nil)
246+
247+
t.Logf("MOCK - Returns a new remote deployment with UID = %d", UID)
242248
scm.
243249
EXPECT().
244250
CreateRemoteDeployment(ctx, gomock.Eq(input.u), gomock.Eq(input.r), gomock.Eq(input.d), gomock.Eq(input.e)).
245251
Return(&vo.RemoteDeployment{
246252
UID: UID,
247253
}, nil)
248254

249-
t.Logf("Check the deployment input has UID")
255+
t.Logf("MOCK - Check the deployment input has UID")
250256
store.
251257
EXPECT().
252258
UpdateDeployment(ctx, gomock.Eq(&ent.Deployment{
253259
ID: input.d.ID,
260+
Env: input.d.Env,
254261
UID: UID,
255262
Status: deployment.StatusCreated,
256263
})).
@@ -264,7 +271,7 @@ func TestInteractor_CreateRemoteDeployment(t *testing.T) {
264271

265272
i := newMockInteractor(store, scm)
266273

267-
d, err := i.CreateRemoteDeployment(context.Background(), input.u, input.r, input.d, input.e)
274+
d, err := i.DeployToRemote(context.Background(), input.u, input.r, input.d, input.e)
268275
if err != nil {
269276
t.Errorf("CreateRemoteDeployment returns a error: %s", err)
270277
t.FailNow()

internal/pkg/github/deployment.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,10 @@ func (g *Github) GetConfig(ctx context.Context, u *ent.User, r *ent.Repo) (*vo.C
7575
Repositories.
7676
GetContents(ctx, r.Namespace, r.Name, r.ConfigPath, &github.RepositoryContentGetOptions{})
7777
if res.StatusCode == http.StatusNotFound {
78-
return nil, &vo.ConfigNotFoundError{
79-
RepoName: r.Name,
80-
}
78+
return nil, e.NewError(
79+
e.ErrorCodeConfigNotFound,
80+
err,
81+
)
8182
} else if err != nil {
8283
return nil, err
8384
}
@@ -89,10 +90,10 @@ func (g *Github) GetConfig(ctx context.Context, u *ent.User, r *ent.Repo) (*vo.C
8990

9091
c := &vo.Config{}
9192
if err := vo.UnmarshalYAML([]byte(content), c); err != nil {
92-
return nil, &vo.ConfigParseError{
93-
RepoName: r.Name,
94-
Err: err,
95-
}
93+
return nil, e.NewError(
94+
e.ErrorCodeConfigParseError,
95+
err,
96+
)
9697
}
9798

9899
return c, nil

internal/server/api/v1/repos/deployment.go

Lines changed: 36 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/gitploy-io/gitploy/ent/deployment"
1414
"github.com/gitploy-io/gitploy/ent/event"
1515
gb "github.com/gitploy-io/gitploy/internal/server/global"
16+
"github.com/gitploy-io/gitploy/pkg/e"
1617
"github.com/gitploy-io/gitploy/vo"
1718
)
1819

@@ -88,23 +89,25 @@ func (r *Repo) CreateDeployment(c *gin.Context) {
8889
re := vr.(*ent.Repo)
8990

9091
cf, err := r.i.GetConfig(ctx, u, re)
91-
if vo.IsConfigNotFoundError(err) || vo.IsConfigParseError(err) {
92-
r.log.Warn("The configuration is invalid.", zap.Error(err))
93-
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "The configuration is invalid.")
92+
if e.HasErrorCode(err, e.ErrorCodeConfigNotFound) {
93+
r.log.Error("The configuration file is not found.", zap.Error(err))
94+
// To override the HTTP status 422.
95+
gb.ResponseWithStatusAndError(c, http.StatusUnprocessableEntity, err)
9496
return
9597
} else if err != nil {
96-
r.log.Error("failed to get the configuration file.", zap.Error(err))
97-
gb.ErrorResponse(c, http.StatusInternalServerError, "It has failed to get the configuraton file.")
98+
r.log.Error("It has failed to get the configuration.")
99+
gb.ResponseWithError(c, err)
98100
return
99101
}
100102

101-
if !cf.HasEnv(p.Env) {
102-
r.log.Warn("The environment is not defined in the config.", zap.Error(err))
103-
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "The environment is not defined in the config.")
103+
var env *vo.Env
104+
if env = cf.GetEnv(p.Env); env == nil {
105+
r.log.Warn("The environment is not defined in the configuration.", zap.Error(err))
106+
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "The environment is not defined in the configuration.")
104107
return
105108
}
106109

107-
if err := cf.GetEnv(p.Env).Eval(&vo.EvalValues{}); err != nil {
110+
if err := env.Eval(&vo.EvalValues{}); err != nil {
108111
r.log.Warn("It has failed to eval variables in the config.", zap.Error(err))
109112
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "It has failed to eval variables in the config.")
110113
return
@@ -124,7 +127,6 @@ func (r *Repo) CreateDeployment(c *gin.Context) {
124127
return
125128
}
126129

127-
// Dispatch the event.
128130
if _, err := r.i.CreateEvent(ctx, &ent.Event{
129131
Kind: event.KindDeployment,
130132
Type: event.TypeCreated,
@@ -169,54 +171,34 @@ func (r *Repo) UpdateDeployment(c *gin.Context) {
169171
}
170172

171173
cf, err := r.i.GetConfig(ctx, u, re)
172-
if vo.IsConfigNotFoundError(err) || vo.IsConfigParseError(err) {
173-
r.log.Warn("The configuration is invalid.", zap.Error(err))
174-
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "The configuration is invalid.")
174+
if e.HasErrorCode(err, e.ErrorCodeConfigNotFound) {
175+
r.log.Error("The configuration file is not found.", zap.Error(err))
176+
// To override the HTTP status 422.
177+
gb.ResponseWithStatusAndError(c, http.StatusUnprocessableEntity, err)
175178
return
176179
} else if err != nil {
177-
r.log.Error("failed to get the configuration file.", zap.Error(err))
178-
gb.ErrorResponse(c, http.StatusInternalServerError, "It has failed to get the configuraton file.")
180+
r.log.Error("It has failed to get the configuration.")
181+
gb.ResponseWithError(c, err)
179182
return
180183
}
181184

182-
if !cf.HasEnv(d.Env) {
185+
var env *vo.Env
186+
if env = cf.GetEnv(d.Env); env == nil {
183187
r.log.Warn("The environment is not defined in the config.", zap.Error(err))
184188
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "The environment is not defined in the config.")
185189
return
186190
}
187191

188-
if err := cf.GetEnv(d.Env).Eval(&vo.EvalValues{IsRollback: d.IsRollback}); err != nil {
192+
if err := env.Eval(&vo.EvalValues{IsRollback: d.IsRollback}); err != nil {
189193
r.log.Warn("It has failed to eval variables in the config.", zap.Error(err))
190194
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "It has failed to eval variables in the config.")
191195
return
192196
}
193197

194-
if locked, err := r.i.HasLockOfRepoForEnv(ctx, re, d.Env); locked {
195-
r.log.Info("The environment is locked.", zap.String("env", d.Env))
196-
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "The environment is locked.")
197-
return
198-
} else if err != nil {
199-
r.log.Error("It has failed to check the lock.", zap.Error(err))
200-
gb.ErrorResponse(c, http.StatusInternalServerError, "It has failed to check the lock.")
201-
return
202-
}
203-
204198
if p.Status == string(deployment.StatusCreated) && d.Status == deployment.StatusWaiting {
205-
// Check the deployment is approved:
206-
// Approved >= Required Approval Count
207-
if !r.i.IsApproved(ctx, d) {
208-
r.log.Warn("The deployment is not approved yet.", zap.Int("deployment_id", d.ID))
209-
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "It is not approved yet.")
210-
return
211-
}
212-
213-
if d, err = r.i.CreateRemoteDeployment(ctx, u, re, d, cf.GetEnv(d.Env)); vo.IsUnprocessibleDeploymentError(err) {
214-
r.log.Warn("It is unprocessible entity.", zap.Error(err))
215-
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "There is a merge conflict or the commit's status checks failed.")
216-
return
217-
} else if err != nil {
218-
r.log.Error("It has failed to create the remote deployment.", zap.Error(err))
219-
gb.ErrorResponse(c, http.StatusInternalServerError, "It has failed to create the remote deployment.")
199+
if d, err = r.i.DeployToRemote(ctx, u, re, d, env); err != nil {
200+
r.log.Error("It has failed to deploy to the remote.", zap.Error(err))
201+
gb.ResponseWithError(c, err)
220202
return
221203
}
222204

@@ -258,23 +240,25 @@ func (r *Repo) RollbackDeployment(c *gin.Context) {
258240
}
259241

260242
cf, err := r.i.GetConfig(ctx, u, re)
261-
if vo.IsConfigNotFoundError(err) || vo.IsConfigParseError(err) {
262-
r.log.Warn("The configuration is invalid.", zap.Error(err))
263-
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "The configuration is invalid.")
243+
if e.HasErrorCode(err, e.ErrorCodeConfigNotFound) {
244+
r.log.Error("The configuration file is not found.", zap.Error(err))
245+
// To override the HTTP status 422.
246+
gb.ResponseWithStatusAndError(c, http.StatusUnprocessableEntity, err)
264247
return
265248
} else if err != nil {
266-
r.log.Error("failed to get the configuration file.", zap.Error(err))
267-
gb.ErrorResponse(c, http.StatusInternalServerError, "It has failed to get the configuraton file.")
249+
r.log.Error("It has failed to get the configuration.")
250+
gb.ResponseWithError(c, err)
268251
return
269252
}
270253

271-
if !cf.HasEnv(d.Env) {
254+
var env *vo.Env
255+
if env = cf.GetEnv(d.Env); env == nil {
272256
r.log.Warn("The environment is not defined in the configuration.", zap.Error(err))
273257
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "The environment is not defined in the configuration.")
274258
return
275259
}
276260

277-
if err := cf.GetEnv(d.Env).Eval(&vo.EvalValues{IsRollback: true}); err != nil {
261+
if err := env.Eval(&vo.EvalValues{IsRollback: true}); err != nil {
278262
r.log.Warn("It has failed to eval variables in the config.", zap.Error(err))
279263
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "It has failed to eval variables in the config.")
280264
return
@@ -410,17 +394,9 @@ func (r *Repo) GetConfig(c *gin.Context) {
410394
ctx := c.Request.Context()
411395

412396
config, err := r.i.GetConfig(ctx, u, re)
413-
if vo.IsConfigNotFoundError(err) {
414-
r.log.Warn("failed to find the config file.", zap.Error(err))
415-
gb.ErrorResponse(c, http.StatusNotFound, "It has failed to find the configuraton file.")
416-
return
417-
} else if vo.IsConfigParseError(err) {
418-
r.log.Warn("failed to parse the config.", zap.Error(err))
419-
gb.ErrorResponse(c, http.StatusUnprocessableEntity, "It has failed to parse the configuraton file.")
420-
return
421-
} else if err != nil {
422-
r.log.Error("failed to get the config file.", zap.Error(err))
423-
gb.ErrorResponse(c, http.StatusInternalServerError, "It has failed to get the config file.")
397+
if err != nil {
398+
r.log.Error("It has failed to get the configuration.", zap.Error(err))
399+
gb.ResponseWithError(c, err)
424400
return
425401
}
426402

internal/server/api/v1/repos/interface.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type (
2929
FindPrevSuccessDeployment(ctx context.Context, d *ent.Deployment) (*ent.Deployment, error)
3030
IsApproved(ctx context.Context, d *ent.Deployment) bool
3131
Deploy(ctx context.Context, u *ent.User, re *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error)
32-
CreateRemoteDeployment(ctx context.Context, u *ent.User, re *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error)
32+
DeployToRemote(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error)
3333
GetConfig(ctx context.Context, u *ent.User, r *ent.Repo) (*vo.Config, error)
3434

3535
ListApprovals(ctx context.Context, d *ent.Deployment) ([]*ent.Approval, error)

internal/server/api/v1/repos/mock/interactor.go

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/server/global/http.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,23 @@ func ResponseWithError(c *gin.Context, err error) {
2121
if ge, ok := err.(*e.Error); ok {
2222
c.JSON(e.GetHttpCode(ge.Code), map[string]string{
2323
"code": string(ge.Code),
24-
"message": e.GetMessage(ge.Code),
24+
"message": ge.Message,
25+
})
26+
return
27+
}
28+
29+
c.JSON(http.StatusInternalServerError, map[string]string{
30+
"code": string(e.ErrorCodeInternalError),
31+
"message": err.Error(),
32+
})
33+
}
34+
35+
// ResponseWithStatusAndError
36+
func ResponseWithStatusAndError(c *gin.Context, status int, err error) {
37+
if ge, ok := err.(*e.Error); ok {
38+
c.JSON(status, map[string]string{
39+
"code": string(ge.Code),
40+
"message": ge.Message,
2541
})
2642
return
2743
}

0 commit comments

Comments
 (0)