*: implement update projectgroup

This commit is contained in:
Simone Gotti 2019-05-14 17:53:48 +02:00
parent 749109cd75
commit 6c11ab0596
7 changed files with 274 additions and 19 deletions

View File

@ -73,18 +73,40 @@ func (h *ActionHandler) GetProjectGroupProjects(ctx context.Context, projectGrou
return projects, nil return projects, nil
} }
func (h *ActionHandler) CreateProjectGroup(ctx context.Context, projectGroup *types.ProjectGroup) (*types.ProjectGroup, error) { func (h *ActionHandler) ValidateProjectGroup(ctx context.Context, projectGroup *types.ProjectGroup) error {
if projectGroup.Name == "" { if projectGroup.Parent.Type != types.ConfigTypeProjectGroup &&
return nil, util.NewErrBadRequest(errors.Errorf("project group name required")) projectGroup.Parent.Type != types.ConfigTypeOrg &&
} projectGroup.Parent.Type != types.ConfigTypeUser {
if !util.ValidateName(projectGroup.Name) { return util.NewErrBadRequest(errors.Errorf("invalid project group parent type %q", projectGroup.Parent.Type))
return nil, util.NewErrBadRequest(errors.Errorf("invalid project group name %q", projectGroup.Name))
} }
if projectGroup.Parent.ID == "" { 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) { 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 var cgt *datamanager.ChangeGroupsUpdateToken
@ -131,7 +153,7 @@ func (h *ActionHandler) CreateProjectGroup(ctx context.Context, projectGroup *ty
projectGroup.ID = uuid.NewV4().String() projectGroup.ID = uuid.NewV4().String()
projectGroup.Parent.Type = types.ConfigTypeProjectGroup projectGroup.Parent.Type = types.ConfigTypeProjectGroup
pcj, err := json.Marshal(projectGroup) pgj, err := json.Marshal(projectGroup)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to marshal projectGroup") 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, ActionType: datamanager.ActionTypePut,
DataType: string(types.ConfigTypeProjectGroup), DataType: string(types.ConfigTypeProjectGroup),
ID: projectGroup.ID, ID: projectGroup.ID,
Data: pcj, Data: pgj,
}, },
} }
@ -148,6 +170,98 @@ func (h *ActionHandler) CreateProjectGroup(ctx context.Context, projectGroup *ty
return projectGroup, err 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 { func (h *ActionHandler) DeleteProjectGroup(ctx context.Context, projectGroupRef string) error {
var projectGroup *types.ProjectGroup var projectGroup *types.ProjectGroup

View File

@ -135,6 +135,17 @@ func (c *Client) CreateProjectGroup(ctx context.Context, projectGroup *types.Pro
return resProjectGroup, resp, err 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) { 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) return c.getResponse(ctx, "DELETE", fmt.Sprintf("/projectgroups/%s", url.PathEscape(projectGroupRef)), nil, jsonContent, nil)
} }

View File

@ -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 { type DeleteProjectGroupHandler struct {
log *zap.SugaredLogger log *zap.SugaredLogger
ah *action.ActionHandler ah *action.ActionHandler

View File

@ -129,6 +129,7 @@ func (s *Configstore) Run(ctx context.Context) error {
projectGroupSubgroupsHandler := api.NewProjectGroupSubgroupsHandler(logger, s.ah, s.readDB) projectGroupSubgroupsHandler := api.NewProjectGroupSubgroupsHandler(logger, s.ah, s.readDB)
projectGroupProjectsHandler := api.NewProjectGroupProjectsHandler(logger, s.ah, s.readDB) projectGroupProjectsHandler := api.NewProjectGroupProjectsHandler(logger, s.ah, s.readDB)
createProjectGroupHandler := api.NewCreateProjectGroupHandler(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) deleteProjectGroupHandler := api.NewDeleteProjectGroupHandler(logger, s.ah)
projectHandler := api.NewProjectHandler(logger, s.readDB) 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}/subgroups", projectGroupSubgroupsHandler).Methods("GET")
apirouter.Handle("/projectgroups/{projectgroupref}/projects", projectGroupProjectsHandler).Methods("GET") apirouter.Handle("/projectgroups/{projectgroupref}/projects", projectGroupProjectsHandler).Methods("GET")
apirouter.Handle("/projectgroups", createProjectGroupHandler).Methods("POST") apirouter.Handle("/projectgroups", createProjectGroupHandler).Methods("POST")
apirouter.Handle("/projectgroups/{projectgroupref}", updateProjectGroupHandler).Methods("PUT")
apirouter.Handle("/projectgroups/{projectgroupref}", deleteProjectGroupHandler).Methods("DELETE") apirouter.Handle("/projectgroups/{projectgroupref}", deleteProjectGroupHandler).Methods("DELETE")
apirouter.Handle("/projects/{projectref}", projectHandler).Methods("GET") apirouter.Handle("/projects/{projectref}", projectHandler).Methods("GET")

View File

@ -104,6 +104,38 @@ func (h *ActionHandler) CreateProjectGroup(ctx context.Context, req *CreateProje
return rp, nil 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 { func (h *ActionHandler) DeleteProjectGroup(ctx context.Context, projectRef string) error {
p, resp, err := h.configstoreClient.GetProjectGroup(ctx, projectRef) p, resp, err := h.configstoreClient.GetProjectGroup(ctx, projectRef)
if err != nil { if err != nil {

View File

@ -30,9 +30,9 @@ import (
) )
type CreateProjectGroupRequest struct { type CreateProjectGroupRequest struct {
Name string `json:"name,omitempty"` Name string `json:"name"`
ParentRef string `json:"parent_ref,omitempty"` ParentRef string `json:"parent_ref"`
Visibility types.Visibility `json:"visibility,omitempty"` Visibility types.Visibility `json:"visibility"`
} }
type CreateProjectGroupHandler struct { 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 { type DeleteProjectGroupHandler struct {
log *zap.SugaredLogger log *zap.SugaredLogger
ah *action.ActionHandler ah *action.ActionHandler
@ -209,12 +255,12 @@ func (h *ProjectGroupSubgroupsHandler) ServeHTTP(w http.ResponseWriter, r *http.
} }
type ProjectGroupResponse struct { type ProjectGroupResponse struct {
ID string `json:"id,omitempty"` ID string `json:"id"`
Name string `json:"name,omitempty"` Name string `json:"name"`
Path string `json:"path,omitempty"` Path string `json:"path"`
ParentPath string `json:"parent_path,omitempty"` ParentPath string `json:"parent_path"`
Visibility types.Visibility `json:"visibility,omitempty"` Visibility types.Visibility `json:"visibility"`
GlobalVisibility string `json:"global_visibility,omitempty"` GlobalVisibility string `json:"global_visibility"`
} }
func createProjectGroupResponse(r *csapi.ProjectGroup) *ProjectGroupResponse { func createProjectGroupResponse(r *csapi.ProjectGroup) *ProjectGroupResponse {

View File

@ -153,6 +153,7 @@ func (g *Gateway) Run(ctx context.Context) error {
projectGroupSubgroupsHandler := api.NewProjectGroupSubgroupsHandler(logger, g.ah) projectGroupSubgroupsHandler := api.NewProjectGroupSubgroupsHandler(logger, g.ah)
projectGroupProjectsHandler := api.NewProjectGroupProjectsHandler(logger, g.ah) projectGroupProjectsHandler := api.NewProjectGroupProjectsHandler(logger, g.ah)
createProjectGroupHandler := api.NewCreateProjectGroupHandler(logger, g.ah) createProjectGroupHandler := api.NewCreateProjectGroupHandler(logger, g.ah)
updateProjectGroupHandler := api.NewUpdateProjectGroupHandler(logger, g.ah)
deleteProjectGroupHandler := api.NewDeleteProjectGroupHandler(logger, g.ah) deleteProjectGroupHandler := api.NewDeleteProjectGroupHandler(logger, g.ah)
projectHandler := api.NewProjectHandler(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}/subgroups", authForcedHandler(projectGroupSubgroupsHandler)).Methods("GET")
apirouter.Handle("/projectgroups/{projectgroupref}/projects", authForcedHandler(projectGroupProjectsHandler)).Methods("GET") apirouter.Handle("/projectgroups/{projectgroupref}/projects", authForcedHandler(projectGroupProjectsHandler)).Methods("GET")
apirouter.Handle("/projectgroups", authForcedHandler(createProjectGroupHandler)).Methods("POST") 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("/projectgroups/{projectgroupref}", authForcedHandler(deleteProjectGroupHandler)).Methods("DELETE")
apirouter.Handle("/projects/{projectref}", authForcedHandler(projectHandler)).Methods("GET") apirouter.Handle("/projects/{projectref}", authForcedHandler(projectHandler)).Methods("GET")