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

Add a new feature to configure windows freezing deployment #283

Merged
merged 8 commits into from
Dec 26, 2021
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
18 changes: 18 additions & 0 deletions docs/concepts/deploy.yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 20 additions & 11 deletions docs/references/deploy.yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ 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/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=
Expand Down
40 changes: 29 additions & 11 deletions internal/interactor/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -32,8 +35,12 @@ 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 ok, err := i.isDeployable(ctx, u, r, d, env); !ok {
if err := i.isDeployable(ctx, u, r, d, env); err != nil {
return nil, err
}

Expand Down Expand Up @@ -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(
Expand All @@ -124,7 +133,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
}

Expand Down Expand Up @@ -171,21 +180,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 {
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 condition to skip verifications works only for deployment.

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 {
Copy link
Member Author

Choose a reason for hiding this comment

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

Verify the time is in the deploy freeze window or not.

return e.NewError(e.ErrorCodeDeploymentFrozen, 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{}) {
Expand Down
55 changes: 52 additions & 3 deletions model/extent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package extent
import (
"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"
Expand Down Expand Up @@ -38,13 +41,22 @@ type (
// Review is the configuration of Review,
// It is disabled when it is empty.
Review *Review `json:"review,omitempty" yaml:"review"`

// FrozenWindows is the list of windows to freeze deployments.
FrozenWindows []FrozenWindow `json:"frozen_windows" yaml:"frozen_windows"`
}

Review struct {
Enabled bool `json:"enabled" yaml:"enabled"`
Reviewers []string `json:"reviewers" yaml:"reviewers"`
}

FrozenWindow struct {
Start string `json:"start" yaml:"start"`
Duration string `json:"duration" yaml:"duration"`
Location string `json:"location" yaml:"location"`
}

EvalValues struct {
IsRollback bool
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.FrozenWindows) == 0 {
return false, nil
}

for _, w := range e.FrozenWindows {
s, err := cronexpr.ParseInLocation(strings.TrimSpace(w.Start), w.Location)
if err != nil {
return false, eutil.NewErrorWithMessage(
eutil.ErrorCodeConfigInvalid,
"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,
"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
}
Loading