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

Commit 783a1c7

Browse files
author
Noah Lee
authored
Add a new feature to configure windows freezing deployment (#283)
* Add a new package `cronexpr` * Add a new field `frozen_windows` and a new method `IsFreezed` * Add documentation
1 parent 34c1917 commit 783a1c7

File tree

9 files changed

+242
-30
lines changed

9 files changed

+242
-30
lines changed

docs/concepts/deploy.yml.md

+18
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,24 @@ envs:
6262
production_environment: true
6363
```
6464

65+
### Deploy Freeze Window
66+
67+
Gitploy support to add a window to prevent unintended deployment for the environment. You can freeze a window periodically by a cron expression.
68+
69+
```yaml
70+
envs:
71+
- name: production
72+
frozen_windows:
73+
# Freeze every midnights
74+
- start: "50 23 * * *"
75+
duration: 20m
76+
location: America/New_York
77+
# Freeze every weekends
78+
- start: "0 * * * SAT,SUN"
79+
duration: 1h
80+
location: Asia/Seoul
81+
```
82+
6583
### Review
6684

6785
Gitploy provides the review process. You can list up to users on the configuration file. You can check the [document](./review.md) for the detail.

docs/references/deploy.yml.md

+20-11
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,31 @@ Field |Type |Required |Description
1111
Field |Type |Required |Description
1212
--- |---- |--- |---
1313
`name` |*string* |`true` |This field is the runtime environment such as `production`, `staging`, and `qa`.
14-
`task` |*string* |`false` |This field is used by the deployment system to distinguish the kind of deployment.
15-
`description` |*string* |`false` |This field is the short description of the deployment.
16-
`auto_merge` |*boolean* |`false` |This field is used to ensure that the requested ref is not behind the repository's default branch. If you deploy with the commit or the tag you need to set `false`. For rollback, Gitploy set the field `false`.
17-
`required_contexts` |*[]string* |`false` |This field allows you to specify a subset of contexts that must be success.
18-
`payload` |*object* or *string* |`false` |This field is JSON payload with extra information about the deployment.
14+
`task` |*string* |`false` |This field is used by the deployment system to distinguish the kind of deployment. (*Only for `GitHub`*)
15+
`description` |*string* |`false` |This field is the short description of the deployment. (*Only for `GitHub`*)
16+
`auto_merge` |*boolean* |`false` |This field is used to ensure that the requested ref is not behind the repository's default branch. If you deploy with the commit or the tag you need to set `false`. For rollback, Gitploy set the field `false`. (*Only for `GitHub`*)
17+
`required_contexts` |*[]string* |`false` |This field allows you to specify a subset of contexts that must be success. (*Only for `GitHub`*)
18+
`payload` |*object* or *string* |`false` |This field is JSON payload with extra information about the deployment. (*Only for `GitHub`*)
1919
`production_environment` |*boolean* |`false` |This field specifies whether this runtime environment is production or not.
2020
`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))).
2121
`auto_deploy_on` |*string* |`false` |This field controls auto-deployment behaviour given a ref(branch, SHA, tag). If any new push events are detected on this event, the deployment will be triggered. It supports the regular expression ([re2](https://github.com/google/re2/wiki/Syntax)). E.g. `refs/heads/main` or `refs/tags/v.*`
22-
`review` |*[Review](#review)* |`false` |This field configures review.
22+
`review` |*[Review](#review)* |`false` |This field configures reviewers.
23+
`frozen_windows` |*[][Frozen Window](#frozen-window)* |`false` |This field configures to add a frozen window to prevent unintended deployment for the environment.
2324

2425
## Review
2526

26-
Field |Type |Tag |Description
27-
--- |--- |--- |---
28-
`enabled` |*boolean* |`true` |This field make to enable the review feature. The default value is `false`.
29-
`reviewers` |*[]string* |`false` |This field list up reviewers. The default value is `[]`. You should specify maintainers of the project.
27+
Field |Type |Required |Description
28+
--- |--- |--- |---
29+
`enabled` |*boolean* |`false` |This field makes to enables the review feature. The default value is `false`.
30+
`reviewers` |*[]string* |`false` |This field list up reviewers. The default value is `[]`. You should specify the maintainers of the project.
31+
32+
## Frozen Window
33+
34+
Field |Type |Required |Description
35+
--- |--- |--- |---
36+
`start` |*string* |`true` |This field is a cron expression to indicate when the window starts. For example, `55 23 * * *` means it starts to freeze a window before 5 minutes of midnight. You can check the [documentation](https://github.com/gitploy-io/cronexpr) for details.
37+
`duration` |*string* |`true` |This field configures how long the window is frozen from the starting. The duration string is a possibly signed sequence of decimal numbers and a unit suffix such as `5m`, or `1h30m`. Valid time units are `ns`, `us`, `ms`, `s`, `m`, `h`.
38+
`location` |*string* |`false` |This field configures the location of the `start` time. The value is taken to be a location name corresponding to a file in the IANA Time Zone database, such as `America/New_York`. The default value is `UTC`. You can check the [document](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the Time Zone database name.
3039

3140
## Variables
3241

@@ -36,7 +45,7 @@ The following variables are available in `${ }` syntax when evaluating `deploy.y
3645
* `GITPLOY_ROLLBACK_TASK`: Returns `rollback` for rollback, but deploy, it returns the empty string.
3746
* `GITPLOY_IS_ROLLBACK`: Returns `true` for rollback, but deploy, it returns `false`.
3847

39-
An example usage of this:
48+
Example usage of this:
4049

4150
```yaml
4251
envs:

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/gin-contrib/cors v1.3.1
1313
github.com/gin-contrib/sse v0.1.0
1414
github.com/gin-gonic/gin v1.7.7
15+
github.com/gitploy-io/cronexpr v0.2.2
1516
github.com/go-sql-driver/mysql v1.5.1-0.20200311113236-681ffa848bae
1617
github.com/golang/mock v1.6.0
1718
github.com/google/go-github/v32 v32.1.0

go.sum

+6
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
104104
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
105105
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
106106
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
107+
github.com/gitploy-io/cronexpr v0.2.0 h1:BmYX3+QNB11Bevp2z98taCOiNDhMo/E1pBeO6OzojAs=
108+
github.com/gitploy-io/cronexpr v0.2.0/go.mod h1:Uep5sbzUSocMZvJ1s0lNI9zi37s5iUI1llkw3vRGK9M=
109+
github.com/gitploy-io/cronexpr v0.2.1 h1:usx6GTAQh2q3E4S8jx5RWGGkr4LSxLH0mBcebGZGv+c=
110+
github.com/gitploy-io/cronexpr v0.2.1/go.mod h1:Uep5sbzUSocMZvJ1s0lNI9zi37s5iUI1llkw3vRGK9M=
111+
github.com/gitploy-io/cronexpr v0.2.2 h1:Au+wK6FqmOLAF7AkW6q4gnrNXTe3rEW97XFZ4chy0xs=
112+
github.com/gitploy-io/cronexpr v0.2.2/go.mod h1:Uep5sbzUSocMZvJ1s0lNI9zi37s5iUI1llkw3vRGK9M=
107113
github.com/go-bindata/go-bindata v1.0.1-0.20190711162640-ee3c2418e368/go.mod h1:7xCgX1lzlrXPHkfvn3EhumqHkmSlzt8at9q7v0ax19c=
108114
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
109115
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=

internal/interactor/deployment.go

+29-11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import (
1414
"go.uber.org/zap"
1515
)
1616

17+
// IsApproved verifies that the request is approved or not.
18+
// It is approved if there is an approval of reviews at least, but
19+
// it is rejected if there is a reject of reviews.
1720
func (i *Interactor) IsApproved(ctx context.Context, d *ent.Deployment) bool {
1821
rvs, _ := i.Store.ListReviews(ctx, d)
1922

@@ -32,8 +35,12 @@ func (i *Interactor) IsApproved(ctx context.Context, d *ent.Deployment) bool {
3235
return false
3336
}
3437

38+
// Deploy posts a new deployment to SCM with the payload.
39+
// But if it requires a review, it saves the payload on the DB
40+
// and waits until reviewed.
41+
// It returns an error for a undeployable payload.
3542
func (i *Interactor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *extent.Env) (*ent.Deployment, error) {
36-
if ok, err := i.isDeployable(ctx, u, r, d, env); !ok {
43+
if err := i.isDeployable(ctx, u, r, d, env); err != nil {
3744
return nil, err
3845
}
3946

@@ -114,7 +121,9 @@ func (i *Interactor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *en
114121
return d, nil
115122
}
116123

117-
// DeployToRemote create a new remote deployment after the deployment was approved.
124+
// DeployToRemote posts a new deployment to SCM with the saved payload
125+
// after review has finished.
126+
// It returns an error for a undeployable payload.
118127
func (i *Interactor) DeployToRemote(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *extent.Env) (*ent.Deployment, error) {
119128
if d.Status != deployment.StatusWaiting {
120129
return nil, e.NewErrorWithMessage(
@@ -124,7 +133,7 @@ func (i *Interactor) DeployToRemote(ctx context.Context, u *ent.User, r *ent.Rep
124133
)
125134
}
126135

127-
if ok, err := i.isDeployable(ctx, u, r, d, env); !ok {
136+
if err := i.isDeployable(ctx, u, r, d, env); err != nil {
128137
return nil, err
129138
}
130139

@@ -171,21 +180,30 @@ func (i *Interactor) createRemoteDeployment(ctx context.Context, u *ent.User, r
171180
return i.SCM.CreateRemoteDeployment(ctx, u, r, d, env)
172181
}
173182

174-
func (i *Interactor) isDeployable(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *extent.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.ErrorCodeEntityUnprocessable, "The ref is not matched with 'deployable_ref'.", nil)
183+
func (i *Interactor) isDeployable(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *extent.Env) error {
184+
// Skip verifications for roll back.
185+
if !d.IsRollback {
186+
if ok, err := env.IsDeployableRef(d.Ref); !ok {
187+
return e.NewErrorWithMessage(e.ErrorCodeEntityUnprocessable, "The ref is not matched with 'deployable_ref'.", nil)
188+
} else if err != nil {
189+
return err
190+
}
179191
}
180192

181193
// Check that the environment is locked.
182194
if locked, err := i.Store.HasLockOfRepoForEnv(ctx, r, d.Env); locked {
183-
return false, e.NewError(e.ErrorCodeDeploymentLocked, err)
195+
return e.NewError(e.ErrorCodeDeploymentLocked, err)
196+
} else if err != nil {
197+
return err
198+
}
199+
200+
if freezed, err := env.IsFreezed(time.Now().UTC()); freezed {
201+
return e.NewError(e.ErrorCodeDeploymentFrozen, err)
184202
} else if err != nil {
185-
return false, e.NewError(e.ErrorCodeInternalError, err)
203+
return err
186204
}
187205

188-
return true, nil
206+
return nil
189207
}
190208

191209
func (i *Interactor) runClosingInactiveDeployment(stop <-chan struct{}) {

model/extent/config.go

+52-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ package extent
33
import (
44
"regexp"
55
"strconv"
6+
"strings"
7+
"time"
68

79
"github.com/drone/envsubst"
10+
"github.com/gitploy-io/cronexpr"
811
"gopkg.in/yaml.v3"
912

1013
eutil "github.com/gitploy-io/gitploy/pkg/e"
@@ -38,13 +41,22 @@ type (
3841
// Review is the configuration of Review,
3942
// It is disabled when it is empty.
4043
Review *Review `json:"review,omitempty" yaml:"review"`
44+
45+
// FrozenWindows is the list of windows to freeze deployments.
46+
FrozenWindows []FrozenWindow `json:"frozen_windows" yaml:"frozen_windows"`
4147
}
4248

4349
Review struct {
4450
Enabled bool `json:"enabled" yaml:"enabled"`
4551
Reviewers []string `json:"reviewers" yaml:"reviewers"`
4652
}
4753

54+
FrozenWindow struct {
55+
Start string `json:"start" yaml:"start"`
56+
Duration string `json:"duration" yaml:"duration"`
57+
Location string `json:"location" yaml:"location"`
58+
}
59+
4860
EvalValues struct {
4961
IsRollback bool
5062
}
@@ -131,12 +143,12 @@ func (c *Config) GetEnv(name string) *Env {
131143
return nil
132144
}
133145

134-
// IsProductionEnvironment check whether the environment is production or not.
146+
// IsProductionEnvironment verifies whether the environment is production or not.
135147
func (e *Env) IsProductionEnvironment() bool {
136148
return e.ProductionEnvironment != nil && *e.ProductionEnvironment
137149
}
138150

139-
// IsDeployableRef validate the ref is deployable.
151+
// IsDeployableRef verifies the ref is deployable.
140152
func (e *Env) IsDeployableRef(ref string) (bool, error) {
141153
if e.DeployableRef == nil {
142154
return true, nil
@@ -150,7 +162,7 @@ func (e *Env) IsDeployableRef(ref string) (bool, error) {
150162
return matched, nil
151163
}
152164

153-
// IsAutoDeployOn validate the ref is matched with 'auto_deploy_on'.
165+
// IsAutoDeployOn verifies the ref is matched with 'auto_deploy_on'.
154166
func (e *Env) IsAutoDeployOn(ref string) (bool, error) {
155167
if e.AutoDeployOn == nil {
156168
return false, nil
@@ -168,3 +180,40 @@ func (e *Env) IsAutoDeployOn(ref string) (bool, error) {
168180
func (e *Env) HasReview() bool {
169181
return e.Review != nil && e.Review.Enabled
170182
}
183+
184+
// IsFreezed verifies whether the current time is in a freeze window.
185+
// It returns an error when parsing an expression is failed.
186+
func (e *Env) IsFreezed(t time.Time) (bool, error) {
187+
if len(e.FrozenWindows) == 0 {
188+
return false, nil
189+
}
190+
191+
for _, w := range e.FrozenWindows {
192+
s, err := cronexpr.ParseInLocation(strings.TrimSpace(w.Start), w.Location)
193+
if err != nil {
194+
return false, eutil.NewErrorWithMessage(
195+
eutil.ErrorCodeConfigInvalid,
196+
"The crontab expression of the freeze window is invalid.",
197+
err,
198+
)
199+
}
200+
201+
d, err := time.ParseDuration(w.Duration)
202+
if err != nil {
203+
return false, eutil.NewErrorWithMessage(
204+
eutil.ErrorCodeConfigInvalid,
205+
"The duration of the freeze window is invalid.",
206+
err,
207+
)
208+
}
209+
210+
// Add one minute to include the starting time.
211+
start := s.Prev(t.Add(time.Minute))
212+
end := start.Add(d)
213+
if t.After(start) && t.Before(end) {
214+
return true, nil
215+
}
216+
}
217+
218+
return false, nil
219+
}

0 commit comments

Comments
 (0)