diff --git a/internal/services/configstore/action/variable.go b/internal/services/configstore/action/variable.go index 71f78d4..0dfa59c 100644 --- a/internal/services/configstore/action/variable.go +++ b/internal/services/configstore/action/variable.go @@ -48,24 +48,32 @@ func (h *ActionHandler) GetVariables(ctx context.Context, parentType types.Confi return variables, nil } -func (h *ActionHandler) CreateVariable(ctx context.Context, variable *types.Variable) (*types.Variable, error) { +func (h *ActionHandler) ValidateVariable(ctx context.Context, variable *types.Variable) error { if variable.Name == "" { - return nil, util.NewErrBadRequest(errors.Errorf("variable name required")) + return util.NewErrBadRequest(errors.Errorf("variable name required")) } if !util.ValidateName(variable.Name) { - return nil, util.NewErrBadRequest(errors.Errorf("invalid variable name %q", variable.Name)) + return util.NewErrBadRequest(errors.Errorf("invalid variable name %q", variable.Name)) } if len(variable.Values) == 0 { - return nil, util.NewErrBadRequest(errors.Errorf("variable values required")) + return util.NewErrBadRequest(errors.Errorf("variable values required")) } if variable.Parent.Type == "" { - return nil, util.NewErrBadRequest(errors.Errorf("variable parent type required")) + return util.NewErrBadRequest(errors.Errorf("variable parent type required")) } if variable.Parent.ID == "" { - return nil, util.NewErrBadRequest(errors.Errorf("variable parent id required")) + return util.NewErrBadRequest(errors.Errorf("variable parent id required")) } if variable.Parent.Type != types.ConfigTypeProject && variable.Parent.Type != types.ConfigTypeProjectGroup { - return nil, util.NewErrBadRequest(errors.Errorf("invalid variable parent type %q", variable.Parent.Type)) + return util.NewErrBadRequest(errors.Errorf("invalid variable parent type %q", variable.Parent.Type)) + } + + return nil +} + +func (h *ActionHandler) CreateVariable(ctx context.Context, variable *types.Variable) (*types.Variable, error) { + if err := h.ValidateVariable(ctx, variable); err != nil { + return nil, err } var cgt *datamanager.ChangeGroupsUpdateToken @@ -120,6 +128,86 @@ func (h *ActionHandler) CreateVariable(ctx context.Context, variable *types.Vari return variable, err } +type UpdateVariableRequest struct { + VariableName string + + Variable *types.Variable +} + +func (h *ActionHandler) UpdateVariable(ctx context.Context, req *UpdateVariableRequest) (*types.Variable, error) { + if err := h.ValidateVariable(ctx, req.Variable); err != nil { + return nil, err + } + + var curVariable *types.Variable + var cgt *datamanager.ChangeGroupsUpdateToken + // changegroup is the variable name + + // 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 + + parentID, err := h.readDB.ResolveConfigID(tx, req.Variable.Parent.Type, req.Variable.Parent.ID) + if err != nil { + return err + } + req.Variable.Parent.ID = parentID + + // check variable exists + curVariable, err = h.readDB.GetVariableByName(tx, req.Variable.Parent.ID, req.VariableName) + if err != nil { + return err + } + if curVariable == nil { + return util.NewErrBadRequest(errors.Errorf("variable with name %q for %s with id %q doesn't exists", req.VariableName, req.Variable.Parent.Type, req.Variable.Parent.ID)) + } + + if curVariable.Name != req.Variable.Name { + // check duplicate variable name + u, err := h.readDB.GetVariableByName(tx, req.Variable.Parent.ID, req.Variable.Name) + if err != nil { + return err + } + if u != nil { + return util.NewErrBadRequest(errors.Errorf("variable with name %q for %s with id %q already exists", req.Variable.Name, req.Variable.Parent.Type, req.Variable.Parent.ID)) + } + } + + // set/override ID that must be kept from the current variable + req.Variable.ID = curVariable.ID + + cgNames := []string{ + util.EncodeSha256Hex("variablename-" + req.Variable.ID), + util.EncodeSha256Hex("variablename-" + req.Variable.Name), + } + cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + variablej, err := json.Marshal(req.Variable) + if err != nil { + return nil, errors.Errorf("failed to marshal variable: %w", err) + } + actions := []*datamanager.Action{ + { + ActionType: datamanager.ActionTypePut, + DataType: string(types.ConfigTypeVariable), + ID: req.Variable.ID, + Data: variablej, + }, + } + + _, err = h.dm.WriteWal(ctx, actions, cgt) + return req.Variable, err +} + func (h *ActionHandler) DeleteVariable(ctx context.Context, parentType types.ConfigType, parentRef, variableName string) error { var variable *types.Variable diff --git a/internal/services/configstore/api/client.go b/internal/services/configstore/api/client.go index 8598114..608a1fc 100644 --- a/internal/services/configstore/api/client.go +++ b/internal/services/configstore/api/client.go @@ -267,6 +267,17 @@ func (c *Client) CreateProjectGroupVariable(ctx context.Context, projectGroupRef return resVariable, resp, err } +func (c *Client) UpdateProjectGroupVariable(ctx context.Context, projectGroupRef, variableName string, variable *types.Variable) (*Variable, *http.Response, error) { + pj, err := json.Marshal(variable) + if err != nil { + return nil, nil, err + } + + resVariable := new(Variable) + resp, err := c.getParsedResponse(ctx, "PUT", fmt.Sprintf("/projectgroups/%s/variables/%s", url.PathEscape(projectGroupRef), variableName), nil, jsonContent, bytes.NewReader(pj), resVariable) + return resVariable, resp, err +} + func (c *Client) CreateProjectVariable(ctx context.Context, projectRef string, variable *types.Variable) (*Variable, *http.Response, error) { pj, err := json.Marshal(variable) if err != nil { @@ -278,6 +289,17 @@ func (c *Client) CreateProjectVariable(ctx context.Context, projectRef string, v return resVariable, resp, err } +func (c *Client) UpdateProjectVariable(ctx context.Context, projectRef, variableName string, variable *types.Variable) (*Variable, *http.Response, error) { + pj, err := json.Marshal(variable) + if err != nil { + return nil, nil, err + } + + resVariable := new(Variable) + resp, err := c.getParsedResponse(ctx, "PUT", fmt.Sprintf("/projects/%s/variables/%s", url.PathEscape(projectRef), variableName), nil, jsonContent, bytes.NewReader(pj), resVariable) + return resVariable, resp, err +} + func (c *Client) DeleteProjectGroupVariable(ctx context.Context, projectGroupRef, variableName string) (*http.Response, error) { return c.getResponse(ctx, "DELETE", fmt.Sprintf("/projectgroups/%s/variables/%s", url.PathEscape(projectGroupRef), variableName), nil, jsonContent, nil) } diff --git a/internal/services/configstore/api/variable.go b/internal/services/configstore/api/variable.go index b0c4073..f3076c6 100644 --- a/internal/services/configstore/api/variable.go +++ b/internal/services/configstore/api/variable.go @@ -127,6 +127,51 @@ func (h *CreateVariableHandler) ServeHTTP(w http.ResponseWriter, r *http.Request } } +type UpdateVariableHandler struct { + log *zap.SugaredLogger + ah *action.ActionHandler +} + +func NewUpdateVariableHandler(logger *zap.Logger, ah *action.ActionHandler) *UpdateVariableHandler { + return &UpdateVariableHandler{log: logger.Sugar(), ah: ah} +} + +func (h *UpdateVariableHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + variableName := vars["variablename"] + + parentType, parentRef, err := GetConfigTypeRef(r) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + var variable *types.Variable + d := json.NewDecoder(r.Body) + if err := d.Decode(&variable); err != nil { + httpError(w, util.NewErrBadRequest(err)) + return + } + + variable.Parent.Type = parentType + variable.Parent.ID = parentRef + + areq := &action.UpdateVariableRequest{ + VariableName: variableName, + Variable: variable, + } + variable, err = h.ah.UpdateVariable(ctx, areq) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + if err := httpResponse(w, http.StatusOK, variable); err != nil { + h.log.Errorf("err: %+v", err) + } +} + type DeleteVariableHandler struct { log *zap.SugaredLogger ah *action.ActionHandler diff --git a/internal/services/configstore/configstore.go b/internal/services/configstore/configstore.go index 7174859..a2d7bb9 100644 --- a/internal/services/configstore/configstore.go +++ b/internal/services/configstore/configstore.go @@ -138,6 +138,7 @@ func (s *Configstore) Run(ctx context.Context) error { variablesHandler := api.NewVariablesHandler(logger, s.ah, s.readDB) createVariableHandler := api.NewCreateVariableHandler(logger, s.ah) + updateVariableHandler := api.NewUpdateVariableHandler(logger, s.ah) deleteVariableHandler := api.NewDeleteVariableHandler(logger, s.ah) userHandler := api.NewUserHandler(logger, s.readDB) @@ -196,6 +197,8 @@ func (s *Configstore) Run(ctx context.Context) error { apirouter.Handle("/projects/{projectref}/variables", variablesHandler).Methods("GET") apirouter.Handle("/projectgroups/{projectgroupref}/variables", createVariableHandler).Methods("POST") apirouter.Handle("/projects/{projectref}/variables", createVariableHandler).Methods("POST") + apirouter.Handle("/projectgroups/{projectgroupref}/variables/{variablename}", updateVariableHandler).Methods("PUT") + apirouter.Handle("/projects/{projectref}/variables/{variablename}", updateVariableHandler).Methods("PUT") apirouter.Handle("/projectgroups/{projectgroupref}/variables/{variablename}", deleteVariableHandler).Methods("DELETE") apirouter.Handle("/projects/{projectref}/variables/{variablename}", deleteVariableHandler).Methods("DELETE")