From 10f896d29afb4515b6c2e141d8cc82f296570754 Mon Sep 17 00:00:00 2001 From: noah Date: Mon, 11 Oct 2021 22:44:10 +0900 Subject: [PATCH 1/6] Fix the locaiton of lock button --- ui/src/components/LockList.tsx | 46 +++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/ui/src/components/LockList.tsx b/ui/src/components/LockList.tsx index a3ccb3a1..2705b3b0 100644 --- a/ui/src/components/LockList.tsx +++ b/ui/src/components/LockList.tsx @@ -1,4 +1,8 @@ -import { List, Button } from "antd" +import { + List, + Button, + DatePicker, +} from "antd" import { LockOutlined, UnlockOutlined } from "@ant-design/icons" import moment from 'moment' @@ -20,28 +24,40 @@ export default function LockList(props: LockListProps): JSX.Element { const lock = props.locks.find((lock) => lock.env === env.name) return (lock)? - + + , + + ]} + > {env.name.toUpperCase()} } description={} /> - : - + {props.onClickLock(env.name)}} + > + LOCK + , + ]} + > {env.name.toUpperCase()} } description={} /> - }} /> @@ -57,6 +73,6 @@ function LockDescription(props: LockDescriptionProps) { return ( (props.lock)? Locked by {moment(props.lock.createdAt).fromNow()}: - + <> ) } \ No newline at end of file From 72f11549720e5852a2d00cb60072eda48847b35e Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 17 Oct 2021 19:35:43 +0900 Subject: [PATCH 2/6] Add the expired_at field into Lock --- ent/lock.go | 12 ++- ent/lock/lock.go | 3 + ent/lock/where.go | 97 +++++++++++++++++++ ent/lock_create.go | 22 +++++ ent/lock_update.go | 66 +++++++++++++ ent/migrate/schema.go | 12 ++- ent/mutation.go | 80 ++++++++++++++- ent/runtime.go | 2 +- ent/schema/lock.go | 2 + internal/interactor/interface.go | 1 + internal/interactor/mock/pkg.go | 15 +++ internal/pkg/store/lock.go | 8 ++ internal/server/api/v1/repos/interface.go | 1 + internal/server/api/v1/repos/lock.go | 84 +++++++++++++++- internal/server/api/v1/repos/lock_test.go | 62 +++++++++++- .../server/api/v1/repos/mock/interactor.go | 15 +++ internal/server/router.go | 1 + openapi.yml | 55 ++++++++++- 18 files changed, 522 insertions(+), 16 deletions(-) diff --git a/ent/lock.go b/ent/lock.go index 0a2b9846..b24f5976 100644 --- a/ent/lock.go +++ b/ent/lock.go @@ -20,6 +20,8 @@ type Lock struct { ID int `json:"id,omitempty"` // Env holds the value of the "env" field. Env string `json:"env"` + // ExpiredAt holds the value of the "expired_at" field. + ExpiredAt time.Time `json:"expired_at,omitemtpy"` // CreatedAt holds the value of the "created_at" field. CreatedAt time.Time `json:"created_at"` // UserID holds the value of the "user_id" field. @@ -79,7 +81,7 @@ func (*Lock) scanValues(columns []string) ([]interface{}, error) { values[i] = new(sql.NullInt64) case lock.FieldEnv: values[i] = new(sql.NullString) - case lock.FieldCreatedAt: + case lock.FieldExpiredAt, lock.FieldCreatedAt: values[i] = new(sql.NullTime) default: return nil, fmt.Errorf("unexpected column %q for type Lock", columns[i]) @@ -108,6 +110,12 @@ func (l *Lock) assignValues(columns []string, values []interface{}) error { } else if value.Valid { l.Env = value.String } + case lock.FieldExpiredAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field expired_at", values[i]) + } else if value.Valid { + l.ExpiredAt = value.Time + } case lock.FieldCreatedAt: if value, ok := values[i].(*sql.NullTime); !ok { return fmt.Errorf("unexpected type %T for field created_at", values[i]) @@ -166,6 +174,8 @@ func (l *Lock) String() string { builder.WriteString(fmt.Sprintf("id=%v", l.ID)) builder.WriteString(", env=") builder.WriteString(l.Env) + builder.WriteString(", expired_at=") + builder.WriteString(l.ExpiredAt.Format(time.ANSIC)) builder.WriteString(", created_at=") builder.WriteString(l.CreatedAt.Format(time.ANSIC)) builder.WriteString(", user_id=") diff --git a/ent/lock/lock.go b/ent/lock/lock.go index 8103eadd..1a0d4f04 100644 --- a/ent/lock/lock.go +++ b/ent/lock/lock.go @@ -13,6 +13,8 @@ const ( FieldID = "id" // FieldEnv holds the string denoting the env field in the database. FieldEnv = "env" + // FieldExpiredAt holds the string denoting the expired_at field in the database. + FieldExpiredAt = "expired_at" // FieldCreatedAt holds the string denoting the created_at field in the database. FieldCreatedAt = "created_at" // FieldUserID holds the string denoting the user_id field in the database. @@ -45,6 +47,7 @@ const ( var Columns = []string{ FieldID, FieldEnv, + FieldExpiredAt, FieldCreatedAt, FieldUserID, FieldRepoID, diff --git a/ent/lock/where.go b/ent/lock/where.go index 8b0f066c..9709e433 100644 --- a/ent/lock/where.go +++ b/ent/lock/where.go @@ -100,6 +100,13 @@ func Env(v string) predicate.Lock { }) } +// ExpiredAt applies equality check predicate on the "expired_at" field. It's identical to ExpiredAtEQ. +func ExpiredAt(v time.Time) predicate.Lock { + return predicate.Lock(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldExpiredAt), v)) + }) +} + // CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. func CreatedAt(v time.Time) predicate.Lock { return predicate.Lock(func(s *sql.Selector) { @@ -232,6 +239,96 @@ func EnvContainsFold(v string) predicate.Lock { }) } +// ExpiredAtEQ applies the EQ predicate on the "expired_at" field. +func ExpiredAtEQ(v time.Time) predicate.Lock { + return predicate.Lock(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldExpiredAt), v)) + }) +} + +// ExpiredAtNEQ applies the NEQ predicate on the "expired_at" field. +func ExpiredAtNEQ(v time.Time) predicate.Lock { + return predicate.Lock(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldExpiredAt), v)) + }) +} + +// ExpiredAtIn applies the In predicate on the "expired_at" field. +func ExpiredAtIn(vs ...time.Time) predicate.Lock { + v := make([]interface{}, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Lock(func(s *sql.Selector) { + // if not arguments were provided, append the FALSE constants, + // since we can't apply "IN ()". This will make this predicate falsy. + if len(v) == 0 { + s.Where(sql.False()) + return + } + s.Where(sql.In(s.C(FieldExpiredAt), v...)) + }) +} + +// ExpiredAtNotIn applies the NotIn predicate on the "expired_at" field. +func ExpiredAtNotIn(vs ...time.Time) predicate.Lock { + v := make([]interface{}, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Lock(func(s *sql.Selector) { + // if not arguments were provided, append the FALSE constants, + // since we can't apply "IN ()". This will make this predicate falsy. + if len(v) == 0 { + s.Where(sql.False()) + return + } + s.Where(sql.NotIn(s.C(FieldExpiredAt), v...)) + }) +} + +// ExpiredAtGT applies the GT predicate on the "expired_at" field. +func ExpiredAtGT(v time.Time) predicate.Lock { + return predicate.Lock(func(s *sql.Selector) { + s.Where(sql.GT(s.C(FieldExpiredAt), v)) + }) +} + +// ExpiredAtGTE applies the GTE predicate on the "expired_at" field. +func ExpiredAtGTE(v time.Time) predicate.Lock { + return predicate.Lock(func(s *sql.Selector) { + s.Where(sql.GTE(s.C(FieldExpiredAt), v)) + }) +} + +// ExpiredAtLT applies the LT predicate on the "expired_at" field. +func ExpiredAtLT(v time.Time) predicate.Lock { + return predicate.Lock(func(s *sql.Selector) { + s.Where(sql.LT(s.C(FieldExpiredAt), v)) + }) +} + +// ExpiredAtLTE applies the LTE predicate on the "expired_at" field. +func ExpiredAtLTE(v time.Time) predicate.Lock { + return predicate.Lock(func(s *sql.Selector) { + s.Where(sql.LTE(s.C(FieldExpiredAt), v)) + }) +} + +// ExpiredAtIsNil applies the IsNil predicate on the "expired_at" field. +func ExpiredAtIsNil() predicate.Lock { + return predicate.Lock(func(s *sql.Selector) { + s.Where(sql.IsNull(s.C(FieldExpiredAt))) + }) +} + +// ExpiredAtNotNil applies the NotNil predicate on the "expired_at" field. +func ExpiredAtNotNil() predicate.Lock { + return predicate.Lock(func(s *sql.Selector) { + s.Where(sql.NotNull(s.C(FieldExpiredAt))) + }) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v time.Time) predicate.Lock { return predicate.Lock(func(s *sql.Selector) { diff --git a/ent/lock_create.go b/ent/lock_create.go index c9057c9c..e24f3a9e 100644 --- a/ent/lock_create.go +++ b/ent/lock_create.go @@ -28,6 +28,20 @@ func (lc *LockCreate) SetEnv(s string) *LockCreate { return lc } +// SetExpiredAt sets the "expired_at" field. +func (lc *LockCreate) SetExpiredAt(t time.Time) *LockCreate { + lc.mutation.SetExpiredAt(t) + return lc +} + +// SetNillableExpiredAt sets the "expired_at" field if the given value is not nil. +func (lc *LockCreate) SetNillableExpiredAt(t *time.Time) *LockCreate { + if t != nil { + lc.SetExpiredAt(*t) + } + return lc +} + // SetCreatedAt sets the "created_at" field. func (lc *LockCreate) SetCreatedAt(t time.Time) *LockCreate { lc.mutation.SetCreatedAt(t) @@ -196,6 +210,14 @@ func (lc *LockCreate) createSpec() (*Lock, *sqlgraph.CreateSpec) { }) _node.Env = value } + if value, ok := lc.mutation.ExpiredAt(); ok { + _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: lock.FieldExpiredAt, + }) + _node.ExpiredAt = value + } if value, ok := lc.mutation.CreatedAt(); ok { _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ Type: field.TypeTime, diff --git a/ent/lock_update.go b/ent/lock_update.go index 86532d74..23572dff 100644 --- a/ent/lock_update.go +++ b/ent/lock_update.go @@ -36,6 +36,26 @@ func (lu *LockUpdate) SetEnv(s string) *LockUpdate { return lu } +// SetExpiredAt sets the "expired_at" field. +func (lu *LockUpdate) SetExpiredAt(t time.Time) *LockUpdate { + lu.mutation.SetExpiredAt(t) + return lu +} + +// SetNillableExpiredAt sets the "expired_at" field if the given value is not nil. +func (lu *LockUpdate) SetNillableExpiredAt(t *time.Time) *LockUpdate { + if t != nil { + lu.SetExpiredAt(*t) + } + return lu +} + +// ClearExpiredAt clears the value of the "expired_at" field. +func (lu *LockUpdate) ClearExpiredAt() *LockUpdate { + lu.mutation.ClearExpiredAt() + return lu +} + // SetCreatedAt sets the "created_at" field. func (lu *LockUpdate) SetCreatedAt(t time.Time) *LockUpdate { lu.mutation.SetCreatedAt(t) @@ -185,6 +205,19 @@ func (lu *LockUpdate) sqlSave(ctx context.Context) (n int, err error) { Column: lock.FieldEnv, }) } + if value, ok := lu.mutation.ExpiredAt(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: lock.FieldExpiredAt, + }) + } + if lu.mutation.ExpiredAtCleared() { + _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Column: lock.FieldExpiredAt, + }) + } if value, ok := lu.mutation.CreatedAt(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeTime, @@ -287,6 +320,26 @@ func (luo *LockUpdateOne) SetEnv(s string) *LockUpdateOne { return luo } +// SetExpiredAt sets the "expired_at" field. +func (luo *LockUpdateOne) SetExpiredAt(t time.Time) *LockUpdateOne { + luo.mutation.SetExpiredAt(t) + return luo +} + +// SetNillableExpiredAt sets the "expired_at" field if the given value is not nil. +func (luo *LockUpdateOne) SetNillableExpiredAt(t *time.Time) *LockUpdateOne { + if t != nil { + luo.SetExpiredAt(*t) + } + return luo +} + +// ClearExpiredAt clears the value of the "expired_at" field. +func (luo *LockUpdateOne) ClearExpiredAt() *LockUpdateOne { + luo.mutation.ClearExpiredAt() + return luo +} + // SetCreatedAt sets the "created_at" field. func (luo *LockUpdateOne) SetCreatedAt(t time.Time) *LockUpdateOne { luo.mutation.SetCreatedAt(t) @@ -460,6 +513,19 @@ func (luo *LockUpdateOne) sqlSave(ctx context.Context) (_node *Lock, err error) Column: lock.FieldEnv, }) } + if value, ok := luo.mutation.ExpiredAt(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: lock.FieldExpiredAt, + }) + } + if luo.mutation.ExpiredAtCleared() { + _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Column: lock.FieldExpiredAt, + }) + } if value, ok := luo.mutation.CreatedAt(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeTime, diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index 6d1a10af..713ab98a 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -195,6 +195,11 @@ var ( Unique: false, Columns: []*schema.Column{DeploymentStatisticsColumns[9]}, }, + { + Name: "deploymentcount_updated_at", + Unique: false, + Columns: []*schema.Column{DeploymentCountsColumns[6]}, + }, }, } // DeploymentStatusColumns holds the columns for the "deployment_status" table. @@ -262,6 +267,7 @@ var ( LocksColumns = []*schema.Column{ {Name: "id", Type: field.TypeInt, Increment: true}, {Name: "env", Type: field.TypeString}, + {Name: "expired_at", Type: field.TypeTime, Nullable: true}, {Name: "created_at", Type: field.TypeTime}, {Name: "repo_id", Type: field.TypeInt64, Nullable: true}, {Name: "user_id", Type: field.TypeInt64, Nullable: true}, @@ -274,13 +280,13 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "locks_repos_locks", - Columns: []*schema.Column{LocksColumns[3]}, + Columns: []*schema.Column{LocksColumns[4]}, RefColumns: []*schema.Column{ReposColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "locks_users_locks", - Columns: []*schema.Column{LocksColumns[4]}, + Columns: []*schema.Column{LocksColumns[5]}, RefColumns: []*schema.Column{UsersColumns[0]}, OnDelete: schema.SetNull, }, @@ -289,7 +295,7 @@ var ( { Name: "lock_repo_id_env", Unique: true, - Columns: []*schema.Column{LocksColumns[3], LocksColumns[1]}, + Columns: []*schema.Column{LocksColumns[4], LocksColumns[1]}, }, }, } diff --git a/ent/mutation.go b/ent/mutation.go index 75fcdffe..2351f07a 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -6122,6 +6122,7 @@ type LockMutation struct { typ string id *int env *string + expired_at *time.Time created_at *time.Time clearedFields map[string]struct{} user *int64 @@ -6248,6 +6249,55 @@ func (m *LockMutation) ResetEnv() { m.env = nil } +// SetExpiredAt sets the "expired_at" field. +func (m *LockMutation) SetExpiredAt(t time.Time) { + m.expired_at = &t +} + +// ExpiredAt returns the value of the "expired_at" field in the mutation. +func (m *LockMutation) ExpiredAt() (r time.Time, exists bool) { + v := m.expired_at + if v == nil { + return + } + return *v, true +} + +// OldExpiredAt returns the old "expired_at" field's value of the Lock entity. +// If the Lock 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 *LockMutation) OldExpiredAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, fmt.Errorf("OldExpiredAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, fmt.Errorf("OldExpiredAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExpiredAt: %w", err) + } + return oldValue.ExpiredAt, nil +} + +// ClearExpiredAt clears the value of the "expired_at" field. +func (m *LockMutation) ClearExpiredAt() { + m.expired_at = nil + m.clearedFields[lock.FieldExpiredAt] = struct{}{} +} + +// ExpiredAtCleared returns if the "expired_at" field was cleared in this mutation. +func (m *LockMutation) ExpiredAtCleared() bool { + _, ok := m.clearedFields[lock.FieldExpiredAt] + return ok +} + +// ResetExpiredAt resets all changes to the "expired_at" field. +func (m *LockMutation) ResetExpiredAt() { + m.expired_at = nil + delete(m.clearedFields, lock.FieldExpiredAt) +} + // SetCreatedAt sets the "created_at" field. func (m *LockMutation) SetCreatedAt(t time.Time) { m.created_at = &t @@ -6427,10 +6477,13 @@ func (m *LockMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *LockMutation) Fields() []string { - fields := make([]string, 0, 4) + fields := make([]string, 0, 5) if m.env != nil { fields = append(fields, lock.FieldEnv) } + if m.expired_at != nil { + fields = append(fields, lock.FieldExpiredAt) + } if m.created_at != nil { fields = append(fields, lock.FieldCreatedAt) } @@ -6450,6 +6503,8 @@ func (m *LockMutation) Field(name string) (ent.Value, bool) { switch name { case lock.FieldEnv: return m.Env() + case lock.FieldExpiredAt: + return m.ExpiredAt() case lock.FieldCreatedAt: return m.CreatedAt() case lock.FieldUserID: @@ -6467,6 +6522,8 @@ func (m *LockMutation) OldField(ctx context.Context, name string) (ent.Value, er switch name { case lock.FieldEnv: return m.OldEnv(ctx) + case lock.FieldExpiredAt: + return m.OldExpiredAt(ctx) case lock.FieldCreatedAt: return m.OldCreatedAt(ctx) case lock.FieldUserID: @@ -6489,6 +6546,13 @@ func (m *LockMutation) SetField(name string, value ent.Value) error { } m.SetEnv(v) return nil + case lock.FieldExpiredAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetExpiredAt(v) + return nil case lock.FieldCreatedAt: v, ok := value.(time.Time) if !ok { @@ -6542,7 +6606,11 @@ func (m *LockMutation) AddField(name string, value ent.Value) error { // ClearedFields returns all nullable fields that were cleared during this // mutation. func (m *LockMutation) ClearedFields() []string { - return nil + var fields []string + if m.FieldCleared(lock.FieldExpiredAt) { + fields = append(fields, lock.FieldExpiredAt) + } + return fields } // FieldCleared returns a boolean indicating if a field with the given name was @@ -6555,6 +6623,11 @@ func (m *LockMutation) FieldCleared(name string) bool { // ClearField clears the value of the field with the given name. It returns an // error if the field is not defined in the schema. func (m *LockMutation) ClearField(name string) error { + switch name { + case lock.FieldExpiredAt: + m.ClearExpiredAt() + return nil + } return fmt.Errorf("unknown Lock nullable field %s", name) } @@ -6565,6 +6638,9 @@ func (m *LockMutation) ResetField(name string) error { case lock.FieldEnv: m.ResetEnv() return nil + case lock.FieldExpiredAt: + m.ResetExpiredAt() + return nil case lock.FieldCreatedAt: m.ResetCreatedAt() return nil diff --git a/ent/runtime.go b/ent/runtime.go index 7d2ab986..644b36cd 100644 --- a/ent/runtime.go +++ b/ent/runtime.go @@ -152,7 +152,7 @@ func init() { lockFields := schema.Lock{}.Fields() _ = lockFields // lockDescCreatedAt is the schema descriptor for created_at field. - lockDescCreatedAt := lockFields[1].Descriptor() + lockDescCreatedAt := lockFields[2].Descriptor() // lock.DefaultCreatedAt holds the default value on creation for the created_at field. lock.DefaultCreatedAt = lockDescCreatedAt.Default.(func() time.Time) permFields := schema.Perm{}.Fields() diff --git a/ent/schema/lock.go b/ent/schema/lock.go index 9e291986..ff614ed0 100644 --- a/ent/schema/lock.go +++ b/ent/schema/lock.go @@ -18,6 +18,8 @@ type Lock struct { func (Lock) Fields() []ent.Field { return []ent.Field{ field.String("env"), + field.Time("expired_at"). + Optional(), field.Time("created_at"). Default(time.Now), // Edges diff --git a/internal/interactor/interface.go b/internal/interactor/interface.go index 48c0c999..0f50b219 100644 --- a/internal/interactor/interface.go +++ b/internal/interactor/interface.go @@ -79,6 +79,7 @@ type ( HasLockOfRepoForEnv(ctx context.Context, r *ent.Repo, env string) (bool, error) FindLockByID(ctx context.Context, id int) (*ent.Lock, error) CreateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) + UpdateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) DeleteLock(ctx context.Context, l *ent.Lock) error ListEventsGreaterThanTime(ctx context.Context, t time.Time) ([]*ent.Event, error) diff --git a/internal/interactor/mock/pkg.go b/internal/interactor/mock/pkg.go index dd3188e3..2a161f11 100644 --- a/internal/interactor/mock/pkg.go +++ b/internal/interactor/mock/pkg.go @@ -874,6 +874,21 @@ func (mr *MockStoreMockRecorder) UpdateDeploymentStatistics(ctx, s interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeploymentStatistics", reflect.TypeOf((*MockStore)(nil).UpdateDeploymentStatistics), ctx, s) } +// UpdateLock mocks base method. +func (m *MockStore) UpdateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateLock", ctx, l) + ret0, _ := ret[0].(*ent.Lock) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateLock indicates an expected call of UpdateLock. +func (mr *MockStoreMockRecorder) UpdateLock(ctx, l interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLock", reflect.TypeOf((*MockStore)(nil).UpdateLock), ctx, l) +} + // UpdatePerm mocks base method. func (m *MockStore) UpdatePerm(ctx context.Context, p *ent.Perm) (*ent.Perm, error) { m.ctrl.T.Helper() diff --git a/internal/pkg/store/lock.go b/internal/pkg/store/lock.go index 5df9e92a..85fe0281 100644 --- a/internal/pkg/store/lock.go +++ b/internal/pkg/store/lock.go @@ -64,11 +64,19 @@ func (s *Store) CreateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) return s.c.Lock. Create(). SetEnv(l.Env). + SetExpiredAt(l.ExpiredAt). SetRepoID(l.RepoID). SetUserID(l.UserID). Save(ctx) } +func (s *Store) UpdateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) { + return s.c.Lock. + UpdateOne(l). + SetExpiredAt(l.ExpiredAt). + Save(ctx) +} + func (s *Store) DeleteLock(ctx context.Context, l *ent.Lock) error { return s.c.Lock. DeleteOne(l). diff --git a/internal/server/api/v1/repos/interface.go b/internal/server/api/v1/repos/interface.go index 4aa1a36d..52025620 100644 --- a/internal/server/api/v1/repos/interface.go +++ b/internal/server/api/v1/repos/interface.go @@ -44,6 +44,7 @@ type ( HasLockOfRepoForEnv(ctx context.Context, r *ent.Repo, env string) (bool, error) FindLockByID(ctx context.Context, id int) (*ent.Lock, error) CreateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) + UpdateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) DeleteLock(ctx context.Context, l *ent.Lock) error CreateEvent(ctx context.Context, e *ent.Event) (*ent.Event, error) diff --git a/internal/server/api/v1/repos/lock.go b/internal/server/api/v1/repos/lock.go index 7bf65b97..0ee28662 100644 --- a/internal/server/api/v1/repos/lock.go +++ b/internal/server/api/v1/repos/lock.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "strconv" + "time" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" @@ -17,7 +18,12 @@ import ( type ( lockPostPayload struct { - Env string `json:"env"` + Env string `json:"env"` + ExpiredAt *string `json:"expired_at,omitempty"` + } + + lockPatchPayload struct { + ExpiredAt *string `json:"expired_at,omitempty"` } ) @@ -47,6 +53,17 @@ func (r *Repo) CreateLock(c *gin.Context) { return } + var ( + expiredAt time.Time = time.Time{} + err error + ) + if p.ExpiredAt != nil { + if expiredAt, err = time.Parse(time.RFC3339, *p.ExpiredAt); err != nil { + gb.ErrorResponse(c, http.StatusBadRequest, "Invalid format of \"expired_at\" parameter, RFC3339 format only.") + return + } + } + vr, _ := c.Get(KeyRepo) re := vr.(*ent.Repo) @@ -83,9 +100,10 @@ func (r *Repo) CreateLock(c *gin.Context) { // Lock the environment. l, err := r.i.CreateLock(ctx, &ent.Lock{ - Env: p.Env, - UserID: u.ID, - RepoID: re.ID, + Env: p.Env, + ExpiredAt: expiredAt, + UserID: u.ID, + RepoID: re.ID, }) if err != nil { r.log.Error("It has failed to lock the env.", zap.Error(err)) @@ -101,6 +119,64 @@ func (r *Repo) CreateLock(c *gin.Context) { gb.Response(c, http.StatusCreated, l) } +func (r *Repo) UpdateLock(c *gin.Context) { + ctx := c.Request.Context() + + var ( + sid = c.Param("lockID") + ) + + id, err := strconv.Atoi(sid) + if err != nil { + r.log.Error("The lock ID must to be number.", zap.Error(err)) + gb.ErrorResponse(c, http.StatusBadRequest, "The lock ID must to be number.") + return + } + + p := &lockPatchPayload{} + if err := c.ShouldBindBodyWith(p, binding.JSON); err != nil { + r.log.Error("It has failed to bind the payload.", zap.Error(err)) + gb.ErrorResponse(c, http.StatusBadRequest, "It has failed to bind the payload.") + return + } + + expiredAt := time.Time{} + if p.ExpiredAt != nil { + if expiredAt, err = time.Parse(time.RFC3339, *p.ExpiredAt); err != nil { + gb.ErrorResponse(c, http.StatusBadRequest, "Invalid format of \"expired_at\" parameter, RFC3339 format only.") + return + } + } + + l, err := r.i.FindLockByID(ctx, id) + if ent.IsNotFound(err) { + r.log.Warn("The lock is not found.", zap.Error(err)) + gb.ErrorResponse(c, http.StatusUnprocessableEntity, "The lock is not found.") + return + } else if err != nil { + r.log.Error("It has failed to find the lock.", zap.Error(err)) + gb.ErrorResponse(c, http.StatusInternalServerError, "It has failed to find the lock.") + return + } + + if p.ExpiredAt != nil { + l.ExpiredAt = expiredAt + r.log.Debug("Update the expired_at of the lock.", zap.Int("id", l.ID), zap.Time("expired_at", l.ExpiredAt)) + } + + if _, err := r.i.UpdateLock(ctx, l); err != nil { + r.log.Error("It has failed to update the lock.", zap.Error(err)) + gb.ErrorResponse(c, http.StatusInternalServerError, "It has failed to update the lock.") + return + } + + if nl, err := r.i.FindLockByID(ctx, l.ID); err == nil { + l = nl + } + + gb.Response(c, http.StatusOK, l) +} + func (r *Repo) DeleteLock(c *gin.Context) { ctx := c.Request.Context() diff --git a/internal/server/api/v1/repos/lock_test.go b/internal/server/api/v1/repos/lock_test.go index e9db385d..a693e316 100644 --- a/internal/server/api/v1/repos/lock_test.go +++ b/internal/server/api/v1/repos/lock_test.go @@ -2,12 +2,15 @@ package repos import ( "bytes" + "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" + "time" + "github.com/AlekSi/pointer" "github.com/gin-gonic/gin" "github.com/golang/mock/gomock" @@ -124,6 +127,60 @@ func TestRepo_CreateLock(t *testing.T) { }) } +func TestRepo_UpdateLock(t *testing.T) { + t.Run("Update the expired time.", func(t *testing.T) { + expiredAt := time.Date(2021, 10, 17, 0, 0, 0, 0, time.UTC) + + input := struct { + id int + payload *lockPatchPayload + }{ + id: 1, + payload: &lockPatchPayload{ + ExpiredAt: pointer.ToString(expiredAt.Format(time.RFC3339)), + }, + } + + ctrl := gomock.NewController(t) + m := mock.NewMockInteractor(ctrl) + + t.Log("MOCK - Find the lock by ID.") + m. + EXPECT(). + FindLockByID(gomock.Any(), input.id). + Return(&ent.Lock{ID: input.id, Env: "production"}, nil). + MaxTimes(2) + + t.Log("MOCK - Update the expired_at field.") + m. + EXPECT(). + UpdateLock(gomock.Any(), gomock.Eq(&ent.Lock{ + ID: input.id, + Env: "production", + ExpiredAt: expiredAt, + })). + DoAndReturn(func(_ context.Context, l *ent.Lock) (*ent.Lock, error) { + return l, nil + }) + + r := NewRepo(RepoConfig{}, m) + + gin.SetMode(gin.ReleaseMode) + router := gin.New() + router.PATCH("repos/:id/locks/:lockID", r.UpdateLock) + + body, _ := json.Marshal(input.payload) + req, _ := http.NewRequest("PATCH", fmt.Sprintf("/repos/1/locks/%d", input.id), bytes.NewBuffer(body)) + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("Code = %v, wanted %v. Body=%v", w.Code, http.StatusCreated, w.Body) + } + }) +} + func TestRepo_DeleteLock(t *testing.T) { t.Run("Unlock the env", func(t *testing.T) { input := struct { @@ -151,10 +208,7 @@ func TestRepo_DeleteLock(t *testing.T) { gin.SetMode(gin.ReleaseMode) router := gin.New() - router.DELETE("repos/:id/locks/:lockID", func(c *gin.Context) { - c.Set(global.KeyUser, &ent.User{}) - c.Set(KeyRepo, &ent.Repo{}) - }, r.DeleteLock) + router.DELETE("repos/:id/locks/:lockID", r.DeleteLock) req, _ := http.NewRequest("DELETE", fmt.Sprintf("/repos/1/locks/%d", input.id), nil) w := httptest.NewRecorder() diff --git a/internal/server/api/v1/repos/mock/interactor.go b/internal/server/api/v1/repos/mock/interactor.go index 4b41c6c4..c88cf269 100644 --- a/internal/server/api/v1/repos/mock/interactor.go +++ b/internal/server/api/v1/repos/mock/interactor.go @@ -589,6 +589,21 @@ func (mr *MockInteractorMockRecorder) UpdateApproval(ctx, a interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateApproval", reflect.TypeOf((*MockInteractor)(nil).UpdateApproval), ctx, a) } +// UpdateLock mocks base method. +func (m *MockInteractor) UpdateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateLock", ctx, l) + ret0, _ := ret[0].(*ent.Lock) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateLock indicates an expected call of UpdateLock. +func (mr *MockInteractorMockRecorder) UpdateLock(ctx, l interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLock", reflect.TypeOf((*MockInteractor)(nil).UpdateLock), ctx, l) +} + // UpdateRepo mocks base method. func (m *MockInteractor) UpdateRepo(ctx context.Context, r *ent.Repo) (*ent.Repo, error) { m.ctrl.T.Helper() diff --git a/internal/server/router.go b/internal/server/router.go index 6df2aef7..1c86321a 100644 --- a/internal/server/router.go +++ b/internal/server/router.go @@ -155,6 +155,7 @@ func NewRouter(c *RouterConfig) *gin.Engine { repov1.DELETE("/:namespace/:name/approvals/:aid", rm.RepoReadPerm(), r.DeleteApproval) repov1.GET("/:namespace/:name/locks", rm.RepoReadPerm(), r.ListLocks) repov1.POST("/:namespace/:name/locks", rm.RepoWritePerm(), r.CreateLock) + repov1.PATCH("/:namespace/:name/locks/:lockID", rm.RepoWritePerm(), r.UpdateLock) repov1.DELETE("/:namespace/:name/locks/:lockID", rm.RepoWritePerm(), r.DeleteLock) repov1.GET("/:namespace/:name/perms", rm.RepoReadPerm(), r.ListPerms) repov1.GET("/:namespace/:name/config", rm.RepoReadPerm(), r.GetConfig) diff --git a/openapi.yml b/openapi.yml index f6906dcd..5a9dc69d 100644 --- a/openapi.yml +++ b/openapi.yml @@ -1048,6 +1048,10 @@ paths: env: type: string description: The name of env in deploy.yml + expired_at: + type: string + required: + - env responses: '201': description: Lock @@ -1066,6 +1070,52 @@ paths: '500': $ref: '#/components/responses/500InternalError' /repos/{namespace}/{name}/locks/{lockId}: + patch: + tags: + - Repo + summary: Patch the lock. + parameters: + - in: path + name: namespace + required: true + schema: + type: string + - in: path + name: name + required: true + schema: + type: string + - in: path + name: lockId + required: true + schema: + type: integer + description: The lock ID. + requestBody: + content: + application/json: + schema: + type: object + properties: + expired_at: + type: string + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + '401': + $ref: '#/components/responses/401Unauthorized' + '402': + $ref: '#/components/responses/402PaymentRequired' + '403': + $ref: '#/components/responses/403Forbidden' + '404': + $ref: '#/components/responses/404NotFound' + '500': + $ref: '#/components/responses/500InternalError' delete: tags: - Repo @@ -1086,7 +1136,7 @@ paths: required: true schema: type: integer - description: The lock id. + description: The lock ID. responses: '200': description: Ok @@ -1812,6 +1862,8 @@ components: type: number env: type: string + expired_at: + type: string created_at: type: string edges: @@ -1824,6 +1876,7 @@ components: required: - id - env + - expired_at - created_at Config: type: object From 803a5bebf2852aa7c67f7b3175abb6b0a291d25f Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 17 Oct 2021 11:23:15 +0900 Subject: [PATCH 3/6] Add the worker for the auto-unlock --- internal/interactor/interactor.go | 6 ++++ internal/interactor/interface.go | 1 + internal/interactor/lock.go | 35 +++++++++++++++++++ internal/interactor/mock/pkg.go | 15 ++++++++ internal/pkg/store/lock.go | 10 ++++++ internal/pkg/store/lock_test.go | 58 +++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+) create mode 100644 internal/interactor/lock.go create mode 100644 internal/pkg/store/lock_test.go diff --git a/internal/interactor/interactor.go b/internal/interactor/interactor.go index 44ab7a5d..9266954b 100644 --- a/internal/interactor/interactor.go +++ b/internal/interactor/interactor.go @@ -68,5 +68,11 @@ func NewInteractor(c *InteractorConfig) *Interactor { i.log.Info("Start the worker canceling inactive deployments.") i.runClosingInactiveDeployment(i.stopCh) }() + + go func() { + i.log.Info("Start the worker for the auto unlock.") + i.runAutoUnlock(i.stopCh) + }() + return i } diff --git a/internal/interactor/interface.go b/internal/interactor/interface.go index 0f50b219..2077f45c 100644 --- a/internal/interactor/interface.go +++ b/internal/interactor/interface.go @@ -74,6 +74,7 @@ type ( UpdateApproval(ctx context.Context, a *ent.Approval) (*ent.Approval, error) DeleteApproval(ctx context.Context, a *ent.Approval) error + ListExpiredLocksLessThanTime(ctx context.Context, t time.Time) ([]*ent.Lock, error) ListLocksOfRepo(ctx context.Context, r *ent.Repo) ([]*ent.Lock, error) FindLockOfRepoByEnv(ctx context.Context, r *ent.Repo, env string) (*ent.Lock, error) HasLockOfRepoForEnv(ctx context.Context, r *ent.Repo, env string) (bool, error) diff --git a/internal/interactor/lock.go b/internal/interactor/lock.go new file mode 100644 index 00000000..53a88fcb --- /dev/null +++ b/internal/interactor/lock.go @@ -0,0 +1,35 @@ +package interactor + +import ( + "context" + "time" + + "go.uber.org/zap" +) + +func (i *Interactor) runAutoUnlock(stop <-chan struct{}) { + ctx := context.Background() + + ticker := time.NewTicker(time.Minute) +L: + for { + select { + case _, ok := <-stop: + if !ok { + ticker.Stop() + break L + } + case t := <-ticker.C: + ls, err := i.ListExpiredLocksLessThanTime(ctx, t) + if err != nil { + i.log.Error("It has failed to read expired locks.", zap.Error(err)) + continue + } + + for _, l := range ls { + i.DeleteLock(ctx, l) + i.log.Debug("Delete the expired lock.", zap.Int("id", l.ID)) + } + } + } +} diff --git a/internal/interactor/mock/pkg.go b/internal/interactor/mock/pkg.go index 2a161f11..342cab43 100644 --- a/internal/interactor/mock/pkg.go +++ b/internal/interactor/mock/pkg.go @@ -694,6 +694,21 @@ func (mr *MockStoreMockRecorder) ListEventsGreaterThanTime(ctx, t interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEventsGreaterThanTime", reflect.TypeOf((*MockStore)(nil).ListEventsGreaterThanTime), ctx, t) } +// ListExpiredLocksLessThanTime mocks base method. +func (m *MockStore) ListExpiredLocksLessThanTime(ctx context.Context, t time.Time) ([]*ent.Lock, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListExpiredLocksLessThanTime", ctx, t) + ret0, _ := ret[0].([]*ent.Lock) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListExpiredLocksLessThanTime indicates an expected call of ListExpiredLocksLessThanTime. +func (mr *MockStoreMockRecorder) ListExpiredLocksLessThanTime(ctx, t interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExpiredLocksLessThanTime", reflect.TypeOf((*MockStore)(nil).ListExpiredLocksLessThanTime), ctx, t) +} + // ListInactiveDeploymentsLessThanTime mocks base method. func (m *MockStore) ListInactiveDeploymentsLessThanTime(ctx context.Context, t time.Time, page, perPage int) ([]*ent.Deployment, error) { m.ctrl.T.Helper() diff --git a/internal/pkg/store/lock.go b/internal/pkg/store/lock.go index 85fe0281..b2645c63 100644 --- a/internal/pkg/store/lock.go +++ b/internal/pkg/store/lock.go @@ -2,11 +2,21 @@ package store import ( "context" + "time" "github.com/gitploy-io/gitploy/ent" "github.com/gitploy-io/gitploy/ent/lock" ) +func (s *Store) ListExpiredLocksLessThanTime(ctx context.Context, t time.Time) ([]*ent.Lock, error) { + return s.c.Lock. + Query(). + Where(lock.ExpiredAtLT(t)). + WithUser(). + WithRepo(). + All(ctx) +} + func (s *Store) ListLocksOfRepo(ctx context.Context, r *ent.Repo) ([]*ent.Lock, error) { return s.c.Lock. Query(). diff --git a/internal/pkg/store/lock_test.go b/internal/pkg/store/lock_test.go new file mode 100644 index 00000000..7efd06d7 --- /dev/null +++ b/internal/pkg/store/lock_test.go @@ -0,0 +1,58 @@ +package store + +import ( + "context" + "testing" + "time" + + "github.com/gitploy-io/gitploy/ent/enttest" + "github.com/gitploy-io/gitploy/ent/migrate" +) + +func TestStore_ListExpiredLocksLessThanTime(t *testing.T) { + t.Run("Returns expired locks.", func(t *testing.T) { + ctx := context.Background() + + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1", + enttest.WithMigrateOptions(migrate.WithForeignKeys(false)), + ) + defer client.Close() + + tm := time.Date(2021, 10, 17, 0, 0, 0, 0, time.UTC) + + client.Lock. + Create(). + SetEnv("dev"). + SetExpiredAt(tm.Add(-time.Hour)). + SetRepoID(1). + SetUserID(1). + SaveX(ctx) + + client.Lock. + Create(). + SetEnv("staging"). + SetRepoID(1). + SetUserID(1). + SaveX(ctx) + + client.Lock. + Create(). + SetEnv("production"). + SetRepoID(1). + SetUserID(1). + SaveX(ctx) + + s := NewStore(client) + + ls, err := s.ListExpiredLocksLessThanTime(ctx, tm) + if err != nil { + t.Fatalf("ListExpiredLocksLessThanTime returns an error: %s", err) + } + + expected := 1 + if len(ls) != expected { + t.Log("The zero value of time must to be skipped.") + t.Fatalf("len(ListExpiredLocksLessThanTime) = %v: want %v", len(ls), expected) + } + }) +} From a7b29e770c2f6ee5bc88862c59e2e8bf8e7dca60 Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 17 Oct 2021 12:39:24 +0900 Subject: [PATCH 4/6] Fix the expired_at field nillable --- ent/lock.go | 11 +++++++---- ent/lock_create.go | 2 +- ent/mutation.go | 2 +- ent/schema/lock.go | 3 ++- internal/pkg/store/lock.go | 8 +++++--- internal/server/api/v1/repos/lock.go | 17 +++++++++++------ internal/server/api/v1/repos/lock_test.go | 2 +- 7 files changed, 28 insertions(+), 17 deletions(-) diff --git a/ent/lock.go b/ent/lock.go index b24f5976..86cfb129 100644 --- a/ent/lock.go +++ b/ent/lock.go @@ -21,7 +21,7 @@ type Lock struct { // Env holds the value of the "env" field. Env string `json:"env"` // ExpiredAt holds the value of the "expired_at" field. - ExpiredAt time.Time `json:"expired_at,omitemtpy"` + ExpiredAt *time.Time `json:"expired_at,omitemtpy"` // CreatedAt holds the value of the "created_at" field. CreatedAt time.Time `json:"created_at"` // UserID holds the value of the "user_id" field. @@ -114,7 +114,8 @@ func (l *Lock) assignValues(columns []string, values []interface{}) error { if value, ok := values[i].(*sql.NullTime); !ok { return fmt.Errorf("unexpected type %T for field expired_at", values[i]) } else if value.Valid { - l.ExpiredAt = value.Time + l.ExpiredAt = new(time.Time) + *l.ExpiredAt = value.Time } case lock.FieldCreatedAt: if value, ok := values[i].(*sql.NullTime); !ok { @@ -174,8 +175,10 @@ func (l *Lock) String() string { builder.WriteString(fmt.Sprintf("id=%v", l.ID)) builder.WriteString(", env=") builder.WriteString(l.Env) - builder.WriteString(", expired_at=") - builder.WriteString(l.ExpiredAt.Format(time.ANSIC)) + if v := l.ExpiredAt; v != nil { + builder.WriteString(", expired_at=") + builder.WriteString(v.Format(time.ANSIC)) + } builder.WriteString(", created_at=") builder.WriteString(l.CreatedAt.Format(time.ANSIC)) builder.WriteString(", user_id=") diff --git a/ent/lock_create.go b/ent/lock_create.go index e24f3a9e..0e9c975c 100644 --- a/ent/lock_create.go +++ b/ent/lock_create.go @@ -216,7 +216,7 @@ func (lc *LockCreate) createSpec() (*Lock, *sqlgraph.CreateSpec) { Value: value, Column: lock.FieldExpiredAt, }) - _node.ExpiredAt = value + _node.ExpiredAt = &value } if value, ok := lc.mutation.CreatedAt(); ok { _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ diff --git a/ent/mutation.go b/ent/mutation.go index 2351f07a..cac14324 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -6266,7 +6266,7 @@ func (m *LockMutation) ExpiredAt() (r time.Time, exists bool) { // OldExpiredAt returns the old "expired_at" field's value of the Lock entity. // If the Lock 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 *LockMutation) OldExpiredAt(ctx context.Context) (v time.Time, err error) { +func (m *LockMutation) OldExpiredAt(ctx context.Context) (v *time.Time, err error) { if !m.op.Is(OpUpdateOne) { return v, fmt.Errorf("OldExpiredAt is only allowed on UpdateOne operations") } diff --git a/ent/schema/lock.go b/ent/schema/lock.go index ff614ed0..7b47adfc 100644 --- a/ent/schema/lock.go +++ b/ent/schema/lock.go @@ -19,7 +19,8 @@ func (Lock) Fields() []ent.Field { return []ent.Field{ field.String("env"), field.Time("expired_at"). - Optional(), + Optional(). + Nillable(), field.Time("created_at"). Default(time.Now), // Edges diff --git a/internal/pkg/store/lock.go b/internal/pkg/store/lock.go index b2645c63..5e6a74de 100644 --- a/internal/pkg/store/lock.go +++ b/internal/pkg/store/lock.go @@ -11,7 +11,9 @@ import ( func (s *Store) ListExpiredLocksLessThanTime(ctx context.Context, t time.Time) ([]*ent.Lock, error) { return s.c.Lock. Query(). - Where(lock.ExpiredAtLT(t)). + Where( + lock.ExpiredAtLT(t), + ). WithUser(). WithRepo(). All(ctx) @@ -74,7 +76,7 @@ func (s *Store) CreateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) return s.c.Lock. Create(). SetEnv(l.Env). - SetExpiredAt(l.ExpiredAt). + SetNillableExpiredAt(l.ExpiredAt). SetRepoID(l.RepoID). SetUserID(l.UserID). Save(ctx) @@ -83,7 +85,7 @@ func (s *Store) CreateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) func (s *Store) UpdateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) { return s.c.Lock. UpdateOne(l). - SetExpiredAt(l.ExpiredAt). + SetNillableExpiredAt(l.ExpiredAt). Save(ctx) } diff --git a/internal/server/api/v1/repos/lock.go b/internal/server/api/v1/repos/lock.go index 0ee28662..cbca1ff3 100644 --- a/internal/server/api/v1/repos/lock.go +++ b/internal/server/api/v1/repos/lock.go @@ -54,14 +54,16 @@ func (r *Repo) CreateLock(c *gin.Context) { } var ( - expiredAt time.Time = time.Time{} - err error + expiredAt *time.Time ) if p.ExpiredAt != nil { - if expiredAt, err = time.Parse(time.RFC3339, *p.ExpiredAt); err != nil { + e, err := time.Parse(time.RFC3339, *p.ExpiredAt) + if err != nil { gb.ErrorResponse(c, http.StatusBadRequest, "Invalid format of \"expired_at\" parameter, RFC3339 format only.") return } + + expiredAt = &e } vr, _ := c.Get(KeyRepo) @@ -140,12 +142,15 @@ func (r *Repo) UpdateLock(c *gin.Context) { return } - expiredAt := time.Time{} + var expiredAt *time.Time if p.ExpiredAt != nil { - if expiredAt, err = time.Parse(time.RFC3339, *p.ExpiredAt); err != nil { + e, err := time.Parse(time.RFC3339, *p.ExpiredAt) + if err != nil { gb.ErrorResponse(c, http.StatusBadRequest, "Invalid format of \"expired_at\" parameter, RFC3339 format only.") return } + + expiredAt = &e } l, err := r.i.FindLockByID(ctx, id) @@ -161,7 +166,7 @@ func (r *Repo) UpdateLock(c *gin.Context) { if p.ExpiredAt != nil { l.ExpiredAt = expiredAt - r.log.Debug("Update the expired_at of the lock.", zap.Int("id", l.ID), zap.Time("expired_at", l.ExpiredAt)) + r.log.Debug("Update the expired_at of the lock.", zap.Int("id", l.ID), zap.Timep("expired_at", l.ExpiredAt)) } if _, err := r.i.UpdateLock(ctx, l); err != nil { diff --git a/internal/server/api/v1/repos/lock_test.go b/internal/server/api/v1/repos/lock_test.go index a693e316..b6f6aff6 100644 --- a/internal/server/api/v1/repos/lock_test.go +++ b/internal/server/api/v1/repos/lock_test.go @@ -157,7 +157,7 @@ func TestRepo_UpdateLock(t *testing.T) { UpdateLock(gomock.Any(), gomock.Eq(&ent.Lock{ ID: input.id, Env: "production", - ExpiredAt: expiredAt, + ExpiredAt: &expiredAt, })). DoAndReturn(func(_ context.Context, l *ent.Lock) (*ent.Lock, error) { return l, nil From 00b40769ae694fde89b319cb90735531cc9ee1bc Mon Sep 17 00:00:00 2001 From: noah Date: Sun, 17 Oct 2021 18:51:25 +0900 Subject: [PATCH 5/6] Add the date picker for auto unlock --- internal/server/api/v1/repos/lock.go | 5 +++-- openapi.yml | 1 - ui/src/apis/index.ts | 4 +++- ui/src/apis/lock.ts | 24 +++++++++++++++++++++++ ui/src/components/LockList.tsx | 15 +++++++++++--- ui/src/models/Lock.ts | 1 + ui/src/redux/repoLock.ts | 29 +++++++++++++++++++++++++++- ui/src/views/RepoLock.tsx | 7 ++++++- 8 files changed, 77 insertions(+), 9 deletions(-) diff --git a/internal/server/api/v1/repos/lock.go b/internal/server/api/v1/repos/lock.go index cbca1ff3..607e9b74 100644 --- a/internal/server/api/v1/repos/lock.go +++ b/internal/server/api/v1/repos/lock.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/AlekSi/pointer" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "go.uber.org/zap" @@ -63,7 +64,7 @@ func (r *Repo) CreateLock(c *gin.Context) { return } - expiredAt = &e + expiredAt = pointer.ToTime(e.UTC()) } vr, _ := c.Get(KeyRepo) @@ -150,7 +151,7 @@ func (r *Repo) UpdateLock(c *gin.Context) { return } - expiredAt = &e + expiredAt = pointer.ToTime(e.UTC()) } l, err := r.i.FindLockByID(ctx, id) diff --git a/openapi.yml b/openapi.yml index 5a9dc69d..8351e2c9 100644 --- a/openapi.yml +++ b/openapi.yml @@ -1876,7 +1876,6 @@ components: required: - id - env - - expired_at - created_at Config: type: object diff --git a/ui/src/apis/index.ts b/ui/src/apis/index.ts index 85efbc5f..b961c367 100644 --- a/ui/src/apis/index.ts +++ b/ui/src/apis/index.ts @@ -36,7 +36,8 @@ import { import { listLocks, lock, - unlock + unlock, + updateLock } from "./lock" import { getLicense } from "./license" import { subscribeEvents } from "./events" @@ -82,6 +83,7 @@ export { listLocks, lock, unlock, + updateLock, getLicense, subscribeEvents } \ No newline at end of file diff --git a/ui/src/apis/lock.ts b/ui/src/apis/lock.ts index 993a0d89..f6585482 100644 --- a/ui/src/apis/lock.ts +++ b/ui/src/apis/lock.ts @@ -8,6 +8,7 @@ import { Lock, User, HttpForbiddenError, HttpUnprocessableEntityError } from ".. interface LockData { id: number env: string + expired_at?: string created_at: string edges: { user?: UserData @@ -24,6 +25,7 @@ const mapDataToLock = (data: LockData): Lock => { return { id: data.id, env: data.env, + expiredAt: (data.expired_at)? new Date(data.expired_at) : undefined, createdAt: new Date(data.created_at), user, } @@ -73,4 +75,26 @@ export const unlock = async (namespace: string, name: string, id: number): Promi const {message} = await res.json() throw new HttpForbiddenError(message) } +} + +export const updateLock = async (namespace: string, name: string, id: number, payload: {expiredAt?: Date}): Promise => { + const expired_at: string | undefined = (payload.expiredAt)? payload.expiredAt.toISOString() : undefined + + const res = await _fetch(`${instance}/api/v1/repos/${namespace}/${name}/locks/${id}`, { + headers, + credentials: 'same-origin', + method: "PATCH", + body: JSON.stringify({ + expired_at, + }), + }) + + if (res.status === StatusCodes.FORBIDDEN) { + const {message} = await res.json() + throw new HttpForbiddenError(message) + } + + const lock = res.json() + .then(data => mapDataToLock(data)) + return lock } \ No newline at end of file diff --git a/ui/src/components/LockList.tsx b/ui/src/components/LockList.tsx index 2705b3b0..2dd7cc60 100644 --- a/ui/src/components/LockList.tsx +++ b/ui/src/components/LockList.tsx @@ -14,6 +14,7 @@ interface LockListProps { locks: Lock[] onClickLock(env: string): void onClickUnlock(env: string): void + onChangeExpiredAt(env: string, expiredAt: Date): void } export default function LockList(props: LockListProps): JSX.Element { @@ -27,9 +28,17 @@ export default function LockList(props: LockListProps): JSX.Element { { + if (date) { + props.onChangeExpiredAt(env.name, new Date(date.format())) + } + }} + style={{width: 170}} /> ,