From 5e7b07f7309f3279c6520cc23028b1350dfafc68 Mon Sep 17 00:00:00 2001 From: Rima Shah Date: Thu, 17 Aug 2023 17:44:07 -0600 Subject: [PATCH] added logic and routes for read, create, update, and delete. --- lib/go-tc/profiles.go | 15 +- .../testing/api/v5/traffic_control_test.go | 2 +- .../dbhelpers/db_helpers.go | 7 + .../traffic_ops_golang/profile/profiles.go | 257 +++++++++++++++++- .../traffic_ops_golang/routing/routes.go | 8 +- 5 files changed, 273 insertions(+), 16 deletions(-) diff --git a/lib/go-tc/profiles.go b/lib/go-tc/profiles.go index 890c966bef..eff7516954 100644 --- a/lib/go-tc/profiles.go +++ b/lib/go-tc/profiles.go @@ -97,14 +97,13 @@ type Profile struct { // which it is assigned. Note: Field LastUpdated represents RFC3339 type ProfileV5 struct { ID int `json:"id" db:"id"` - LastUpdated time.Time `json:"lastUpdated"` - Name string `json:"name"` - Parameter string `json:"param"` - Description string `json:"description"` - CDNName string `json:"cdnName"` - CDNID int `json:"cdn"` - RoutingDisabled bool `json:"routingDisabled"` - Type string `json:"type"` + LastUpdated time.Time `json:"lastUpdated" db:"last_updated"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + CDNName string `json:"cdnName" db:"cdn_name"` + CDNID int `json:"cdn" db:"cdn"` + RoutingDisabled bool `json:"routingDisabled" db:"routing_disabled"` + Type string `json:"type" db:"type"` Parameters []ParameterNullable `json:"params,omitempty"` } diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go b/traffic_ops/testing/api/v5/traffic_control_test.go index 4d9db708b5..7ae2a32ec7 100644 --- a/traffic_ops/testing/api/v5/traffic_control_test.go +++ b/traffic_ops/testing/api/v5/traffic_control_test.go @@ -39,7 +39,7 @@ type TrafficControl struct { FederationResolvers []tc.FederationResolverV5 `json:"federation_resolvers"` Jobs []tc.InvalidationJobCreateV4 `json:"jobs"` Origins []tc.Origin `json:"origins"` - Profiles []tc.Profile `json:"profiles"` + Profiles []tc.ProfileV5 `json:"profiles"` Parameters []tc.Parameter `json:"parameters"` ProfileParameters []tc.ProfileParameter `json:"profileParameters"` PhysLocations []tc.PhysLocationV5 `json:"physLocations"` diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go index c70056279b..1155a123f5 100644 --- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go +++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go @@ -2263,6 +2263,13 @@ func PhysLocationExists(tx *sql.Tx, id string) (bool, error) { return true, nil } +func ProfileExists(tx *sql.Tx, id string) (bool, error) { + row := tx.QueryRow(`SELECT EXISTS(SELECT * FROM profile WHERE profile.id=$1)`, id) + var exists bool + err := row.Scan(&exists) + return exists, err +} + // GetCoordinateID obtains coordinateID, and an error (if one occurs) func GetCoordinateID(tx *sql.Tx, id int) (*int, error) { q := `SELECT coordinate FROM cachegroup WHERE id = $1` diff --git a/traffic_ops/traffic_ops_golang/profile/profiles.go b/traffic_ops/traffic_ops_golang/profile/profiles.go index df05c50f4c..58b409d5ae 100644 --- a/traffic_ops/traffic_ops_golang/profile/profiles.go +++ b/traffic_ops/traffic_ops_golang/profile/profiles.go @@ -25,6 +25,8 @@ package profile */ import ( + "database/sql" + "encoding/json" "errors" "fmt" "net/http" @@ -189,7 +191,7 @@ func (prof *TOProfile) Read(h http.Header, useIMS bool) ([]interface{}, error, e for _, profile := range profiles { // Attach Parameters if the 'id' parameter is sent if _, ok := prof.APIInfo().Params[IDQueryParam]; ok { - profile.Parameters, err = ReadParameters(prof.ReqInfo.Tx, prof.APIInfo().Params, prof.ReqInfo.User, profile) + profile.Parameters, err = ReadParameters(prof.ReqInfo.Tx, prof.ReqInfo.User, profile.ID) if err != nil { return nil, nil, errors.New("profile read reading parameters: " + err.Error()), http.StatusInternalServerError, nil } @@ -226,10 +228,10 @@ LEFT JOIN cdn c ON prof.cdn = c.id` return query } -func ReadParameters(tx *sqlx.Tx, parameters map[string]string, user *auth.CurrentUser, profile tc.ProfileNullable) ([]tc.ParameterNullable, error) { +func ReadParameters(tx *sqlx.Tx, user *auth.CurrentUser, profileID *int) ([]tc.ParameterNullable, error) { privLevel := user.PrivLevel queryValues := make(map[string]interface{}) - queryValues["profile_id"] = *profile.ID + queryValues["profile_id"] = profileID query := selectParametersQuery() rows, err := tx.NamedQuery(query, queryValues) @@ -360,3 +362,252 @@ type) VALUES ( func deleteQuery() string { return `DELETE FROM profile WHERE id = :id` } + +// Read gets list of Profiles for APIv5 +func Read(w http.ResponseWriter, r *http.Request) { + var runSecond bool + var maxTime time.Time + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + tx := inf.Tx + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + // Query Parameters to Database Query column mappings + queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{ + "cdn": {Column: "c.id", Checker: api.IsInt}, + "name": {Column: "prof.name", Checker: nil}, + "id": {Column: "prof.id", Checker: api.IsInt}, + "param": {Column: "pp.parameter", Checker: api.IsInt}, + } + + query := selectProfilesQuery() + if paramValue, ok := inf.Params["param"]; ok { + if len(paramValue) > 0 { + query += " LEFT JOIN profile_parameter pp ON prof.id = pp.profile" + } + } + + where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, queryParamsToQueryCols) + if len(errs) > 0 { + api.HandleErr(w, r, tx.Tx, http.StatusBadRequest, util.JoinErrs(errs), nil) + } + + if inf.Config.UseIMS { + runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, r.Header, queryValues, selectMaxLastUpdatedQuery(where)) + if !runSecond { + log.Debugln("IMS HIT") + api.AddLastModifiedHdr(w, maxTime) + w.WriteHeader(http.StatusNotModified) + return + } + log.Debugln("IMS MISS") + } else { + log.Debugln("Non IMS request") + } + + query += where + orderBy + pagination + rows, err := tx.NamedQuery(query, queryValues) + if err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("profile read: error getting profile(s): %w", err)) + } + defer log.Close(rows, "unable to close DB connection") + + profile := tc.ProfileV5{} + profileList := []tc.ProfileV5{} + for rows.Next() { + if err = rows.StructScan(&profile); err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("error getting profile(s): %w", err)) + } + profileList = append(profileList, profile) + } + rows.Close() + profileInterfaces := []interface{}{} + for _, p := range profileList { + // Attach Parameters if the 'id' parameter is sent + if _, ok := inf.Params["id"]; ok { + profile.Parameters, err = ReadParameters(inf.Tx, inf.User, &p.ID) + if err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("profile read: error reading parameters for a profile: %w", err)) + } + } + profileInterfaces = append(profileInterfaces, profile) + } + + api.WriteResp(w, r, profileList) + return +} + +// Create a Profile for APIv5 +func Create(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + tx := inf.Tx.Tx + + profile, readValErr := readAndValidateJsonStruct(r) + if readValErr != nil { + api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil) + return + } + + // check if profile already exists + var count int + err := tx.QueryRow("SELECT count(*) from profile where name=$1", profile.Name).Scan(&count) + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if profile '%s' exists", err, profile.Name)) + return + } + if count == 1 { + api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("profile:'%s' already exists", profile.Name), nil) + return + } + + // create profile + query := `INSERT INTO profile (name, cdn, type, routing_disabled, description) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, last_updated, name, description, (select name FROM cdn where id = $2), cdn, routing_disabled, type` + + err = tx.QueryRow(query, profile.Name, profile.CDNID, profile.Type, profile.RoutingDisabled, profile.Description). + Scan(&profile.ID, &profile.LastUpdated, &profile.Name, &profile.Description, &profile.CDNName, &profile.CDNID, &profile.RoutingDisabled, &profile.Type) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("error: %w in creating profile:%s", err, profile.Name), nil) + return + } + usrErr, sysErr, code := api.ParseDBError(err) + api.HandleErr(w, r, tx, code, usrErr, sysErr) + return + } + + alerts := tc.CreateAlerts(tc.SuccessLevel, "profile was created.") + w.Header().Set("Location", fmt.Sprintf("/api/%d.%d/profiles?id=%d", inf.Version.Major, inf.Version.Minor, profile.ID)) + api.WriteAlertsObj(w, r, http.StatusCreated, alerts, profile) + return +} + +// Update a profile for APIv5 +func Update(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"}) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + tx := inf.Tx.Tx + profile, readValErr := readAndValidateJsonStruct(r) + if readValErr != nil { + api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil) + return + } + + requestedProfileId := inf.IntParams["id"] + // check if the entity was already updated + userErr, sysErr, errCode = api.CheckIfUnModified(r.Header, inf.Tx, requestedProfileId, "profile") + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, errCode, userErr, sysErr) + return + } + + //update profile + query := `UPDATE profile SET + name = $2, + cdn = $3, + type = $4, + routing_disabled = $5, + description = $6 + WHERE id = $1 + RETURNING id, last_updated, name, description, (select name FROM cdn where id = $3), cdn, routing_disabled, type` + + err := tx.QueryRow(query, requestedProfileId, profile.Name, profile.CDNID, profile.Type, profile.RoutingDisabled, profile.Description). + Scan(&profile.ID, &profile.LastUpdated, &profile.Name, &profile.Description, &profile.CDNName, &profile.CDNID, &profile.RoutingDisabled, &profile.Type) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("profile: %s not found", profile.Name), nil) + return + } + usrErr, sysErr, code := api.ParseDBError(err) + api.HandleErr(w, r, tx, code, usrErr, sysErr) + return + } + + alerts := tc.CreateAlerts(tc.SuccessLevel, "profile was updated") + api.WriteAlertsObj(w, r, http.StatusOK, alerts, profile) + return +} + +// Delete an profile for APIv5 +func Delete(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + tx := inf.Tx.Tx + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + id := inf.Params["id"] + exists, err := dbhelpers.ProfileExists(tx, id) + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) + return + } + if !exists { + if id != "" { + api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("no profile exists by id: %s", id), nil) + return + } else { + api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("no profile exists for empty id"), nil) + return + } + } + + res, err := tx.Exec("DELETE FROM profile WHERE id=$1", id) + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) + return + } + rowsAffected, err := res.RowsAffected() + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("determining rows affected for delete profile: %w", err)) + return + } + if rowsAffected == 0 { + api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("no rows deleted for profile"), nil) + return + } + alerts := tc.CreateAlerts(tc.SuccessLevel, "profile was deleted.") + api.WriteAlerts(w, r, http.StatusOK, alerts) + return +} + +// readAndValidateJsonStruct reads json body and validates json fields +func readAndValidateJsonStruct(r *http.Request) (tc.ProfileV5, error) { + var profile tc.ProfileV5 + if err := json.NewDecoder(r.Body).Decode(&profile); err != nil { + userErr := fmt.Errorf("error decoding POST request body into ASNV5 struct %w", err) + return profile, userErr + } + + rule := validation.NewStringRule(tovalidate.IsAlphanumericUnderscoreDash, "must consist of only alphanumeric, dash, or underscore characters") + // validate JSON body + errs := tovalidate.ToErrors(validation.Errors{ + "name": validation.Validate(profile.Name, validation.NotNil, rule), + "cdn": validation.Validate(profile.CDNID, validation.NotNil, validation.Min(0)), + "type": validation.Validate(profile.Type, validation.NotNil), + "routingDisabled": validation.Validate(profile.RoutingDisabled, validation.NotNil), + "description": validation.Validate(profile.Description, validation.Required), + }) + if len(errs) > 0 { + userErr := util.JoinErrs(errs) + return profile, userErr + } + return profile, nil +} diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go index 0a6285cca1..8d0b110d58 100644 --- a/traffic_ops/traffic_ops_golang/routing/routes.go +++ b/traffic_ops/traffic_ops_golang/routing/routes.go @@ -266,10 +266,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) { {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `vault/ping/?$`, Handler: ping.Vault, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"TRAFFIC-VAULT:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 488401211431}, //Profile: CRUD - {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `profiles/?$`, Handler: api.ReadHandler(&profile.TOProfile{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"PROFILE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 46875858931}, - {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `profiles/{id}$`, Handler: api.UpdateHandler(&profile.TOProfile{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PROFILE:UPDATE", "PROFILE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4843917231}, - {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `profiles/?$`, Handler: api.CreateHandler(&profile.TOProfile{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PROFILE:CREATE", "PROFILE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 454021155631}, - {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `profiles/{id}$`, Handler: api.DeleteHandler(&profile.TOProfile{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PROFILE:DELETE", "PROFILE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 420559446531}, + {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `profiles/?$`, Handler: profile.Read, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"PROFILE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 46875858931}, + {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `profiles/{id}$`, Handler: profile.Update, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PROFILE:UPDATE", "PROFILE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4843917231}, + {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `profiles/?$`, Handler: profile.Create, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PROFILE:CREATE", "PROFILE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 454021155631}, + {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `profiles/{id}$`, Handler: profile.Delete, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PROFILE:DELETE", "PROFILE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 420559446531}, {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `profiles/{id}/export/?$`, Handler: profile.ExportProfileHandler, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"PROFILE:READ", "PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4013351731}, {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `profiles/import/?$`, Handler: profile.ImportProfileHandler, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PROFILE:CREATE", "PARAMETER:CREATE", "PROFILE:READ", "PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 40614320831},