diff --git a/model/extent/config.go b/model/extent/config.go index 30cd5975..038a4ac9 100644 --- a/model/extent/config.go +++ b/model/extent/config.go @@ -1,13 +1,9 @@ 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" @@ -22,45 +18,6 @@ type ( source []byte } - Env struct { - Name string `json:"name" yaml:"name"` - - // GitHub parameters of deployment. - Task *string `json:"task" yaml:"task"` - Description *string `json:"description" yaml:"description"` - AutoMerge *bool `json:"auto_merge" yaml:"auto_merge"` - RequiredContexts *[]string `json:"required_contexts,omitempty" yaml:"required_contexts"` - Payload interface{} `json:"payload" yaml:"payload"` - ProductionEnvironment *bool `json:"production_environment" yaml:"production_environment"` - - // DeployableRef validates the ref is deployable or not. - DeployableRef *string `json:"deployable_ref" yaml:"deployable_ref"` - - // AutoDeployOn deploys automatically when the pattern is matched. - AutoDeployOn *string `json:"auto_deploy_on" yaml:"auto_deploy_on"` - - // Serialization verify if there is a running deployment. - Serialization *bool `json:"serialization" yaml:"serialization"` - - // 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 } @@ -146,78 +103,3 @@ func (c *Config) GetEnv(name string) *Env { return nil } - -// IsProductionEnvironment verifies whether the environment is production or not. -func (e *Env) IsProductionEnvironment() bool { - return e.ProductionEnvironment != nil && *e.ProductionEnvironment -} - -// IsDeployableRef verifies the ref is deployable. -func (e *Env) IsDeployableRef(ref string) (bool, error) { - if e.DeployableRef == nil { - return true, nil - } - - matched, err := regexp.MatchString(*e.DeployableRef, ref) - if err != nil { - return false, eutil.NewError(eutil.ErrorCodeConfigInvalid, err) - } - - return matched, nil -} - -// 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 - } - - matched, err := regexp.MatchString(*e.AutoDeployOn, ref) - if err != nil { - return false, eutil.NewError(eutil.ErrorCodeConfigInvalid, err) - } - - return matched, nil -} - -// HasReview check whether the review is enabled or not. -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 -} diff --git a/model/extent/config_test.go b/model/extent/config_test.go index 34c076d4..ad267e3c 100644 --- a/model/extent/config_test.go +++ b/model/extent/config_test.go @@ -3,7 +3,6 @@ package extent import ( "reflect" "testing" - "time" "github.com/AlekSi/pointer" "github.com/davecgh/go-spew/spew" @@ -187,147 +186,3 @@ envs: } }) } - -func TestEnv_IsProductionEnvironment(t *testing.T) { - t.Run("Reutrn false when the production environment is nil", func(t *testing.T) { - e := &Env{} - - expected := false - if e.IsProductionEnvironment() != expected { - t.Errorf("IsProductionEnvironment = %v, wanted %v", e.IsProductionEnvironment(), expected) - } - }) - - t.Run("Reutrn true when the production environment is true", func(t *testing.T) { - e := &Env{ - ProductionEnvironment: pointer.ToBool(true), - } - - expected := true - if e.IsProductionEnvironment() != expected { - t.Errorf("IsProductionEnvironment = %v, wanted %v", e.IsProductionEnvironment(), expected) - } - }) -} - -func TestEnv_IsDeployableRef(t *testing.T) { - t.Run("Return true when 'deployable_ref' is not defined.", func(t *testing.T) { - e := &Env{} - - ret, err := e.IsDeployableRef("") - if err != nil { - t.Fatalf("IsDeployableRef returns an error: %s", err) - } - - expected := true - if ret != expected { - t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected) - } - }) - - t.Run("Return true when 'deployable_ref' is matched.", func(t *testing.T) { - e := &Env{ - DeployableRef: pointer.ToString("main"), - } - - ret, err := e.IsDeployableRef("main") - if err != nil { - t.Fatalf("IsDeployableRef returns an error: %s", err) - } - - expected := true - if ret != expected { - t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected) - } - }) - - t.Run("Return false when 'deployable_ref' is not matched.", func(t *testing.T) { - e := &Env{ - DeployableRef: pointer.ToString("main"), - } - - ret, err := e.IsDeployableRef("branch") - if err != nil { - t.Fatalf("IsDeployableRef returns an error: %s", err) - } - - expected := false - if ret != expected { - t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected) - } - }) -} - -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{ - FrozenWindows: []FrozenWindow{ - { - Start: "55 23 * Dec *", - Duration: "10m", - }, - }, - }, - want: true, - }, - { - t: time.Date(2012, 1, 1, 0, 3, 0, 0, time.UTC), - e: &Env{ - FrozenWindows: []FrozenWindow{ - { - Start: "55 23 * Dec *", - Duration: "10m", - }, - }, - }, - want: true, - }, - } - e := &Env{ - FrozenWindows: []FrozenWindow{ - { - 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{ - FrozenWindows: []FrozenWindow{ - { - 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) - } - }) -} diff --git a/model/extent/env.go b/model/extent/env.go new file mode 100644 index 00000000..e04cac23 --- /dev/null +++ b/model/extent/env.go @@ -0,0 +1,126 @@ +package extent + +import ( + "regexp" + "strings" + "time" + + "github.com/gitploy-io/cronexpr" + eutil "github.com/gitploy-io/gitploy/pkg/e" +) + +type ( + Env struct { + Name string `json:"name" yaml:"name"` + + // GitHub parameters of deployment. + Task *string `json:"task" yaml:"task"` + Description *string `json:"description" yaml:"description"` + AutoMerge *bool `json:"auto_merge" yaml:"auto_merge"` + RequiredContexts *[]string `json:"required_contexts,omitempty" yaml:"required_contexts"` + Payload interface{} `json:"payload" yaml:"payload"` + ProductionEnvironment *bool `json:"production_environment" yaml:"production_environment"` + + // DeployableRef validates the ref is deployable or not. + DeployableRef *string `json:"deployable_ref" yaml:"deployable_ref"` + + // AutoDeployOn deploys automatically when the pattern is matched. + AutoDeployOn *string `json:"auto_deploy_on" yaml:"auto_deploy_on"` + + // Serialization verify if there is a running deployment. + Serialization *bool `json:"serialization" yaml:"serialization"` + + // 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"` + } +) + +// IsProductionEnvironment verifies whether the environment is production or not. +func (e *Env) IsProductionEnvironment() bool { + return e.ProductionEnvironment != nil && *e.ProductionEnvironment +} + +// IsDeployableRef verifies the ref is deployable. +func (e *Env) IsDeployableRef(ref string) (bool, error) { + if e.DeployableRef == nil { + return true, nil + } + + matched, err := regexp.MatchString(*e.DeployableRef, ref) + if err != nil { + return false, eutil.NewError(eutil.ErrorCodeConfigInvalid, err) + } + + return matched, nil +} + +// 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 + } + + matched, err := regexp.MatchString(*e.AutoDeployOn, ref) + if err != nil { + return false, eutil.NewError(eutil.ErrorCodeConfigInvalid, err) + } + + return matched, nil +} + +// HasReview check whether the review is enabled or not. +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 +} diff --git a/model/extent/env_test.go b/model/extent/env_test.go new file mode 100644 index 00000000..2e0f6632 --- /dev/null +++ b/model/extent/env_test.go @@ -0,0 +1,152 @@ +package extent + +import ( + "testing" + "time" + + "github.com/AlekSi/pointer" +) + +func TestEnv_IsProductionEnvironment(t *testing.T) { + t.Run("Reutrn false when the production environment is nil", func(t *testing.T) { + e := &Env{} + + expected := false + if e.IsProductionEnvironment() != expected { + t.Errorf("IsProductionEnvironment = %v, wanted %v", e.IsProductionEnvironment(), expected) + } + }) + + t.Run("Reutrn true when the production environment is true", func(t *testing.T) { + e := &Env{ + ProductionEnvironment: pointer.ToBool(true), + } + + expected := true + if e.IsProductionEnvironment() != expected { + t.Errorf("IsProductionEnvironment = %v, wanted %v", e.IsProductionEnvironment(), expected) + } + }) +} + +func TestEnv_IsDeployableRef(t *testing.T) { + t.Run("Return true when 'deployable_ref' is not defined.", func(t *testing.T) { + e := &Env{} + + ret, err := e.IsDeployableRef("") + if err != nil { + t.Fatalf("IsDeployableRef returns an error: %s", err) + } + + expected := true + if ret != expected { + t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected) + } + }) + + t.Run("Return true when 'deployable_ref' is matched.", func(t *testing.T) { + e := &Env{ + DeployableRef: pointer.ToString("main"), + } + + ret, err := e.IsDeployableRef("main") + if err != nil { + t.Fatalf("IsDeployableRef returns an error: %s", err) + } + + expected := true + if ret != expected { + t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected) + } + }) + + t.Run("Return false when 'deployable_ref' is not matched.", func(t *testing.T) { + e := &Env{ + DeployableRef: pointer.ToString("main"), + } + + ret, err := e.IsDeployableRef("branch") + if err != nil { + t.Fatalf("IsDeployableRef returns an error: %s", err) + } + + expected := false + if ret != expected { + t.Fatalf("IsDeployableRef = %v, wanted %v", ret, expected) + } + }) +} + +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{ + FrozenWindows: []FrozenWindow{ + { + Start: "55 23 * Dec *", + Duration: "10m", + }, + }, + }, + want: true, + }, + { + t: time.Date(2012, 1, 1, 0, 3, 0, 0, time.UTC), + e: &Env{ + FrozenWindows: []FrozenWindow{ + { + Start: "55 23 * Dec *", + Duration: "10m", + }, + }, + }, + want: true, + }, + } + e := &Env{ + FrozenWindows: []FrozenWindow{ + { + 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{ + FrozenWindows: []FrozenWindow{ + { + 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) + } + }) +} \ No newline at end of file