From 6c11ab05968e0862c9d20a31dfc2be8374c25543 Mon Sep 17 00:00:00 2001 From: Simone Gotti Date: Tue, 14 May 2019 17:53:48 +0200 Subject: [PATCH] *: implement update projectgroup --- .../configstore/action/projectgroup.go | 134 ++++++++++++++++-- internal/services/configstore/api/client.go | 11 ++ .../services/configstore/api/projectgroup.go | 48 +++++++ internal/services/configstore/configstore.go | 2 + .../services/gateway/action/projectgroup.go | 32 +++++ internal/services/gateway/api/projectgroup.go | 64 +++++++-- internal/services/gateway/gateway.go | 2 + 7 files changed, 274 insertions(+), 19 deletions(-) diff --git a/internal/services/configstore/action/projectgroup.go b/internal/services/configstore/action/projectgroup.go index bc1ed76..db659b8 100644 --- a/internal/services/configstore/action/projectgroup.go +++ b/internal/services/configstore/action/projectgroup.go @@ -73,18 +73,40 @@ func (h *ActionHandler) GetProjectGroupProjects(ctx context.Context, projectGrou return projects, nil } -func (h *ActionHandler) CreateProjectGroup(ctx context.Context, projectGroup *types.ProjectGroup) (*types.ProjectGroup, error) { - if projectGroup.Name == "" { - return nil, util.NewErrBadRequest(errors.Errorf("project group name required")) - } - if !util.ValidateName(projectGroup.Name) { - return nil, util.NewErrBadRequest(errors.Errorf("invalid project group name %q", projectGroup.Name)) +func (h *ActionHandler) ValidateProjectGroup(ctx context.Context, projectGroup *types.ProjectGroup) error { + if projectGroup.Parent.Type != types.ConfigTypeProjectGroup && + projectGroup.Parent.Type != types.ConfigTypeOrg && + projectGroup.Parent.Type != types.ConfigTypeUser { + return util.NewErrBadRequest(errors.Errorf("invalid project group parent type %q", projectGroup.Parent.Type)) } if projectGroup.Parent.ID == "" { - return nil, util.NewErrBadRequest(errors.Errorf("project group parent id required")) + return util.NewErrBadRequest(errors.Errorf("project group parent id required")) + } + + // if the project group is a root project group the name must be empty + if projectGroup.Parent.Type == types.ConfigTypeOrg || + projectGroup.Parent.Type == types.ConfigTypeUser { + if projectGroup.Name != "" { + return util.NewErrBadRequest(errors.Errorf("project group name for root project group must be empty")) + } + } else { + if projectGroup.Name == "" { + return util.NewErrBadRequest(errors.Errorf("project group name required")) + } + if !util.ValidateName(projectGroup.Name) { + return util.NewErrBadRequest(errors.Errorf("invalid project group name %q", projectGroup.Name)) + } } if !types.IsValidVisibility(projectGroup.Visibility) { - return nil, util.NewErrBadRequest(errors.Errorf("invalid project group visibility")) + return util.NewErrBadRequest(errors.Errorf("invalid project group visibility")) + } + + return nil +} + +func (h *ActionHandler) CreateProjectGroup(ctx context.Context, projectGroup *types.ProjectGroup) (*types.ProjectGroup, error) { + if err := h.ValidateProjectGroup(ctx, projectGroup); err != nil { + return nil, err } var cgt *datamanager.ChangeGroupsUpdateToken @@ -131,7 +153,7 @@ func (h *ActionHandler) CreateProjectGroup(ctx context.Context, projectGroup *ty projectGroup.ID = uuid.NewV4().String() projectGroup.Parent.Type = types.ConfigTypeProjectGroup - pcj, err := json.Marshal(projectGroup) + pgj, err := json.Marshal(projectGroup) if err != nil { return nil, errors.Wrapf(err, "failed to marshal projectGroup") } @@ -140,7 +162,7 @@ func (h *ActionHandler) CreateProjectGroup(ctx context.Context, projectGroup *ty ActionType: datamanager.ActionTypePut, DataType: string(types.ConfigTypeProjectGroup), ID: projectGroup.ID, - Data: pcj, + Data: pgj, }, } @@ -148,6 +170,98 @@ func (h *ActionHandler) CreateProjectGroup(ctx context.Context, projectGroup *ty return projectGroup, err } +type UpdateProjectGroupRequest struct { + ProjectGroupRef string + + ProjectGroup *types.ProjectGroup +} + +func (h *ActionHandler) UpdateProjectGroup(ctx context.Context, req *UpdateProjectGroupRequest) (*types.ProjectGroup, error) { + if err := h.ValidateProjectGroup(ctx, req.ProjectGroup); err != nil { + return nil, err + } + + var cgt *datamanager.ChangeGroupsUpdateToken + + // must do all the checks in a single transaction to avoid concurrent changes + err := h.readDB.Do(func(tx *db.Tx) error { + var err error + // check project exists + pg, err := h.readDB.GetProjectGroup(tx, req.ProjectGroupRef) + if err != nil { + return err + } + if pg == nil { + return util.NewErrBadRequest(errors.Errorf("project group with ref %q doesn't exist", req.ProjectGroupRef)) + } + // check that the project.ID matches + if pg.ID != req.ProjectGroup.ID { + return util.NewErrBadRequest(errors.Errorf("project group with ref %q has a different id", req.ProjectGroupRef)) + } + + // check parent exists + switch pg.Parent.Type { + case types.ConfigTypeProjectGroup: + group, err := h.readDB.GetProjectGroup(tx, req.ProjectGroup.Parent.ID) + if err != nil { + return err + } + if group == nil { + return util.NewErrBadRequest(errors.Errorf("project group with id %q doesn't exist", req.ProjectGroup.Parent.ID)) + } + } + + // currently we don't support changing parent + // TODO(sgotti) handle project move (changed parent project group) + if pg.Parent.Type != req.ProjectGroup.Parent.Type { + return util.NewErrBadRequest(errors.Errorf("changing project group parent isn't supported")) + } + if pg.Parent.ID != req.ProjectGroup.Parent.ID { + return util.NewErrBadRequest(errors.Errorf("changing project group parent isn't supported")) + } + + // if the project group is a root project group force the name to be empty + if pg.Parent.Type == types.ConfigTypeOrg || + pg.Parent.Type == types.ConfigTypeUser { + req.ProjectGroup.Name = "" + } + + pgp, err := h.readDB.GetProjectGroupPath(tx, pg) + if err != nil { + return err + } + + // changegroup is the project group path. Use "projectpath" prefix as it must + // cover both projects and projectgroups + cgNames := []string{util.EncodeSha256Hex("projectpath-" + pgp)} + cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + pgj, err := json.Marshal(req.ProjectGroup) + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal project") + } + actions := []*datamanager.Action{ + { + ActionType: datamanager.ActionTypePut, + DataType: string(types.ConfigTypeProjectGroup), + ID: req.ProjectGroup.ID, + Data: pgj, + }, + } + + _, err = h.dm.WriteWal(ctx, actions, cgt) + return req.ProjectGroup, err +} + func (h *ActionHandler) DeleteProjectGroup(ctx context.Context, projectGroupRef string) error { var projectGroup *types.ProjectGroup diff --git a/internal/services/configstore/api/client.go b/internal/services/configstore/api/client.go index 474d3b5..c5c7172 100644 --- a/internal/services/configstore/api/client.go +++ b/internal/services/configstore/api/client.go @@ -135,6 +135,17 @@ func (c *Client) CreateProjectGroup(ctx context.Context, projectGroup *types.Pro return resProjectGroup, resp, err } +func (c *Client) UpdateProjectGroup(ctx context.Context, projectGroupRef string, projectGroup *types.ProjectGroup) (*ProjectGroup, *http.Response, error) { + pj, err := json.Marshal(projectGroup) + if err != nil { + return nil, nil, err + } + + resProjectGroup := new(ProjectGroup) + resp, err := c.getParsedResponse(ctx, "PUT", fmt.Sprintf("/projectgroups/%s", url.PathEscape(projectGroupRef)), nil, jsonContent, bytes.NewReader(pj), resProjectGroup) + return resProjectGroup, resp, err +} + func (c *Client) DeleteProjectGroup(ctx context.Context, projectGroupRef string) (*http.Response, error) { return c.getResponse(ctx, "DELETE", fmt.Sprintf("/projectgroups/%s", url.PathEscape(projectGroupRef)), nil, jsonContent, nil) } diff --git a/internal/services/configstore/api/projectgroup.go b/internal/services/configstore/api/projectgroup.go index a931d4c..2aa2406 100644 --- a/internal/services/configstore/api/projectgroup.go +++ b/internal/services/configstore/api/projectgroup.go @@ -244,6 +244,54 @@ func (h *CreateProjectGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Req } } +type UpdateProjectGroupHandler struct { + log *zap.SugaredLogger + ah *action.ActionHandler + readDB *readdb.ReadDB +} + +func NewUpdateProjectGroupHandler(logger *zap.Logger, ah *action.ActionHandler, readDB *readdb.ReadDB) *UpdateProjectGroupHandler { + return &UpdateProjectGroupHandler{log: logger.Sugar(), ah: ah, readDB: readDB} +} + +func (h *UpdateProjectGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + projectGroupRef, err := url.PathUnescape(vars["projectgroupref"]) + if err != nil { + httpError(w, util.NewErrBadRequest(err)) + return + } + + var projectGroup *types.ProjectGroup + d := json.NewDecoder(r.Body) + if err := d.Decode(&projectGroup); err != nil { + httpError(w, util.NewErrBadRequest(err)) + return + } + + areq := &action.UpdateProjectGroupRequest{ + ProjectGroupRef: projectGroupRef, + ProjectGroup: projectGroup, + } + projectGroup, err = h.ah.UpdateProjectGroup(ctx, areq) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + resProjectGroup, err := projectGroupResponse(h.readDB, projectGroup) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + if err := httpResponse(w, http.StatusCreated, resProjectGroup); err != nil { + h.log.Errorf("err: %+v", err) + } +} + type DeleteProjectGroupHandler struct { log *zap.SugaredLogger ah *action.ActionHandler diff --git a/internal/services/configstore/configstore.go b/internal/services/configstore/configstore.go index 6121437..46f50b5 100644 --- a/internal/services/configstore/configstore.go +++ b/internal/services/configstore/configstore.go @@ -129,6 +129,7 @@ func (s *Configstore) Run(ctx context.Context) error { projectGroupSubgroupsHandler := api.NewProjectGroupSubgroupsHandler(logger, s.ah, s.readDB) projectGroupProjectsHandler := api.NewProjectGroupProjectsHandler(logger, s.ah, s.readDB) createProjectGroupHandler := api.NewCreateProjectGroupHandler(logger, s.ah, s.readDB) + updateProjectGroupHandler := api.NewUpdateProjectGroupHandler(logger, s.ah, s.readDB) deleteProjectGroupHandler := api.NewDeleteProjectGroupHandler(logger, s.ah) projectHandler := api.NewProjectHandler(logger, s.readDB) @@ -180,6 +181,7 @@ func (s *Configstore) Run(ctx context.Context) error { apirouter.Handle("/projectgroups/{projectgroupref}/subgroups", projectGroupSubgroupsHandler).Methods("GET") apirouter.Handle("/projectgroups/{projectgroupref}/projects", projectGroupProjectsHandler).Methods("GET") apirouter.Handle("/projectgroups", createProjectGroupHandler).Methods("POST") + apirouter.Handle("/projectgroups/{projectgroupref}", updateProjectGroupHandler).Methods("PUT") apirouter.Handle("/projectgroups/{projectgroupref}", deleteProjectGroupHandler).Methods("DELETE") apirouter.Handle("/projects/{projectref}", projectHandler).Methods("GET") diff --git a/internal/services/gateway/action/projectgroup.go b/internal/services/gateway/action/projectgroup.go index c2c1f54..05840eb 100644 --- a/internal/services/gateway/action/projectgroup.go +++ b/internal/services/gateway/action/projectgroup.go @@ -104,6 +104,38 @@ func (h *ActionHandler) CreateProjectGroup(ctx context.Context, req *CreateProje return rp, nil } +type UpdateProjectGroupRequest struct { + Name string + Visibility types.Visibility +} + +func (h *ActionHandler) UpdateProjectGroup(ctx context.Context, projectGroupRef string, req *UpdateProjectGroupRequest) (*csapi.ProjectGroup, error) { + pg, resp, err := h.configstoreClient.GetProjectGroup(ctx, projectGroupRef) + if err != nil { + return nil, ErrFromRemote(resp, errors.Wrapf(err, "failed to get project group %q", projectGroupRef)) + } + + isProjectOwner, err := h.IsProjectOwner(ctx, pg.OwnerType, pg.OwnerID) + if err != nil { + return nil, errors.Wrapf(err, "failed to determine ownership") + } + if !isProjectOwner { + return nil, util.NewErrForbidden(errors.Errorf("user not authorized")) + } + + pg.Name = req.Name + pg.Visibility = req.Visibility + + h.log.Infof("updating project group") + rp, resp, err := h.configstoreClient.UpdateProjectGroup(ctx, pg.ID, pg.ProjectGroup) + if err != nil { + return nil, ErrFromRemote(resp, errors.Wrapf(err, "failed to update project group")) + } + h.log.Infof("project group %q updated, ID: %s", pg.Name, pg.ID) + + return rp, nil +} + func (h *ActionHandler) DeleteProjectGroup(ctx context.Context, projectRef string) error { p, resp, err := h.configstoreClient.GetProjectGroup(ctx, projectRef) if err != nil { diff --git a/internal/services/gateway/api/projectgroup.go b/internal/services/gateway/api/projectgroup.go index 2e4b0b8..9c5f280 100644 --- a/internal/services/gateway/api/projectgroup.go +++ b/internal/services/gateway/api/projectgroup.go @@ -30,9 +30,9 @@ import ( ) type CreateProjectGroupRequest struct { - Name string `json:"name,omitempty"` - ParentRef string `json:"parent_ref,omitempty"` - Visibility types.Visibility `json:"visibility,omitempty"` + Name string `json:"name"` + ParentRef string `json:"parent_ref"` + Visibility types.Visibility `json:"visibility"` } type CreateProjectGroupHandler struct { @@ -81,6 +81,52 @@ func (h *CreateProjectGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Req } } +type UpdateProjectGroupRequest struct { + Name string `json:"name,omitempty"` + Visibility types.Visibility `json:"visibility,omitempty"` +} + +type UpdateProjectGroupHandler struct { + log *zap.SugaredLogger + ah *action.ActionHandler +} + +func NewUpdateProjectGroupHandler(logger *zap.Logger, ah *action.ActionHandler) *UpdateProjectGroupHandler { + return &UpdateProjectGroupHandler{log: logger.Sugar(), ah: ah} +} + +func (h *UpdateProjectGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + projectGroupRef, err := url.PathUnescape(vars["projectgroupref"]) + if err != nil { + httpError(w, util.NewErrBadRequest(err)) + return + } + + var req UpdateProjectGroupRequest + d := json.NewDecoder(r.Body) + if err := d.Decode(&req); err != nil { + httpError(w, util.NewErrBadRequest(err)) + return + } + + areq := &action.UpdateProjectGroupRequest{ + Name: req.Name, + Visibility: req.Visibility, + } + projectGroup, err := h.ah.UpdateProjectGroup(ctx, projectGroupRef, areq) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + res := createProjectGroupResponse(projectGroup) + if err := httpResponse(w, http.StatusCreated, res); err != nil { + h.log.Errorf("err: %+v", err) + } +} + type DeleteProjectGroupHandler struct { log *zap.SugaredLogger ah *action.ActionHandler @@ -209,12 +255,12 @@ func (h *ProjectGroupSubgroupsHandler) ServeHTTP(w http.ResponseWriter, r *http. } type ProjectGroupResponse struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Path string `json:"path,omitempty"` - ParentPath string `json:"parent_path,omitempty"` - Visibility types.Visibility `json:"visibility,omitempty"` - GlobalVisibility string `json:"global_visibility,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + ParentPath string `json:"parent_path"` + Visibility types.Visibility `json:"visibility"` + GlobalVisibility string `json:"global_visibility"` } func createProjectGroupResponse(r *csapi.ProjectGroup) *ProjectGroupResponse { diff --git a/internal/services/gateway/gateway.go b/internal/services/gateway/gateway.go index 40c5154..74b8635 100644 --- a/internal/services/gateway/gateway.go +++ b/internal/services/gateway/gateway.go @@ -153,6 +153,7 @@ func (g *Gateway) Run(ctx context.Context) error { projectGroupSubgroupsHandler := api.NewProjectGroupSubgroupsHandler(logger, g.ah) projectGroupProjectsHandler := api.NewProjectGroupProjectsHandler(logger, g.ah) createProjectGroupHandler := api.NewCreateProjectGroupHandler(logger, g.ah) + updateProjectGroupHandler := api.NewUpdateProjectGroupHandler(logger, g.ah) deleteProjectGroupHandler := api.NewDeleteProjectGroupHandler(logger, g.ah) projectHandler := api.NewProjectHandler(logger, g.ah) @@ -230,6 +231,7 @@ func (g *Gateway) Run(ctx context.Context) error { apirouter.Handle("/projectgroups/{projectgroupref}/subgroups", authForcedHandler(projectGroupSubgroupsHandler)).Methods("GET") apirouter.Handle("/projectgroups/{projectgroupref}/projects", authForcedHandler(projectGroupProjectsHandler)).Methods("GET") apirouter.Handle("/projectgroups", authForcedHandler(createProjectGroupHandler)).Methods("POST") + apirouter.Handle("/projectgroups/{projectgroupref}", authForcedHandler(updateProjectGroupHandler)).Methods("PUT") apirouter.Handle("/projectgroups/{projectgroupref}", authForcedHandler(deleteProjectGroupHandler)).Methods("DELETE") apirouter.Handle("/projects/{projectref}", authForcedHandler(projectHandler)).Methods("GET")