diff --git a/go.mod b/go.mod index 98596837..b7120f0b 100644 --- a/go.mod +++ b/go.mod @@ -80,6 +80,8 @@ require ( golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.26.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index db3f0bd5..fd6d97ef 100644 --- a/go.sum +++ b/go.sum @@ -881,6 +881,7 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7 h1:M1gcVrIb2lSn2FIL19DG0+/b8nNVKJ7W7b4WcAGZAYM= golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/interactor/deployment.go b/internal/interactor/deployment.go index 28f7ca4a..c1ae9532 100644 --- a/internal/interactor/deployment.go +++ b/internal/interactor/deployment.go @@ -71,31 +71,37 @@ type ( // Deploy posts a new deployment to SCM with the payload. // But if it requires a review, it saves the payload on the store and waits until reviewed. // It returns an error for a undeployable payload. -func (i *DeploymentInteractor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *extent.Env) (*ent.Deployment, error) { +func (i *DeploymentInteractor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, t *ent.Deployment, env *extent.Env) (*ent.Deployment, error) { number, err := i.store.GetNextDeploymentNumberOfRepo(ctx, r) if err != nil { return nil, e.NewError(e.ErrorCodeInternalError, err) } i.log.Debug("Get the next number, and build the deployment.") - d = &ent.Deployment{ + d := &ent.Deployment{ Number: number, - Type: d.Type, - Env: d.Env, - Ref: d.Ref, + Type: t.Type, + Env: t.Env, + Ref: t.Ref, Status: deployment.DefaultStatus, ProductionEnvironment: env.IsProductionEnvironment(), - IsRollback: d.IsRollback, + IsRollback: t.IsRollback, UserID: u.ID, RepoID: r.ID, } + if env.IsDynamicPayloadEnabled() { + i.log.Debug("Set the dynamic payload.") + d.DynamicPayload = t.DynamicPayload + } + i.log.Debug("Validate the deployment before a request.") v := NewDeploymentValidator([]Validator{ &RefValidator{Env: env}, &FrozenWindowValidator{Env: env}, &LockValidator{Repo: r, Store: i.store}, &SerializationValidator{Env: env, Store: i.store}, + &DynamicPayloadValidator{Env: env}, }) if err := v.Validate(d); err != nil { return nil, err diff --git a/internal/interactor/validator.go b/internal/interactor/validator.go index 521ef7b0..1cbb7574 100644 --- a/internal/interactor/validator.go +++ b/internal/interactor/validator.go @@ -168,3 +168,17 @@ func (v *SerializationValidator) Validate(d *ent.Deployment) error { return err } + +// DynamicPayloadValidator validate the payload with +// the specifications defined in the configuration file. +type DynamicPayloadValidator struct { + Env *extent.Env +} + +func (v *DynamicPayloadValidator) Validate(d *ent.Deployment) error { + if !v.Env.IsDynamicPayloadEnabled() { + return nil + } + + return v.Env.ValidateDynamicPayload(d.DynamicPayload) +} diff --git a/internal/pkg/github/deployment.go b/internal/pkg/github/deployment.go index b36c442c..1ee57637 100644 --- a/internal/pkg/github/deployment.go +++ b/internal/pkg/github/deployment.go @@ -13,6 +13,12 @@ import ( ) func (g *Github) CreateRemoteDeployment(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *extent.Env) (*extent.RemoteDeployment, error) { + // If there is a dynamic payload, set it as the payload. + payload := env.Payload + if d.DynamicPayload != nil { + payload = d.DynamicPayload + } + gd, res, err := g.Client(ctx, u.Token). Repositories. CreateDeployment(ctx, r.Namespace, r.Name, &github.DeploymentRequest{ @@ -22,7 +28,7 @@ func (g *Github) CreateRemoteDeployment(ctx context.Context, u *ent.User, r *ent Description: env.Description, AutoMerge: env.AutoMerge, RequiredContexts: env.RequiredContexts, - Payload: env.Payload, + Payload: payload, ProductionEnvironment: env.ProductionEnvironment, }) if res.StatusCode == http.StatusConflict { diff --git a/internal/pkg/store/deployment.go b/internal/pkg/store/deployment.go index f6dd2a63..4c240378 100644 --- a/internal/pkg/store/deployment.go +++ b/internal/pkg/store/deployment.go @@ -265,6 +265,7 @@ func (s *Store) CreateDeployment(ctx context.Context, d *ent.Deployment) (*ent.D SetType(d.Type). SetRef(d.Ref). SetEnv(d.Env). + SetDynamicPayload(d.DynamicPayload). SetUID(d.UID). SetSha(d.Sha). SetHTMLURL(d.HTMLURL). @@ -277,10 +278,7 @@ func (s *Store) CreateDeployment(ctx context.Context, d *ent.Deployment) (*ent.D if ent.IsConstraintError(err) { return nil, e.NewError(e.ErrorCodeDeploymentConflict, err) } else if ent.IsValidationError(err) { - return nil, e.NewErrorWithMessage( - e.ErrorCodeEntityUnprocessable, - fmt.Sprintf("Failed to create a deployment. The value of \"%s\" field is invalid.", err.(*ent.ValidationError).Name), - err) + return nil, e.NewErrorWithMessage(e.ErrorCodeEntityUnprocessable, fmt.Sprintf("Failed to create a deployment. The value of \"%s\" field is invalid.", err.(*ent.ValidationError).Name), err) } else if err != nil { return nil, e.NewError(e.ErrorCodeInternalError, err) } diff --git a/internal/server/api/v1/repos/deployment_create.go b/internal/server/api/v1/repos/deployment_create.go index 82176b8a..45d28480 100644 --- a/internal/server/api/v1/repos/deployment_create.go +++ b/internal/server/api/v1/repos/deployment_create.go @@ -16,9 +16,10 @@ import ( type ( DeploymentPostPayload struct { - Type string `json:"type"` - Ref string `json:"ref"` - Env string `json:"env"` + Type string `json:"type"` + Ref string `json:"ref"` + Env string `json:"env"` + DynamicPayload map[string]interface{} `json:"dynamic_payload"` } ) @@ -56,9 +57,10 @@ func (s *DeploymentAPI) Create(c *gin.Context) { d, err := s.i.Deploy(ctx, u, re, &ent.Deployment{ - Type: deployment.Type(p.Type), - Env: p.Env, - Ref: p.Ref, + Type: deployment.Type(p.Type), + Env: p.Env, + Ref: p.Ref, + DynamicPayload: p.DynamicPayload, }, env) if err != nil { diff --git a/model/ent/deployment.go b/model/ent/deployment.go index 671ce9af..13fed970 100644 --- a/model/ent/deployment.go +++ b/model/ent/deployment.go @@ -3,6 +3,7 @@ package ent import ( + "encoding/json" "fmt" "strings" "time" @@ -26,6 +27,8 @@ type Deployment struct { Env string `json:"env"` // Ref holds the value of the "ref" field. Ref string `json:"ref"` + // DynamicPayload holds the value of the "dynamic_payload" field. + DynamicPayload map[string]interface{} `json:"dynamic_payload,omitemtpy"` // Status holds the value of the "status" field. Status deployment.Status `json:"status"` // UID holds the value of the "uid" field. @@ -132,6 +135,8 @@ func (*Deployment) scanValues(columns []string) ([]interface{}, error) { values := make([]interface{}, len(columns)) for i := range columns { switch columns[i] { + case deployment.FieldDynamicPayload: + values[i] = new([]byte) case deployment.FieldProductionEnvironment, deployment.FieldIsRollback, deployment.FieldIsApprovalEnabled: values[i] = new(sql.NullBool) case deployment.FieldID, deployment.FieldNumber, deployment.FieldUID, deployment.FieldUserID, deployment.FieldRepoID, deployment.FieldRequiredApprovalCount: @@ -185,6 +190,14 @@ func (d *Deployment) assignValues(columns []string, values []interface{}) error } else if value.Valid { d.Ref = value.String } + case deployment.FieldDynamicPayload: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field dynamic_payload", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &d.DynamicPayload); err != nil { + return fmt.Errorf("unmarshal field dynamic_payload: %w", err) + } + } case deployment.FieldStatus: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field status", values[i]) @@ -320,6 +333,8 @@ func (d *Deployment) String() string { builder.WriteString(d.Env) builder.WriteString(", ref=") builder.WriteString(d.Ref) + builder.WriteString(", dynamic_payload=") + builder.WriteString(fmt.Sprintf("%v", d.DynamicPayload)) builder.WriteString(", status=") builder.WriteString(fmt.Sprintf("%v", d.Status)) builder.WriteString(", uid=") diff --git a/model/ent/deployment/deployment.go b/model/ent/deployment/deployment.go index 094708f1..340f4cff 100644 --- a/model/ent/deployment/deployment.go +++ b/model/ent/deployment/deployment.go @@ -20,6 +20,8 @@ const ( FieldEnv = "env" // FieldRef holds the string denoting the ref field in the database. FieldRef = "ref" + // FieldDynamicPayload holds the string denoting the dynamic_payload field in the database. + FieldDynamicPayload = "dynamic_payload" // FieldStatus holds the string denoting the status field in the database. FieldStatus = "status" // FieldUID holds the string denoting the uid field in the database. @@ -100,6 +102,7 @@ var Columns = []string{ FieldType, FieldEnv, FieldRef, + FieldDynamicPayload, FieldStatus, FieldUID, FieldSha, diff --git a/model/ent/deployment/where.go b/model/ent/deployment/where.go index ea74c3c3..3e70d11e 100644 --- a/model/ent/deployment/where.go +++ b/model/ent/deployment/where.go @@ -537,6 +537,20 @@ func RefContainsFold(v string) predicate.Deployment { }) } +// DynamicPayloadIsNil applies the IsNil predicate on the "dynamic_payload" field. +func DynamicPayloadIsNil() predicate.Deployment { + return predicate.Deployment(func(s *sql.Selector) { + s.Where(sql.IsNull(s.C(FieldDynamicPayload))) + }) +} + +// DynamicPayloadNotNil applies the NotNil predicate on the "dynamic_payload" field. +func DynamicPayloadNotNil() predicate.Deployment { + return predicate.Deployment(func(s *sql.Selector) { + s.Where(sql.NotNull(s.C(FieldDynamicPayload))) + }) +} + // StatusEQ applies the EQ predicate on the "status" field. func StatusEQ(v Status) predicate.Deployment { return predicate.Deployment(func(s *sql.Selector) { diff --git a/model/ent/deployment_create.go b/model/ent/deployment_create.go index d69c3848..fc56cc68 100644 --- a/model/ent/deployment_create.go +++ b/model/ent/deployment_create.go @@ -57,6 +57,12 @@ func (dc *DeploymentCreate) SetRef(s string) *DeploymentCreate { return dc } +// SetDynamicPayload sets the "dynamic_payload" field. +func (dc *DeploymentCreate) SetDynamicPayload(m map[string]interface{}) *DeploymentCreate { + dc.mutation.SetDynamicPayload(m) + return dc +} + // SetStatus sets the "status" field. func (dc *DeploymentCreate) SetStatus(d deployment.Status) *DeploymentCreate { dc.mutation.SetStatus(d) @@ -476,6 +482,14 @@ func (dc *DeploymentCreate) createSpec() (*Deployment, *sqlgraph.CreateSpec) { }) _node.Ref = value } + if value, ok := dc.mutation.DynamicPayload(); ok { + _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ + Type: field.TypeJSON, + Value: value, + Column: deployment.FieldDynamicPayload, + }) + _node.DynamicPayload = value + } if value, ok := dc.mutation.Status(); ok { _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ Type: field.TypeEnum, diff --git a/model/ent/deployment_update.go b/model/ent/deployment_update.go index 78ff3c03..2b11c847 100644 --- a/model/ent/deployment_update.go +++ b/model/ent/deployment_update.go @@ -72,6 +72,18 @@ func (du *DeploymentUpdate) SetRef(s string) *DeploymentUpdate { return du } +// SetDynamicPayload sets the "dynamic_payload" field. +func (du *DeploymentUpdate) SetDynamicPayload(m map[string]interface{}) *DeploymentUpdate { + du.mutation.SetDynamicPayload(m) + return du +} + +// ClearDynamicPayload clears the value of the "dynamic_payload" field. +func (du *DeploymentUpdate) ClearDynamicPayload() *DeploymentUpdate { + du.mutation.ClearDynamicPayload() + return du +} + // SetStatus sets the "status" field. func (du *DeploymentUpdate) SetStatus(d deployment.Status) *DeploymentUpdate { du.mutation.SetStatus(d) @@ -543,6 +555,19 @@ func (du *DeploymentUpdate) sqlSave(ctx context.Context) (n int, err error) { Column: deployment.FieldRef, }) } + if value, ok := du.mutation.DynamicPayload(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeJSON, + Value: value, + Column: deployment.FieldDynamicPayload, + }) + } + if du.mutation.DynamicPayloadCleared() { + _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{ + Type: field.TypeJSON, + Column: deployment.FieldDynamicPayload, + }) + } if value, ok := du.mutation.Status(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeEnum, @@ -947,6 +972,18 @@ func (duo *DeploymentUpdateOne) SetRef(s string) *DeploymentUpdateOne { return duo } +// SetDynamicPayload sets the "dynamic_payload" field. +func (duo *DeploymentUpdateOne) SetDynamicPayload(m map[string]interface{}) *DeploymentUpdateOne { + duo.mutation.SetDynamicPayload(m) + return duo +} + +// ClearDynamicPayload clears the value of the "dynamic_payload" field. +func (duo *DeploymentUpdateOne) ClearDynamicPayload() *DeploymentUpdateOne { + duo.mutation.ClearDynamicPayload() + return duo +} + // SetStatus sets the "status" field. func (duo *DeploymentUpdateOne) SetStatus(d deployment.Status) *DeploymentUpdateOne { duo.mutation.SetStatus(d) @@ -1442,6 +1479,19 @@ func (duo *DeploymentUpdateOne) sqlSave(ctx context.Context) (_node *Deployment, Column: deployment.FieldRef, }) } + if value, ok := duo.mutation.DynamicPayload(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeJSON, + Value: value, + Column: deployment.FieldDynamicPayload, + }) + } + if duo.mutation.DynamicPayloadCleared() { + _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{ + Type: field.TypeJSON, + Column: deployment.FieldDynamicPayload, + }) + } if value, ok := duo.mutation.Status(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeEnum, diff --git a/model/ent/migrate/schema.go b/model/ent/migrate/schema.go index da66523a..b67c7563 100644 --- a/model/ent/migrate/schema.go +++ b/model/ent/migrate/schema.go @@ -40,6 +40,7 @@ var ( {Name: "type", Type: field.TypeEnum, Enums: []string{"commit", "branch", "tag"}, Default: "commit"}, {Name: "env", Type: field.TypeString}, {Name: "ref", Type: field.TypeString}, + {Name: "dynamic_payload", Type: field.TypeJSON, Nullable: true}, {Name: "status", Type: field.TypeEnum, Enums: []string{"waiting", "created", "queued", "running", "success", "failure", "canceled"}, Default: "waiting"}, {Name: "uid", Type: field.TypeInt64, Nullable: true}, {Name: "sha", Type: field.TypeString, Nullable: true}, @@ -61,13 +62,13 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "deployments_repos_deployments", - Columns: []*schema.Column{DeploymentsColumns[15]}, + Columns: []*schema.Column{DeploymentsColumns[16]}, RefColumns: []*schema.Column{ReposColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "deployments_users_deployments", - Columns: []*schema.Column{DeploymentsColumns[16]}, + Columns: []*schema.Column{DeploymentsColumns[17]}, RefColumns: []*schema.Column{UsersColumns[0]}, OnDelete: schema.NoAction, }, @@ -76,32 +77,32 @@ var ( { Name: "deployment_repo_id_env_status_updated_at", Unique: false, - Columns: []*schema.Column{DeploymentsColumns[15], DeploymentsColumns[3], DeploymentsColumns[5], DeploymentsColumns[12]}, + Columns: []*schema.Column{DeploymentsColumns[16], DeploymentsColumns[3], DeploymentsColumns[6], DeploymentsColumns[13]}, }, { Name: "deployment_repo_id_env_created_at", Unique: false, - Columns: []*schema.Column{DeploymentsColumns[15], DeploymentsColumns[3], DeploymentsColumns[11]}, + Columns: []*schema.Column{DeploymentsColumns[16], DeploymentsColumns[3], DeploymentsColumns[12]}, }, { Name: "deployment_repo_id_created_at", Unique: false, - Columns: []*schema.Column{DeploymentsColumns[15], DeploymentsColumns[11]}, + Columns: []*schema.Column{DeploymentsColumns[16], DeploymentsColumns[12]}, }, { Name: "deployment_number_repo_id", Unique: true, - Columns: []*schema.Column{DeploymentsColumns[1], DeploymentsColumns[15]}, + Columns: []*schema.Column{DeploymentsColumns[1], DeploymentsColumns[16]}, }, { Name: "deployment_uid", Unique: false, - Columns: []*schema.Column{DeploymentsColumns[6]}, + Columns: []*schema.Column{DeploymentsColumns[7]}, }, { Name: "deployment_status_created_at", Unique: false, - Columns: []*schema.Column{DeploymentsColumns[5], DeploymentsColumns[11]}, + Columns: []*schema.Column{DeploymentsColumns[6], DeploymentsColumns[12]}, }, }, } diff --git a/model/ent/mutation.go b/model/ent/mutation.go index 14ee1e62..c14beeed 100644 --- a/model/ent/mutation.go +++ b/model/ent/mutation.go @@ -757,6 +757,7 @@ type DeploymentMutation struct { _type *deployment.Type env *string ref *string + dynamic_payload *map[string]interface{} status *deployment.Status uid *int64 adduid *int64 @@ -1050,6 +1051,55 @@ func (m *DeploymentMutation) ResetRef() { m.ref = nil } +// SetDynamicPayload sets the "dynamic_payload" field. +func (m *DeploymentMutation) SetDynamicPayload(value map[string]interface{}) { + m.dynamic_payload = &value +} + +// DynamicPayload returns the value of the "dynamic_payload" field in the mutation. +func (m *DeploymentMutation) DynamicPayload() (r map[string]interface{}, exists bool) { + v := m.dynamic_payload + if v == nil { + return + } + return *v, true +} + +// OldDynamicPayload returns the old "dynamic_payload" field's value of the Deployment entity. +// If the Deployment object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *DeploymentMutation) OldDynamicPayload(ctx context.Context) (v map[string]interface{}, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDynamicPayload is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDynamicPayload requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDynamicPayload: %w", err) + } + return oldValue.DynamicPayload, nil +} + +// ClearDynamicPayload clears the value of the "dynamic_payload" field. +func (m *DeploymentMutation) ClearDynamicPayload() { + m.dynamic_payload = nil + m.clearedFields[deployment.FieldDynamicPayload] = struct{}{} +} + +// DynamicPayloadCleared returns if the "dynamic_payload" field was cleared in this mutation. +func (m *DeploymentMutation) DynamicPayloadCleared() bool { + _, ok := m.clearedFields[deployment.FieldDynamicPayload] + return ok +} + +// ResetDynamicPayload resets all changes to the "dynamic_payload" field. +func (m *DeploymentMutation) ResetDynamicPayload() { + m.dynamic_payload = nil + delete(m.clearedFields, deployment.FieldDynamicPayload) +} + // SetStatus sets the "status" field. func (m *DeploymentMutation) SetStatus(d deployment.Status) { m.status = &d @@ -1822,7 +1872,7 @@ func (m *DeploymentMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *DeploymentMutation) Fields() []string { - fields := make([]string, 0, 16) + fields := make([]string, 0, 17) if m.number != nil { fields = append(fields, deployment.FieldNumber) } @@ -1835,6 +1885,9 @@ func (m *DeploymentMutation) Fields() []string { if m.ref != nil { fields = append(fields, deployment.FieldRef) } + if m.dynamic_payload != nil { + fields = append(fields, deployment.FieldDynamicPayload) + } if m.status != nil { fields = append(fields, deployment.FieldStatus) } @@ -1887,6 +1940,8 @@ func (m *DeploymentMutation) Field(name string) (ent.Value, bool) { return m.Env() case deployment.FieldRef: return m.Ref() + case deployment.FieldDynamicPayload: + return m.DynamicPayload() case deployment.FieldStatus: return m.Status() case deployment.FieldUID: @@ -1928,6 +1983,8 @@ func (m *DeploymentMutation) OldField(ctx context.Context, name string) (ent.Val return m.OldEnv(ctx) case deployment.FieldRef: return m.OldRef(ctx) + case deployment.FieldDynamicPayload: + return m.OldDynamicPayload(ctx) case deployment.FieldStatus: return m.OldStatus(ctx) case deployment.FieldUID: @@ -1989,6 +2046,13 @@ func (m *DeploymentMutation) SetField(name string, value ent.Value) error { } m.SetRef(v) return nil + case deployment.FieldDynamicPayload: + v, ok := value.(map[string]interface{}) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDynamicPayload(v) + return nil case deployment.FieldStatus: v, ok := value.(deployment.Status) if !ok { @@ -2142,6 +2206,9 @@ func (m *DeploymentMutation) AddField(name string, value ent.Value) error { // mutation. func (m *DeploymentMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(deployment.FieldDynamicPayload) { + fields = append(fields, deployment.FieldDynamicPayload) + } if m.FieldCleared(deployment.FieldUID) { fields = append(fields, deployment.FieldUID) } @@ -2171,6 +2238,9 @@ func (m *DeploymentMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *DeploymentMutation) ClearField(name string) error { switch name { + case deployment.FieldDynamicPayload: + m.ClearDynamicPayload() + return nil case deployment.FieldUID: m.ClearUID() return nil @@ -2206,6 +2276,9 @@ func (m *DeploymentMutation) ResetField(name string) error { case deployment.FieldRef: m.ResetRef() return nil + case deployment.FieldDynamicPayload: + m.ResetDynamicPayload() + return nil case deployment.FieldStatus: m.ResetStatus() return nil diff --git a/model/ent/runtime.go b/model/ent/runtime.go index 7a80ec2b..dde6fdb8 100644 --- a/model/ent/runtime.go +++ b/model/ent/runtime.go @@ -37,23 +37,23 @@ func init() { deploymentFields := schema.Deployment{}.Fields() _ = deploymentFields // deploymentDescHTMLURL is the schema descriptor for html_url field. - deploymentDescHTMLURL := deploymentFields[7].Descriptor() + deploymentDescHTMLURL := deploymentFields[8].Descriptor() // deployment.HTMLURLValidator is a validator for the "html_url" field. It is called by the builders before save. deployment.HTMLURLValidator = deploymentDescHTMLURL.Validators[0].(func(string) error) // deploymentDescProductionEnvironment is the schema descriptor for production_environment field. - deploymentDescProductionEnvironment := deploymentFields[8].Descriptor() + deploymentDescProductionEnvironment := deploymentFields[9].Descriptor() // deployment.DefaultProductionEnvironment holds the default value on creation for the production_environment field. deployment.DefaultProductionEnvironment = deploymentDescProductionEnvironment.Default.(bool) // deploymentDescIsRollback is the schema descriptor for is_rollback field. - deploymentDescIsRollback := deploymentFields[9].Descriptor() + deploymentDescIsRollback := deploymentFields[10].Descriptor() // deployment.DefaultIsRollback holds the default value on creation for the is_rollback field. deployment.DefaultIsRollback = deploymentDescIsRollback.Default.(bool) // deploymentDescCreatedAt is the schema descriptor for created_at field. - deploymentDescCreatedAt := deploymentFields[10].Descriptor() + deploymentDescCreatedAt := deploymentFields[11].Descriptor() // deployment.DefaultCreatedAt holds the default value on creation for the created_at field. deployment.DefaultCreatedAt = deploymentDescCreatedAt.Default.(func() time.Time) // deploymentDescUpdatedAt is the schema descriptor for updated_at field. - deploymentDescUpdatedAt := deploymentFields[11].Descriptor() + deploymentDescUpdatedAt := deploymentFields[12].Descriptor() // deployment.DefaultUpdatedAt holds the default value on creation for the updated_at field. deployment.DefaultUpdatedAt = deploymentDescUpdatedAt.Default.(func() time.Time) // deployment.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. diff --git a/model/ent/schema/deployment.go b/model/ent/schema/deployment.go index 7dc81eb2..e6a93b05 100644 --- a/model/ent/schema/deployment.go +++ b/model/ent/schema/deployment.go @@ -26,6 +26,8 @@ func (Deployment) Fields() []ent.Field { Default("commit"), field.String("env"), field.String("ref"), + field.JSON("dynamic_payload", map[string]interface{}{}). + Optional(), field.Enum("status"). Values( "waiting", diff --git a/model/extent/env.go b/model/extent/env.go index e04cac23..92d1150e 100644 --- a/model/extent/env.go +++ b/model/extent/env.go @@ -1,54 +1,55 @@ package extent import ( + "fmt" "regexp" "strings" "time" "github.com/gitploy-io/cronexpr" + "github.com/gitploy-io/gitploy/pkg/e" eutil "github.com/gitploy-io/gitploy/pkg/e" ) -type ( - Env struct { - Name string `json:"name" yaml:"name"` +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"` + // 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"` + DynamicPayload *DynamicPayload `json:"dynamic_payload" yaml:"dynamic_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"` + // 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"` + // 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"` + // 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"` + // 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"` - } + // 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"` - } +type 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"` - } -) +type 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 { @@ -124,3 +125,104 @@ func (e *Env) IsFreezed(t time.Time) (bool, error) { return false, nil } + +func (e *Env) IsDynamicPayloadEnabled() bool { + return (e.DynamicPayload != nil && e.DynamicPayload.Enabled) +} + +func (e *Env) ValidateDynamicPayload(values map[string]interface{}) error { + return e.DynamicPayload.Validate(values) +} + +// DynamicPayload can be set to dynamically fill in the payload. +type DynamicPayload struct { + Enabled bool `json:"enabled" yaml:"enabled"` + Inputs map[string]Input `json:"inputs" yaml:"inputs"` +} + +// Validate validates the payload. +func (dp *DynamicPayload) Validate(values map[string]interface{}) (err error) { + + for key, input := range dp.Inputs { + // If it is a required field, check if the value exists. + value, ok := values[key] + if !ok { + if optional := !(input.Required != nil && *input.Required); optional { + continue + } + + return e.NewErrorWithMessage(e.ErrorCodeDeploymentInvalid, fmt.Sprintf("The '%s' field is required.", key), nil) + } + + err := dp.validate(input, value) + if err != nil { + return err + } + + } + + return nil +} + +func (dp *DynamicPayload) validate(input Input, value interface{}) error { + switch input.Type { + case InputTypeSelect: + // Checks if the selected value matches the option, + // and returns the value if it is. + sv, ok := value.(string) + if !ok { + return e.NewErrorWithMessage(e.ErrorCodeDeploymentInvalid, fmt.Sprintf("The '%v' is not string type.", value), nil) + } + + for _, option := range *input.Options { + if sv == option { + return nil + } + } + + return e.NewErrorWithMessage(e.ErrorCodeDeploymentInvalid, "The '%s' is not matched with the options.", nil) + case InputTypeNumber: + if _, ok := value.(float64); ok { + return nil + } + + if _, ok := value.(int); !ok { + return e.NewErrorWithMessage(e.ErrorCodeDeploymentInvalid, fmt.Sprintf("The '%v' is not number type.", value), nil) + } + + return nil + case InputTypeString: + if _, ok := value.(string); !ok { + return e.NewErrorWithMessage(e.ErrorCodeDeploymentInvalid, fmt.Sprintf("The '%v' is not string type.", value), nil) + } + + return nil + case InputTypeBoolean: + if _, ok := value.(bool); !ok { + return e.NewErrorWithMessage(e.ErrorCodeDeploymentInvalid, fmt.Sprintf("The '%v' is not string type.", value), nil) + } + + return nil + default: + return e.NewErrorWithMessage(e.ErrorCodeDeploymentInvalid, "The type must be 'select', 'number', 'string', or 'boolean'.", nil) + } +} + +// Input defines specifications for input values. +type Input struct { + Type InputType `json:"type" yaml:"type"` + Required *bool `json:"required" yaml:"required"` + Default *interface{} `json:"default" yaml:"default"` + Description *string `json:"description" yaml:"description"` + Options *[]string `json:"options" yaml:"options"` +} + +// InputType is the type for input. +type InputType string + +const ( + InputTypeSelect InputType = "select" + InputTypeNumber InputType = "number" + InputTypeString InputType = "string" + InputTypeBoolean InputType = "boolean" +) diff --git a/model/extent/env_test.go b/model/extent/env_test.go index 2e0f6632..5153d772 100644 --- a/model/extent/env_test.go +++ b/model/extent/env_test.go @@ -149,4 +149,90 @@ func TestEnv_IsFreezed(t *testing.T) { t.Fatalf("IsFreezed = %v, wanted %v", freezed, false) } }) -} \ No newline at end of file +} + +func TestDynamicPayload_Validate(t *testing.T) { + t.Run("Return an error when the required field is not exist.", func(t *testing.T) { + c := DynamicPayload{ + Inputs: map[string]Input{ + "foo": { + Type: InputTypeString, + Required: pointer.ToBool(true), + }, + }, + } + + err := c.Validate(map[string]interface{}{}) + if err == nil { + t.Fatalf("Validate doesn't return an error.") + } + }) + + t.Run("Skip the optional field if there is no value.", func(t *testing.T) { + c := DynamicPayload{ + Inputs: map[string]Input{ + "foo": { + Type: InputTypeString, + }, + }, + } + + err := c.Validate(map[string]interface{}{}) + if err != nil { + t.Fatalf("Validate return an error: %s", err) + } + }) + + t.Run("Return an error when the selected value is not in the options.", func(t *testing.T) { + c := DynamicPayload{ + Inputs: map[string]Input{ + "foo": { + Type: InputTypeSelect, + Required: pointer.ToBool(true), + Options: &[]string{"option1", "option2"}, + }, + }, + } + + input := map[string]interface{}{"foo": "value"} + + err := c.Validate(input) + if err == nil { + t.Fatalf("Validate doesn't return an error.") + } + }) + + t.Run("Return nil if validation has succeed.", func(t *testing.T) { + c := DynamicPayload{ + Inputs: map[string]Input{ + "foo": { + Type: InputTypeSelect, + Required: pointer.ToBool(true), + Options: &[]string{"option1", "option2"}, + }, + "bar": { + Type: InputTypeNumber, + Required: pointer.ToBool(true), + }, + "baz": { + Type: InputTypeNumber, + }, + "qux": { + Type: InputTypeBoolean, + }, + }, + } + + input := map[string]interface{}{ + "foo": "option1", + "bar": 1, + "baz": 4.2, + "qux": false, + } + + err := c.Validate(input) + if err != nil { + t.Fatalf("Evaluate return an error: %s", err) + } + }) +} diff --git a/openapi/v1.yaml b/openapi/v1.yaml index 6da60a01..d75573aa 100644 --- a/openapi/v1.yaml +++ b/openapi/v1.yaml @@ -523,6 +523,8 @@ paths: type: string env: type: string + dynamic_payload: + type: object required: - type - ref diff --git a/pkg/e/code.go b/pkg/e/code.go index dd359e06..b1fc2134 100644 --- a/pkg/e/code.go +++ b/pkg/e/code.go @@ -14,7 +14,7 @@ const ( // ErrorCodeDeploymentConflict is the deployment number is conflicted. ErrorCodeDeploymentConflict ErrorCode = "deployment_conflict" - // ErrorCodeDeploymentInvalid is the payload is invalid when it posts a remote deployment. + // ErrorCodeDeploymentInvalid is the payload is invalid. ErrorCodeDeploymentInvalid ErrorCode = "deployment_invalid" // ErrorCodeDeploymentLocked is when the environment is locked. ErrorCodeDeploymentLocked ErrorCode = "deployment_locked"