Skip to content

Commit

Permalink
added logic and routes for read, create, update, and delete.
Browse files Browse the repository at this point in the history
  • Loading branch information
rimashah25 committed Aug 17, 2023
1 parent 7aab64d commit 5e7b07f
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 16 deletions.
15 changes: 7 additions & 8 deletions lib/go-tc/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
2 changes: 1 addition & 1 deletion traffic_ops/testing/api/v5/traffic_control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
7 changes: 7 additions & 0 deletions traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
257 changes: 254 additions & 3 deletions traffic_ops/traffic_ops_golang/profile/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ package profile
*/

import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
8 changes: 4 additions & 4 deletions traffic_ops/traffic_ops_golang/routing/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down

0 comments on commit 5e7b07f

Please sign in to comment.