From d3b016c63291f3b33da141fa2bf8727e3d1fd939 Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 26 Dec 2021 10:12:57 +0900 Subject: [PATCH 1/8] Add IsFreezed method --- go.mod | 1 + go.sum | 4 ++ model/extent/config.go | 55 ++++++++++++++++- model/extent/config_test.go | 117 ++++++++++++++++++++++++++++++++++-- 4 files changed, 169 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 71f6fffb..e6048dc7 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/gin-contrib/cors v1.3.1 github.com/gin-contrib/sse v0.1.0 github.com/gin-gonic/gin v1.7.7 + github.com/gitploy-io/cronexpr v0.2.1 github.com/go-sql-driver/mysql v1.5.1-0.20200311113236-681ffa848bae github.com/golang/mock v1.6.0 github.com/google/go-github/v32 v32.1.0 diff --git a/go.sum b/go.sum index 81ddeeef..ceb43b4f 100644 --- a/go.sum +++ b/go.sum @@ -104,6 +104,10 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/gitploy-io/cronexpr v0.2.0 h1:BmYX3+QNB11Bevp2z98taCOiNDhMo/E1pBeO6OzojAs= +github.com/gitploy-io/cronexpr v0.2.0/go.mod h1:Uep5sbzUSocMZvJ1s0lNI9zi37s5iUI1llkw3vRGK9M= +github.com/gitploy-io/cronexpr v0.2.1 h1:usx6GTAQh2q3E4S8jx5RWGGkr4LSxLH0mBcebGZGv+c= +github.com/gitploy-io/cronexpr v0.2.1/go.mod h1:Uep5sbzUSocMZvJ1s0lNI9zi37s5iUI1llkw3vRGK9M= github.com/go-bindata/go-bindata v1.0.1-0.20190711162640-ee3c2418e368/go.mod h1:7xCgX1lzlrXPHkfvn3EhumqHkmSlzt8at9q7v0ax19c= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff --git a/model/extent/config.go b/model/extent/config.go index efdd5b4c..b64076b0 100644 --- a/model/extent/config.go +++ b/model/extent/config.go @@ -1,10 +1,14 @@ package extent import ( + "fmt" "regexp" "strconv" + "strings" + "time" "github.com/drone/envsubst" + "github.com/gitploy-io/cronexpr" "gopkg.in/yaml.v3" eutil "github.com/gitploy-io/gitploy/pkg/e" @@ -38,6 +42,8 @@ type ( // Review is the configuration of Review, // It is disabled when it is empty. Review *Review `json:"review,omitempty" yaml:"review"` + + FreezeWindows []FreezeWindow `json:"freeze_windows" yaml:"freeze_windows"` } Review struct { @@ -45,6 +51,12 @@ type ( Reviewers []string `json:"reviewers" yaml:"reviewers"` } + FreezeWindow struct { + Start string `json:"start" yaml:"start"` + Duration string `json:"duration" yaml:"duration"` + Location string `json:"location" yaml:"location"` + } + EvalValues struct { IsRollback bool } @@ -131,12 +143,12 @@ func (c *Config) GetEnv(name string) *Env { return nil } -// IsProductionEnvironment check whether the environment is production or not. +// IsProductionEnvironment verifies whether the environment is production or not. func (e *Env) IsProductionEnvironment() bool { return e.ProductionEnvironment != nil && *e.ProductionEnvironment } -// IsDeployableRef validate the ref is deployable. +// IsDeployableRef verifies the ref is deployable. func (e *Env) IsDeployableRef(ref string) (bool, error) { if e.DeployableRef == nil { return true, nil @@ -150,7 +162,7 @@ func (e *Env) IsDeployableRef(ref string) (bool, error) { return matched, nil } -// IsAutoDeployOn validate the ref is matched with 'auto_deploy_on'. +// IsAutoDeployOn verifies the ref is matched with 'auto_deploy_on'. func (e *Env) IsAutoDeployOn(ref string) (bool, error) { if e.AutoDeployOn == nil { return false, nil @@ -168,3 +180,40 @@ func (e *Env) IsAutoDeployOn(ref string) (bool, error) { func (e *Env) HasReview() bool { return e.Review != nil && e.Review.Enabled } + +// IsFreezed verifies whether the current time is in a freeze window. +// It returns an error when parsing an expression is failed. +func (e *Env) IsFreezed(t time.Time) (bool, error) { + if len(e.FreezeWindows) == 0 { + return false, nil + } + + for _, w := range e.FreezeWindows { + s, err := cronexpr.ParseInLocation(strings.TrimSpace(w.Start), w.Location) + if err != nil { + return false, eutil.NewErrorWithMessage( + eutil.ErrorCodeConfigInvalid, + fmt.Sprintf("The crontab expression of the freeze window is invalid."), + err, + ) + } + + d, err := time.ParseDuration(w.Duration) + if err != nil { + return false, eutil.NewErrorWithMessage( + eutil.ErrorCodeConfigInvalid, + fmt.Sprintf("The duration of the freeze window is invalid."), + err, + ) + } + + // Add one minute to include the starting time. + start := s.Prev(t.Add(time.Minute)) + end := start.Add(d) + if t.After(start) && t.Before(end) { + return true, nil + } + } + + return false, nil +} diff --git a/model/extent/config_test.go b/model/extent/config_test.go index 81576b32..285aeddf 100644 --- a/model/extent/config_test.go +++ b/model/extent/config_test.go @@ -3,13 +3,14 @@ package extent import ( "reflect" "testing" + "time" "github.com/AlekSi/pointer" "github.com/davecgh/go-spew/spew" ) func TestUnmarshalYAML(t *testing.T) { - t.Run("unmarhsal the required_context field", func(tt *testing.T) { + t.Run("Unmarhsal the required_context field", func(tt *testing.T) { s := ` envs: - name: dev @@ -37,7 +38,7 @@ envs: } }) - t.Run("unmarshal auto_merge: false ", func(tt *testing.T) { + t.Run("Unmarshal 'auto_merge: false'", func(tt *testing.T) { s := ` envs: - name: dev @@ -64,7 +65,7 @@ envs: } }) - t.Run("unmarshal auto_merge: true", func(tt *testing.T) { + t.Run("Unmarshal 'auto_merge: true'", func(tt *testing.T) { s := ` envs: - name: dev @@ -92,7 +93,7 @@ envs: } func TestConfig_Eval(t *testing.T) { - t.Run("Evaluate the configuration.", func(t *testing.T) { + t.Run("Umarshal the task with the variable template.", func(t *testing.T) { s := ` envs: - name: dev @@ -122,7 +123,7 @@ envs: } }) - t.Run("Evaluate the configuration with the regexp.", func(t *testing.T) { + t.Run("Unmarshal the deployable_ref field with a regexp.", func(t *testing.T) { s := ` envs: - name: dev @@ -153,6 +154,38 @@ envs: t.Errorf("Config = %v expected %v", spew.Sdump(c), spew.Sdump(e)) } }) + + t.Run("Unmarshal the freeze_windows field", func(t *testing.T) { + s := ` +envs: + - name: dev + freeze_windows: + - start: "55 23 * * *" + duration: "10m"` + + c := &Config{} + if err := UnmarshalYAML([]byte(s), c); err != nil { + t.Fatalf("Failed to parse the configuration file: %v", err) + } + + e := &Config{ + Envs: []*Env{ + { + Name: "dev", + FreezeWindows: []FreezeWindow{ + { + Start: "55 23 * * *", + Duration: "10m", + }, + }, + }, + }, + source: []byte(s), + } + if !reflect.DeepEqual(c, e) { + t.Errorf("Config = %v expected %v", spew.Sdump(c), spew.Sdump(e)) + } + }) } func TestEnv_IsProductionEnvironment(t *testing.T) { @@ -224,3 +257,77 @@ func TestEnv_IsDeployableRef(t *testing.T) { } }) } + +func TestEnv_IsFreezed(t *testing.T) { + t.Run("Return true when the time is in the window", func(t *testing.T) { + runs := []struct { + t time.Time + e *Env + want bool + }{ + { + t: time.Date(2012, 12, 1, 23, 55, 10, 0, time.UTC), + e: &Env{ + FreezeWindows: []FreezeWindow{ + { + Start: "55 23 * Dec *", + Duration: "10m", + }, + }, + }, + want: true, + }, + { + t: time.Date(2012, 1, 1, 0, 3, 0, 0, time.UTC), + e: &Env{ + FreezeWindows: []FreezeWindow{ + { + Start: "55 23 * Dec *", + Duration: "10m", + }, + }, + }, + want: true, + }, + } + e := &Env{ + FreezeWindows: []FreezeWindow{ + { + Start: "55 23 * Dec *", + Duration: "10m", + }, + }, + } + + for _, r := range runs { + freezed, err := e.IsFreezed(r.t) + if err != nil { + t.Fatalf("IsFreezed returns an error: %s", err) + } + + if freezed != r.want { + t.Fatalf("IsFreezed = %v, wanted %v", freezed, r.want) + } + } + }) + + t.Run("Return false when the time is out of the window", func(t *testing.T) { + e := &Env{ + FreezeWindows: []FreezeWindow{ + { + Start: "55 23 * Dec *", + Duration: "10m", + }, + }, + } + + freezed, err := e.IsFreezed(time.Date(2012, 1, 1, 0, 10, 0, 0, time.UTC)) + if err != nil { + t.Fatalf("IsFreezed returns an error: %s", err) + } + + if freezed != false { + t.Fatalf("IsFreezed = %v, wanted %v", freezed, false) + } + }) +} From 74046d4e4206f8ee5af016ac4c47d048cd510784 Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 26 Dec 2021 11:12:04 +0900 Subject: [PATCH 2/8] Add the verfication for a frozen window --- internal/interactor/deployment.go | 29 +++++++++++++++++++---------- pkg/e/code.go | 2 ++ pkg/e/trans.go | 2 ++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/internal/interactor/deployment.go b/internal/interactor/deployment.go index ef0827b8..d49ab7b1 100644 --- a/internal/interactor/deployment.go +++ b/internal/interactor/deployment.go @@ -33,7 +33,7 @@ func (i *Interactor) IsApproved(ctx context.Context, d *ent.Deployment) bool { } func (i *Interactor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *extent.Env) (*ent.Deployment, error) { - if ok, err := i.isDeployable(ctx, u, r, d, env); !ok { + if err := i.isDeployable(ctx, u, r, d, env); err != nil { return nil, err } @@ -124,7 +124,7 @@ func (i *Interactor) DeployToRemote(ctx context.Context, u *ent.User, r *ent.Rep ) } - if ok, err := i.isDeployable(ctx, u, r, d, env); !ok { + if err := i.isDeployable(ctx, u, r, d, env); err != nil { return nil, err } @@ -171,21 +171,30 @@ 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 *extent.Env) (bool, error) { - if ok, err := env.IsDeployableRef(d.Ref); err != nil { - return false, err - } else if !ok { - return false, e.NewErrorWithMessage(e.ErrorCodeEntityUnprocessable, "The ref is not matched with 'deployable_ref'.", nil) +func (i *Interactor) isDeployable(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *extent.Env) error { + // Skip verifications for roll back. + if !d.IsRollback { + if ok, err := env.IsDeployableRef(d.Ref); !ok { + return e.NewErrorWithMessage(e.ErrorCodeEntityUnprocessable, "The ref is not matched with 'deployable_ref'.", nil) + } else if err != nil { + return err + } } // Check that the environment is locked. if locked, err := i.Store.HasLockOfRepoForEnv(ctx, r, d.Env); locked { - return false, e.NewError(e.ErrorCodeDeploymentLocked, err) + return e.NewError(e.ErrorCodeDeploymentLocked, err) + } else if err != nil { + return err + } + + if freezed, err := env.IsFreezed(time.Now().UTC()); freezed { + return e.NewError(e.ErrorCodeDeploymentFreezed, err) } else if err != nil { - return false, e.NewError(e.ErrorCodeInternalError, err) + return err } - return true, nil + return nil } func (i *Interactor) runClosingInactiveDeployment(stop <-chan struct{}) { diff --git a/pkg/e/code.go b/pkg/e/code.go index 50b54096..c0326652 100644 --- a/pkg/e/code.go +++ b/pkg/e/code.go @@ -17,6 +17,8 @@ const ( ErrorCodeDeploymentInvalid ErrorCode = "deployment_invalid" // ErrorCodeDeploymentLocked is when the environment is locked. ErrorCodeDeploymentLocked ErrorCode = "deployment_locked" + // ErrorCodeDeploymentFreezed is when the time in in the freeze window. + ErrorCodeDeploymentFreezed ErrorCode = "deployment_freezed" // ErrorCodeDeploymentUnapproved is when the deployment is not approved. ErrorCodeDeploymentNotApproved ErrorCode = "deployment_not_approved" // ErrorCodeDeploymentStatusNotWaiting is the status must be 'waiting' to create a remote deployment. diff --git a/pkg/e/trans.go b/pkg/e/trans.go index 088747cd..0a91eb00 100644 --- a/pkg/e/trans.go +++ b/pkg/e/trans.go @@ -8,6 +8,7 @@ var messages = map[ErrorCode]string{ ErrorCodeDeploymentConflict: "The conflict occurs, please retry.", ErrorCodeDeploymentInvalid: "The validation has failed.", ErrorCodeDeploymentLocked: "The environment is locked.", + ErrorCodeDeploymentFreezed: "It is in the deploy frozen window.", ErrorCodeDeploymentNotApproved: "The deployment is not approved", ErrorCodeDeploymentStatusInvalid: "The deployment status is invalid", ErrorCodeEntityNotFound: "It is not found.", @@ -35,6 +36,7 @@ var httpCodes = map[ErrorCode]int{ ErrorCodeDeploymentConflict: http.StatusUnprocessableEntity, ErrorCodeDeploymentInvalid: http.StatusUnprocessableEntity, ErrorCodeDeploymentLocked: http.StatusUnprocessableEntity, + ErrorCodeDeploymentFreezed: http.StatusUnprocessableEntity, ErrorCodeDeploymentNotApproved: http.StatusUnprocessableEntity, ErrorCodeDeploymentStatusInvalid: http.StatusUnprocessableEntity, ErrorCodeEntityNotFound: http.StatusNotFound, From 46d863b9c397e3afd1cd46d962220497d3132de0 Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 26 Dec 2021 11:12:25 +0900 Subject: [PATCH 3/8] Add comments for deployment.go --- internal/interactor/deployment.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/interactor/deployment.go b/internal/interactor/deployment.go index d49ab7b1..708509ba 100644 --- a/internal/interactor/deployment.go +++ b/internal/interactor/deployment.go @@ -14,6 +14,9 @@ import ( "go.uber.org/zap" ) +// IsApproved verifies that the request is approved or not. +// It is approved if there is an approval of reviews at least, but +// it is rejected if there is a reject of reviews. func (i *Interactor) IsApproved(ctx context.Context, d *ent.Deployment) bool { rvs, _ := i.Store.ListReviews(ctx, d) @@ -32,6 +35,10 @@ func (i *Interactor) IsApproved(ctx context.Context, d *ent.Deployment) bool { return false } +// Deploy posts a new deployment to SCM with the payload. +// But if it requires a review, it saves the payload on the DB +// and waits until reviewed. +// It returns an error for a undeployable payload. func (i *Interactor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *extent.Env) (*ent.Deployment, error) { if err := i.isDeployable(ctx, u, r, d, env); err != nil { return nil, err @@ -114,7 +121,9 @@ func (i *Interactor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *en return d, nil } -// DeployToRemote create a new remote deployment after the deployment was approved. +// DeployToRemote posts a new deployment to SCM with the saved payload +// after review has finished. +// It returns an error for a undeployable payload. func (i *Interactor) DeployToRemote(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *extent.Env) (*ent.Deployment, error) { if d.Status != deployment.StatusWaiting { return nil, e.NewErrorWithMessage( From 784214fcea539c5f7477a329d562d23e415ef2e2 Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 26 Dec 2021 11:17:09 +0900 Subject: [PATCH 4/8] Fix the name of field --- model/extent/config.go | 14 +++++++------- model/extent/config_test.go | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/model/extent/config.go b/model/extent/config.go index b64076b0..6fab0ecf 100644 --- a/model/extent/config.go +++ b/model/extent/config.go @@ -1,7 +1,6 @@ package extent import ( - "fmt" "regexp" "strconv" "strings" @@ -43,7 +42,8 @@ type ( // It is disabled when it is empty. Review *Review `json:"review,omitempty" yaml:"review"` - FreezeWindows []FreezeWindow `json:"freeze_windows" yaml:"freeze_windows"` + // FrozenWindows is the list of windows to freeze deployments. + FrozenWindows []FrozenWindow `json:"frozen_windows" yaml:"frozen_windows"` } Review struct { @@ -51,7 +51,7 @@ type ( Reviewers []string `json:"reviewers" yaml:"reviewers"` } - FreezeWindow struct { + FrozenWindow struct { Start string `json:"start" yaml:"start"` Duration string `json:"duration" yaml:"duration"` Location string `json:"location" yaml:"location"` @@ -184,16 +184,16 @@ func (e *Env) HasReview() bool { // IsFreezed verifies whether the current time is in a freeze window. // It returns an error when parsing an expression is failed. func (e *Env) IsFreezed(t time.Time) (bool, error) { - if len(e.FreezeWindows) == 0 { + if len(e.FrozenWindows) == 0 { return false, nil } - for _, w := range e.FreezeWindows { + for _, w := range e.FrozenWindows { s, err := cronexpr.ParseInLocation(strings.TrimSpace(w.Start), w.Location) if err != nil { return false, eutil.NewErrorWithMessage( eutil.ErrorCodeConfigInvalid, - fmt.Sprintf("The crontab expression of the freeze window is invalid."), + "The crontab expression of the freeze window is invalid.", err, ) } @@ -202,7 +202,7 @@ func (e *Env) IsFreezed(t time.Time) (bool, error) { if err != nil { return false, eutil.NewErrorWithMessage( eutil.ErrorCodeConfigInvalid, - fmt.Sprintf("The duration of the freeze window is invalid."), + "The duration of the freeze window is invalid.", err, ) } diff --git a/model/extent/config_test.go b/model/extent/config_test.go index 285aeddf..98045f23 100644 --- a/model/extent/config_test.go +++ b/model/extent/config_test.go @@ -172,7 +172,7 @@ envs: Envs: []*Env{ { Name: "dev", - FreezeWindows: []FreezeWindow{ + FrozenWindows: []FrozenWindow{ { Start: "55 23 * * *", Duration: "10m", @@ -268,7 +268,7 @@ func TestEnv_IsFreezed(t *testing.T) { { t: time.Date(2012, 12, 1, 23, 55, 10, 0, time.UTC), e: &Env{ - FreezeWindows: []FreezeWindow{ + FrozenWindows: []FrozenWindow{ { Start: "55 23 * Dec *", Duration: "10m", @@ -280,7 +280,7 @@ func TestEnv_IsFreezed(t *testing.T) { { t: time.Date(2012, 1, 1, 0, 3, 0, 0, time.UTC), e: &Env{ - FreezeWindows: []FreezeWindow{ + FrozenWindows: []FrozenWindow{ { Start: "55 23 * Dec *", Duration: "10m", @@ -291,7 +291,7 @@ func TestEnv_IsFreezed(t *testing.T) { }, } e := &Env{ - FreezeWindows: []FreezeWindow{ + FrozenWindows: []FrozenWindow{ { Start: "55 23 * Dec *", Duration: "10m", @@ -313,7 +313,7 @@ func TestEnv_IsFreezed(t *testing.T) { t.Run("Return false when the time is out of the window", func(t *testing.T) { e := &Env{ - FreezeWindows: []FreezeWindow{ + FrozenWindows: []FrozenWindow{ { Start: "55 23 * Dec *", Duration: "10m", From afdcb1cec9e75d3f141e0496f3787b79a90501c8 Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 26 Dec 2021 12:15:00 +0900 Subject: [PATCH 5/8] Add documentations --- docs/concepts/deploy.yml.md | 18 ++++++++++++++++++ docs/references/deploy.yml.md | 31 ++++++++++++++++++++----------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/docs/concepts/deploy.yml.md b/docs/concepts/deploy.yml.md index cd60e143..3d8e9bcb 100644 --- a/docs/concepts/deploy.yml.md +++ b/docs/concepts/deploy.yml.md @@ -62,6 +62,24 @@ envs: production_environment: true ``` +### Deploy Freeze Window + +Gitploy support to add a window to prevent unintended deployment for the environment. You can freeze a window periodically by a cron expression. + +```yaml +envs: + - name: production + frozen_windows: + # Freeze every midnights + - start: "50 23 * * *" + duration: 20m + location: America/New_York + # Freeze every weekends + - start: "0 * * * SAT,SUN" + duration: 1h + location: Asia/Seoul +``` + ### Review 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. diff --git a/docs/references/deploy.yml.md b/docs/references/deploy.yml.md index a6b2e15c..9a32685d 100644 --- a/docs/references/deploy.yml.md +++ b/docs/references/deploy.yml.md @@ -11,22 +11,31 @@ Field |Type |Required |Description Field |Type |Required |Description --- |---- |--- |--- `name` |*string* |`true` |This field is the runtime environment such as `production`, `staging`, and `qa`. -`task` |*string* |`false` |This field is used by the deployment system to distinguish the kind of deployment. -`description` |*string* |`false` |This field is the short description of the deployment. -`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`. -`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. +`task` |*string* |`false` |This field is used by the deployment system to distinguish the kind of deployment. (*Only for `GitHub`*) +`description` |*string* |`false` |This field is the short description of the deployment. (*Only for `GitHub`*) +`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`*) +`required_contexts` |*[]string* |`false` |This field allows you to specify a subset of contexts that must be success. (*Only for `GitHub`*) +`payload` |*object* or *string* |`false` |This field is JSON payload with extra information about the deployment. (*Only for `GitHub`*) `production_environment` |*boolean* |`false` |This field specifies whether this runtime environment is production or not. `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))). `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.*` -`review` |*[Review](#review)* |`false` |This field configures review. +`review` |*[Review](#review)* |`false` |This field configures reviewers. +`frozen_windows` |*[][Frozen Window](#frozen-window)* |`false` |This field configures to add a frozen window to prevent unintended deployment for the environment. ## Review -Field |Type |Tag |Description ---- |--- |--- |--- -`enabled` |*boolean* |`true` |This field make to enable the review feature. The default value is `false`. -`reviewers` |*[]string* |`false` |This field list up reviewers. The default value is `[]`. You should specify maintainers of the project. +Field |Type |Required |Description +--- |--- |--- |--- +`enabled` |*boolean* |`false` |This field makes to enables the review feature. The default value is `false`. +`reviewers` |*[]string* |`false` |This field list up reviewers. The default value is `[]`. You should specify the maintainers of the project. + +## Frozen Window + +Field |Type |Required |Description +--- |--- |--- |--- +`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. +`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`. +`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. ## Variables @@ -36,7 +45,7 @@ The following variables are available in `${ }` syntax when evaluating `deploy.y * `GITPLOY_ROLLBACK_TASK`: Returns `rollback` for rollback, but deploy, it returns the empty string. * `GITPLOY_IS_ROLLBACK`: Returns `true` for rollback, but deploy, it returns `false`. -An example usage of this: +Example usage of this: ```yaml envs: From fb5fb58a2f1f4025ef2890badf1f61952b4c3a08 Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 26 Dec 2021 12:15:35 +0900 Subject: [PATCH 6/8] Fix test --- model/extent/config_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model/extent/config_test.go b/model/extent/config_test.go index 98045f23..34c076d4 100644 --- a/model/extent/config_test.go +++ b/model/extent/config_test.go @@ -155,11 +155,11 @@ envs: } }) - t.Run("Unmarshal the freeze_windows field", func(t *testing.T) { + t.Run("Unmarshal the frozen_windows field", func(t *testing.T) { s := ` envs: - name: dev - freeze_windows: + frozen_windows: - start: "55 23 * * *" duration: "10m"` From 50def9260db228eb60620db1e6338717ae1a2296 Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 26 Dec 2021 15:28:09 +0900 Subject: [PATCH 7/8] Upgrade the version --- go.mod | 2 +- go.sum | 2 ++ pkg/e/trans.go | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index e6048dc7..e5e18692 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gin-contrib/cors v1.3.1 github.com/gin-contrib/sse v0.1.0 github.com/gin-gonic/gin v1.7.7 - github.com/gitploy-io/cronexpr v0.2.1 + github.com/gitploy-io/cronexpr v0.2.2 github.com/go-sql-driver/mysql v1.5.1-0.20200311113236-681ffa848bae github.com/golang/mock v1.6.0 github.com/google/go-github/v32 v32.1.0 diff --git a/go.sum b/go.sum index ceb43b4f..ae9a9ffe 100644 --- a/go.sum +++ b/go.sum @@ -108,6 +108,8 @@ github.com/gitploy-io/cronexpr v0.2.0 h1:BmYX3+QNB11Bevp2z98taCOiNDhMo/E1pBeO6Oz github.com/gitploy-io/cronexpr v0.2.0/go.mod h1:Uep5sbzUSocMZvJ1s0lNI9zi37s5iUI1llkw3vRGK9M= github.com/gitploy-io/cronexpr v0.2.1 h1:usx6GTAQh2q3E4S8jx5RWGGkr4LSxLH0mBcebGZGv+c= github.com/gitploy-io/cronexpr v0.2.1/go.mod h1:Uep5sbzUSocMZvJ1s0lNI9zi37s5iUI1llkw3vRGK9M= +github.com/gitploy-io/cronexpr v0.2.2 h1:Au+wK6FqmOLAF7AkW6q4gnrNXTe3rEW97XFZ4chy0xs= +github.com/gitploy-io/cronexpr v0.2.2/go.mod h1:Uep5sbzUSocMZvJ1s0lNI9zi37s5iUI1llkw3vRGK9M= github.com/go-bindata/go-bindata v1.0.1-0.20190711162640-ee3c2418e368/go.mod h1:7xCgX1lzlrXPHkfvn3EhumqHkmSlzt8at9q7v0ax19c= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff --git a/pkg/e/trans.go b/pkg/e/trans.go index 0a91eb00..a41be0a8 100644 --- a/pkg/e/trans.go +++ b/pkg/e/trans.go @@ -8,7 +8,7 @@ var messages = map[ErrorCode]string{ ErrorCodeDeploymentConflict: "The conflict occurs, please retry.", ErrorCodeDeploymentInvalid: "The validation has failed.", ErrorCodeDeploymentLocked: "The environment is locked.", - ErrorCodeDeploymentFreezed: "It is in the deploy frozen window.", + ErrorCodeDeploymentFreezed: "It is in the deploy freeze window.", ErrorCodeDeploymentNotApproved: "The deployment is not approved", ErrorCodeDeploymentStatusInvalid: "The deployment status is invalid", ErrorCodeEntityNotFound: "It is not found.", From e31942798e829061ecd7e59f67bb92181f044e37 Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 26 Dec 2021 15:38:27 +0900 Subject: [PATCH 8/8] Fix the typo --- internal/interactor/deployment.go | 2 +- pkg/e/code.go | 4 ++-- pkg/e/trans.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/interactor/deployment.go b/internal/interactor/deployment.go index 708509ba..c6a4ba7b 100644 --- a/internal/interactor/deployment.go +++ b/internal/interactor/deployment.go @@ -198,7 +198,7 @@ func (i *Interactor) isDeployable(ctx context.Context, u *ent.User, r *ent.Repo, } if freezed, err := env.IsFreezed(time.Now().UTC()); freezed { - return e.NewError(e.ErrorCodeDeploymentFreezed, err) + return e.NewError(e.ErrorCodeDeploymentFrozen, err) } else if err != nil { return err } diff --git a/pkg/e/code.go b/pkg/e/code.go index c0326652..0f0a739f 100644 --- a/pkg/e/code.go +++ b/pkg/e/code.go @@ -17,8 +17,8 @@ const ( ErrorCodeDeploymentInvalid ErrorCode = "deployment_invalid" // ErrorCodeDeploymentLocked is when the environment is locked. ErrorCodeDeploymentLocked ErrorCode = "deployment_locked" - // ErrorCodeDeploymentFreezed is when the time in in the freeze window. - ErrorCodeDeploymentFreezed ErrorCode = "deployment_freezed" + // ErrorCodeDeploymentFrozen is when the time in in the freeze window. + ErrorCodeDeploymentFrozen ErrorCode = "deployment_frozen" // ErrorCodeDeploymentUnapproved is when the deployment is not approved. ErrorCodeDeploymentNotApproved ErrorCode = "deployment_not_approved" // ErrorCodeDeploymentStatusNotWaiting is the status must be 'waiting' to create a remote deployment. diff --git a/pkg/e/trans.go b/pkg/e/trans.go index a41be0a8..7e570403 100644 --- a/pkg/e/trans.go +++ b/pkg/e/trans.go @@ -8,7 +8,7 @@ var messages = map[ErrorCode]string{ ErrorCodeDeploymentConflict: "The conflict occurs, please retry.", ErrorCodeDeploymentInvalid: "The validation has failed.", ErrorCodeDeploymentLocked: "The environment is locked.", - ErrorCodeDeploymentFreezed: "It is in the deploy freeze window.", + ErrorCodeDeploymentFrozen: "It is in the deploy freeze window.", ErrorCodeDeploymentNotApproved: "The deployment is not approved", ErrorCodeDeploymentStatusInvalid: "The deployment status is invalid", ErrorCodeEntityNotFound: "It is not found.", @@ -36,7 +36,7 @@ var httpCodes = map[ErrorCode]int{ ErrorCodeDeploymentConflict: http.StatusUnprocessableEntity, ErrorCodeDeploymentInvalid: http.StatusUnprocessableEntity, ErrorCodeDeploymentLocked: http.StatusUnprocessableEntity, - ErrorCodeDeploymentFreezed: http.StatusUnprocessableEntity, + ErrorCodeDeploymentFrozen: http.StatusUnprocessableEntity, ErrorCodeDeploymentNotApproved: http.StatusUnprocessableEntity, ErrorCodeDeploymentStatusInvalid: http.StatusUnprocessableEntity, ErrorCodeEntityNotFound: http.StatusNotFound,