traffic_ops/traffic_ops_golang/coordinate/coordinates.go (350 lines of code) (raw):

// Package coordinate contains API handlers and associated logic for servicing // the `/coordinates` API endpoint. package coordinate /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import ( "database/sql" "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time" "github.com/apache/trafficcontrol/v8/lib/go-log" "github.com/apache/trafficcontrol/v8/lib/go-rfc" "github.com/apache/trafficcontrol/v8/lib/go-tc" "github.com/apache/trafficcontrol/v8/lib/go-tc/tovalidate" "github.com/apache/trafficcontrol/v8/lib/go-util" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/api" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/dbhelpers" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/util/ims" validation "github.com/go-ozzo/ozzo-validation" ) // TOCoordinate is a "CRUDer"-based API wrapper for Coordinate objects. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. type TOCoordinate struct { api.APIInfoImpl `json:"-"` tc.CoordinateNullable } // SetLastUpdated implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (coordinate *TOCoordinate) SetLastUpdated(t tc.TimeNoMod) { coordinate.LastUpdated = &t } // InsertQuery implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (*TOCoordinate) InsertQuery() string { return insertQuery() } // NewReadObj implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (*TOCoordinate) NewReadObj() interface{} { return &tc.CoordinateNullable{} } // SelectQuery implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (*TOCoordinate) SelectQuery() string { return selectQuery() } // ParamColumns implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (*TOCoordinate) ParamColumns() map[string]dbhelpers.WhereColumnInfo { return map[string]dbhelpers.WhereColumnInfo{ "id": {Column: "id", Checker: api.IsInt}, "name": {Column: "name"}, } } // GetLastUpdated implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (coordinate *TOCoordinate) GetLastUpdated() (*time.Time, bool, error) { return api.GetLastUpdated(coordinate.APIInfo().Tx, *coordinate.ID, "coordinate") } // UpdateQuery implements a "CRUD"er interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (*TOCoordinate) UpdateQuery() string { return updateQuery() } // DeleteQuery implements a "CRUD"er interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (*TOCoordinate) DeleteQuery() string { return deleteQuery() } // GetKeyFieldsInfo implements a "CRUD"er interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (coordinate TOCoordinate) GetKeyFieldsInfo() []api.KeyFieldInfo { return []api.KeyFieldInfo{{Field: "id", Func: api.GetIntKey}} } // GetKeys implements the Identifier and Validator interfaces. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (coordinate TOCoordinate) GetKeys() (map[string]interface{}, bool) { if coordinate.ID == nil { return map[string]interface{}{"id": 0}, false } return map[string]interface{}{"id": *coordinate.ID}, true } // GetAuditName implements a "CRUD"er interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (coordinate TOCoordinate) GetAuditName() string { if coordinate.Name != nil { return *coordinate.Name } if coordinate.ID != nil { return strconv.Itoa(*coordinate.ID) } return "0" } // GetType implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (coordinate TOCoordinate) GetType() string { return "coordinate" } // SetKeys implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (coordinate *TOCoordinate) SetKeys(keys map[string]interface{}) { i, _ := keys["id"].(int) coordinate.ID = &i } func isValidCoordinateChar(r rune) bool { if r >= 'a' && r <= 'z' { return true } if r >= 'A' && r <= 'Z' { return true } if r >= '0' && r <= '9' { return true } if r == '.' || r == '-' || r == '_' { return true } return false } // IsValidCoordinateName returns true if the name contains only characters valid // for a Coordinate name. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func IsValidCoordinateName(str string) bool { i := strings.IndexFunc(str, func(r rune) bool { return !isValidCoordinateChar(r) }) return i == -1 } // Validate fulfills the api.Validator interface. // Deprecated: All future Coordinate versions should use non-"CRUDer" // validation. func (coordinate TOCoordinate) Validate() (error, error) { validName := validation.NewStringRule(IsValidCoordinateName, "invalid characters found - Use alphanumeric . or - or _ .") latitudeErr := "Must be a floating point number within the range +-90" longitudeErr := "Must be a floating point number within the range +-180" errs := validation.Errors{ "name": validation.Validate(coordinate.Name, validation.Required, validName), "latitude": validation.Validate(coordinate.Latitude, validation.Min(-90.0).Error(latitudeErr), validation.Max(90.0).Error(latitudeErr)), "longitude": validation.Validate(coordinate.Longitude, validation.Min(-180.0).Error(longitudeErr), validation.Max(180.0).Error(longitudeErr)), } return util.JoinErrs(tovalidate.ToErrors(errs)), nil } // Create implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" Create // function. func (coord *TOCoordinate) Create() (error, error, int) { return api.GenericCreate(coord) } // Read implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" Read // function. func (coord *TOCoordinate) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) { api.DefaultSort(coord.APIInfo(), "name") return api.GenericRead(h, coord, useIMS) } func selectMaxLastUpdatedQuery(where, orderBy, pagination string) string { return ` SELECT max(t) FROM ( SELECT max(last_updated) AS t FROM ( SELECT * FROM coordinate c ` + where + orderBy + pagination + ` ) AS coords UNION ALL SELECT max(last_updated) AS t FROM last_deleted l WHERE l.table_name='coordinate' ) AS res` } // SelectMaxLastUpdatedQuery implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (*TOCoordinate) SelectMaxLastUpdatedQuery(where, orderBy, pagination, _ string) string { return selectMaxLastUpdatedQuery(where, orderBy, pagination) } // Update implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" Update // function. func (coord *TOCoordinate) Update(h http.Header) (error, error, int) { return api.GenericUpdate(h, coord) } // Delete implements a "CRUDer" interface. // Deprecated: All future Coordinate versions should use the non-"CRUDer" // methodology. func (coord *TOCoordinate) Delete() (error, error, int) { return api.GenericDelete(coord) } const readQuery = ` SELECT id, latitude, longitude, last_updated, name FROM coordinate c` func selectQuery() string { return readQuery } const putQuery = ` UPDATE coordinate SET latitude=$1, longitude=$2, name=$3 WHERE id=$4 RETURNING last_updated` func updateQuery() string { query := `UPDATE coordinate SET latitude=:latitude, longitude=:longitude, name=:name WHERE id=:id RETURNING last_updated` return query } const createQuery = ` INSERT INTO coordinate ( latitude, longitude, name ) VALUES ( $1, $2, $3 ) RETURNING id, last_updated` func insertQuery() string { query := `INSERT INTO coordinate ( latitude, longitude, name) VALUES ( :latitude, :longitude, :name) RETURNING id,last_updated` return query } const delQuery = ` DELETE FROM coordinate WHERE id = $1 RETURNING latitude, longitude, name, last_updated ` func deleteQuery() string { return `DELETE FROM coordinate WHERE id = :id` } // Read is the handler for GET requests made to the /coordinates API (in APIv5 // and later). func Read(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() cols := map[string]dbhelpers.WhereColumnInfo{ "id": {Column: "c.id", Checker: api.IsInt}, "name": {Column: "c.name", Checker: nil}, } api.DefaultSort(inf, "name") where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, cols) if len(errs) > 0 { errCode = http.StatusBadRequest userErr = util.JoinErrs(errs) api.HandleErr(w, r, tx, errCode, userErr, nil) return } var maxTime time.Time if inf.UseIMS() { var runSecond bool runSecond, maxTime = ims.TryIfModifiedSinceQuery(inf.Tx, r.Header, queryValues, selectMaxLastUpdatedQuery(where, orderBy, pagination)) if !runSecond { log.Debugln("IMS HIT") api.WriteNotModifiedResponse(maxTime, w, r) return } log.Debugln("IMS MISS") } else { log.Debugln("Non IMS request") } query := readQuery + where + orderBy + pagination rows, err := inf.Tx.NamedQuery(query, queryValues) if err != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("querying coordinates: %w", err)) return } defer log.Close(rows, "closing coordinate query rows") cs := []tc.CoordinateV5{} for rows.Next() { var c tc.CoordinateV5 err := rows.Scan(&c.ID, &c.Latitude, &c.Longitude, &c.LastUpdated, &c.Name) if err != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning a coordinate: %w", err)) return } cs = append(cs, c) } api.WriteResp(w, r, cs) } // isValid returns an error describing why c isn't a valid Coordinate, or nil if // it's actually valid. func isValid(c tc.CoordinateV5) error { validName := validation.NewStringRule(IsValidCoordinateName, "invalid characters found - Use alphanumeric . or - or _ .") latitudeErr := "Must be a floating point number within the range +-90" longitudeErr := "Must be a floating point number within the range +-180" errs := validation.Errors{ "name": validation.Validate(c.Name, validation.Required, validName), "latitude": validation.Validate(c.Latitude, validation.Min(-90.0).Error(latitudeErr), validation.Max(90.0).Error(latitudeErr)), "longitude": validation.Validate(c.Longitude, validation.Min(-180.0).Error(longitudeErr), validation.Max(180.0).Error(longitudeErr)), } return util.JoinErrs(tovalidate.ToErrors(errs)) } // Create is the handler for POST requests made to the /coordinates API (in // APIv5 and later). func Create(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() var c tc.CoordinateV5 err := json.NewDecoder(r.Body).Decode(&c) if err != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil) return } if err = isValid(c); err != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil) return } if err = tx.QueryRow(createQuery, c.Latitude, c.Longitude, c.Name).Scan(&c.ID, &c.LastUpdated); err != nil { userErr, sysErr, errCode = api.ParseDBError(err) api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } w.Header().Set(rfc.Location, fmt.Sprintf("/api/%s/coordinates?id=%d", inf.Version, *c.ID)) w.WriteHeader(http.StatusCreated) api.WriteRespAlertObj(w, r, tc.SuccessLevel, fmt.Sprintf("Coordinate '%s' (#%d) created", c.Name, *c.ID), c) changeLogMsg := fmt.Sprintf("USER: %s, COORDINATE: %s (#%d), ACTION: %s", inf.User.UserName, c.Name, *c.ID, api.Created) api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx) } // Update is the handler for PUT requests made to the /coordinates API (in API // v5 and later). func Update(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"}) tx := inf.Tx.Tx if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } defer inf.Close() var c tc.CoordinateV5 err := json.NewDecoder(r.Body).Decode(&c) if err != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil) return } id := inf.IntParams["id"] if c.ID != nil { if *c.ID != id { api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("ID mismatch; URI specifies %d but payload is for Coordinate #%d", id, *c.ID), nil) return } } else { c.ID = util.Ptr(id) } userErr, sysErr, statusCode := api.CheckIfUnModified(r.Header, inf.Tx, id, "coordinate") if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, statusCode, userErr, sysErr) return } if err = isValid(c); err != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil) return } if err = tx.QueryRow(putQuery, c.Latitude, c.Longitude, c.Name, id).Scan(&c.LastUpdated); err != nil { if errors.Is(err, sql.ErrNoRows) { userErr = fmt.Errorf("no such Coordinate: #%d", id) errCode = http.StatusNotFound sysErr = nil } else { userErr, sysErr, errCode = api.ParseDBError(err) } api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } api.WriteRespAlertObj(w, r, tc.SuccessLevel, fmt.Sprintf("Coordinate '%s' (#%d) updated", c.Name, id), c) changeLogMsg := fmt.Sprintf("USER: %s, COORDINATE: %s (#%d), ACTION: %s", inf.User.UserName, c.Name, id, api.Updated) api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx) } // Delete is the handler for PUT requests made to the /coordinates API (in API // v5 and later). func Delete(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"}) tx := inf.Tx.Tx if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } defer inf.Close() id := inf.IntParams["id"] c := tc.CoordinateV5{ ID: util.Ptr(id), } if err := tx.QueryRow(delQuery, id).Scan(&c.Latitude, &c.Longitude, &c.Name, &c.LastUpdated); err != nil { if errors.Is(err, sql.ErrNoRows) { userErr = fmt.Errorf("no such Coordinate: #%d", id) errCode = http.StatusNotFound sysErr = nil } else { userErr, sysErr, errCode = api.ParseDBError(err) } api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } api.WriteRespAlertObj(w, r, tc.SuccessLevel, fmt.Sprintf("Coordinate '%s' (#%d) deleted", c.Name, id), c) changeLogMsg := fmt.Sprintf("USER: %s, COORDINATE: %s (#%d), ACTION: %s", inf.User.UserName, c.Name, id, api.Deleted) api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx) }