blob: 46355e8fc343d3a06b7a94e7908e507c73766885 [file] [log] [blame]
package cachegroup
/*
* 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"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
"github.com/apache/trafficcontrol/lib/go-log"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
"github.com/apache/trafficcontrol/lib/go-util"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
type TOCacheGroup struct {
api.APIInfoImpl `json:"-"`
tc.CacheGroupNullable
}
func (cg TOCacheGroup) GetKeyFieldsInfo() []api.KeyFieldInfo {
return []api.KeyFieldInfo{{Field: "id", Func: api.GetIntKey}}
}
//Implementation of the Identifier, Validator interface functions
func (cg TOCacheGroup) GetKeys() (map[string]interface{}, bool) {
if cg.ID == nil {
return map[string]interface{}{"id": 0}, false
}
return map[string]interface{}{"id": *cg.ID}, true
}
func (cg *TOCacheGroup) SetKeys(keys map[string]interface{}) {
i, _ := keys["id"].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here.
cg.ID = &i
}
//Implementation of the Identifier, Validator interface functions
func (cg TOCacheGroup) GetID() (int, bool) {
if cg.ID == nil {
return 0, false
}
return *cg.ID, true
}
func (cg TOCacheGroup) GetAuditName() string {
if cg.Name != nil {
return *cg.Name
}
id, _ := cg.GetID()
return strconv.Itoa(id)
}
func (cg TOCacheGroup) GetType() string {
return "cachegroup"
}
func (cg *TOCacheGroup) SetID(i int) {
cg.ID = &i
}
// Is the cachegroup being used?
func isUsed(tx *sqlx.Tx, ID int) (bool, error) {
var usedByTopology bool
var usedByServer bool
var usedByParent bool
var usedBySecondaryParent bool
var usedByASN bool
query := `SELECT
(SELECT id FROM topology_cachegroup WHERE topology_cachegroup.cachegroup = (SELECT name FROM cachegroup WHERE id = $1) LIMIT 1) IS NOT NULL,
(SELECT id FROM server WHERE server.cachegroup = $1 LIMIT 1) IS NOT NULL,
(SELECT id FROM cachegroup WHERE cachegroup.parent_cachegroup_id = $1 LIMIT 1) IS NOT NULL,
(SELECT id FROM cachegroup WHERE cachegroup.secondary_parent_cachegroup_id = $1 LIMIT 1) IS NOT NULL,
(SELECT id FROM asn WHERE cachegroup = $1 LIMIT 1) IS NOT NULL;`
err := tx.QueryRow(query, ID).Scan(&usedByTopology, &usedByServer, &usedByParent, &usedBySecondaryParent, &usedByASN)
if err != nil {
log.Errorf("received error: %++v from query execution", err)
return false, err
}
//Only return the immediate error
if usedByTopology {
return true, errors.New("cachegroup is in use by one or more topologies")
}
if usedByServer {
return true, errors.New("cachegroup is in use by one or more servers")
}
if usedByParent {
return true, errors.New("cachegroup is in use as a parent cachegroup")
}
if usedBySecondaryParent {
return true, errors.New("cachegroup is in use as a secondary parent cachegroup")
}
if usedByASN {
return true, errors.New("cachegroup is in use in one or more ASNs")
}
return false, nil
}
func isValidCacheGroupChar(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
}
// IsValidCacheGroupName returns true if the name contains only characters valid for a CacheGroup name
func IsValidCacheGroupName(str string) bool {
i := strings.IndexFunc(str, func(r rune) bool { return !isValidCacheGroupChar(r) })
return i == -1
}
func IsValidParentCachegroupID(id *int) bool {
if id == nil || *id > 0 {
return true
}
return false
}
// ValidateTypeInTopology validates cachegroup updates to ensure the type of the cachegroup does not change
// if it is assigned to a topology.
func (cg *TOCacheGroup) ValidateTypeInTopology() error {
userErr := fmt.Errorf("unable to check whether cachegroup %s is used in any topologies", *cg.Name)
// language=SQL
const previousTypeQuery = `
SELECT t.id
FROM cachegroup c
JOIN "type" t ON c."type" = t.id
WHERE c.id = $1
`
var previousTypeID int
// We only run this validation on PUT, not POST
if cg.ID == nil {
return nil
}
err := cg.ReqInfo.Tx.QueryRow(previousTypeQuery, *cg.ID).Scan(&previousTypeID)
// Cachegroup does not exist in the database yet
if err == sql.ErrNoRows {
return nil
}
if err != nil {
log.Errorf("%s: getting the previous type of cachegroup %s: %s", userErr.Error(), *cg.Name, err.Error())
return userErr
}
if cg.TypeID == nil || *cg.TypeID == previousTypeID {
return nil
}
// language=SQL
const previousNameQuery = `
SELECT name
FROM cachegroup c
WHERE c.id = $1
`
var previousName string
err = cg.ReqInfo.Tx.QueryRow(previousNameQuery, *cg.ID).Scan(&previousName)
if err == sql.ErrNoRows {
return nil
}
if err != nil {
log.Errorf("%s: getting the previous name of cachegroup %s: %s", userErr.Error(), *cg.Name, err.Error())
return userErr
}
// language=SQL
const usedInTopologyQuery = `
SELECT EXISTS (SELECT
FROM topology_cachegroup tc
WHERE tc.cachegroup = $1)
`
var usedInTopology bool
err = cg.ReqInfo.Tx.QueryRow(usedInTopologyQuery, previousName).Scan(&usedInTopology)
if err != nil {
log.Errorf("%s: querying topology_cachegroup by cachegroup name: %s", userErr.Error(), err.Error())
return userErr
}
if !usedInTopology {
return nil
}
// language=SQL
const readableTypesQuery = `
SELECT t.id, t."name"
FROM "type" t
WHERE id = $1 OR id = $2
`
rows, err := cg.ReqInfo.Tx.Query(readableTypesQuery, previousTypeID, *cg.TypeID)
if err != nil {
log.Errorf("%s: querying type names: %s", userErr.Error(), err.Error())
return userErr
}
typeNameByID := map[int]string{}
for rows.Next() {
var typeID int
var typeName string
err = rows.Scan(&typeID, &typeName)
if err != nil {
log.Errorf("%s: scanning type names: %s", userErr.Error(), err.Error())
return userErr
}
typeNameByID[typeID] = typeName
}
log.Close(rows, "error closing rows for type names")
return fmt.Errorf("cannot change type of cachegroup %s from %s to %s because it is in use by a topology", *cg.Name, typeNameByID[previousTypeID], typeNameByID[*cg.TypeID])
}
// Validate fulfills the api.Validator interface.
//
// TODO: A lot of database operations here either swallow their errors or return
// them to the client.
func (cg TOCacheGroup) Validate() (error, error) {
if _, err := tc.ValidateTypeID(cg.ReqInfo.Tx.Tx, cg.TypeID, "cachegroup"); err != nil {
return err, nil
}
if cg.Fallbacks != nil && len(*cg.Fallbacks) > 0 {
isValid, err := cg.isAllowedToFallback(*cg.TypeID)
if err != nil {
return err, nil
}
if !isValid {
return errors.New("the cache group " + *cg.Name + " is not allowed to have fallbacks. It must be of type EDGE_LOC."), nil
}
for _, fallback := range *cg.Fallbacks {
isValid, err = cg.isValidCacheGroupFallback(fallback)
if err != nil {
return err, nil
}
if !isValid {
return errors.New("the cache group " + fallback + " is not valid as a fallback. It must exist as a cache group and be of type EDGE_LOC."), nil
}
}
}
validName := validation.NewStringRule(IsValidCacheGroupName, "invalid characters found - Use alphanumeric . or - or _ .")
validShortName := validation.NewStringRule(IsValidCacheGroupName, "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(cg.Name, validation.Required, validName),
"shortName": validation.Validate(cg.ShortName, validation.Required, validShortName),
"latitude": validation.Validate(cg.Latitude, validation.Min(-90.0).Error(latitudeErr), validation.Max(90.0).Error(latitudeErr)),
"longitude": validation.Validate(cg.Longitude, validation.Min(-180.0).Error(longitudeErr), validation.Max(180.0).Error(longitudeErr)),
"parentCacheGroupID": validation.Validate(cg.ParentCachegroupID, validation.Min(1)),
"secondaryParentCachegroupID": validation.Validate(cg.SecondaryParentCachegroupID, validation.Min(1)),
"localizationMethods": validation.Validate(cg.LocalizationMethods, validation.By(tovalidate.IsPtrToSliceOfUniqueStringersICase("CZ", "DEEP_CZ", "GEO"))),
"type": cg.ValidateTypeInTopology(),
}
return util.JoinErrs(tovalidate.ToErrors(errs)), nil
}
//The TOCacheGroup implementation of the Creator interface
//The insert sql returns the id and lastUpdated values of the newly inserted cachegroup and have
//to be added to the struct
func (cg *TOCacheGroup) Create() (error, error, int) {
if cg.Latitude == nil {
cg.Latitude = util.FloatPtr(0.0)
}
if cg.Longitude == nil {
cg.Longitude = util.FloatPtr(0.0)
}
if cg.LocalizationMethods == nil {
cg.LocalizationMethods = &[]tc.LocalizationMethod{}
}
if cg.Fallbacks == nil {
cg.Fallbacks = &[]string{}
}
if cg.FallbackToClosest == nil {
fbc := true
cg.FallbackToClosest = &fbc
}
err := cg.ReqInfo.Tx.Tx.QueryRow(
InsertQuery(),
cg.Name,
cg.ShortName,
cg.TypeID,
cg.ParentCachegroupID,
cg.SecondaryParentCachegroupID,
cg.FallbackToClosest,
).Scan(
&cg.ID,
&cg.Type,
&cg.ParentName,
&cg.SecondaryParentName,
)
if err != nil {
return api.ParseDBError(err)
}
coordinateID, err := cg.createCoordinate()
if err != nil {
return nil, errors.New("cachegroup create: creating coord:" + err.Error()), http.StatusInternalServerError
}
err = cg.ReqInfo.Tx.Tx.QueryRow(
`UPDATE cachegroup SET coordinate=$1 WHERE id=$2 RETURNING last_updated`,
coordinateID,
cg.ID,
).Scan(&cg.LastUpdated)
if err != nil {
return nil, fmt.Errorf("followup update during cachegroup create: %v", err), http.StatusInternalServerError
}
if err = cg.createLocalizationMethods(); err != nil {
return nil, errors.New("creating cachegroup: creating localization methods: " + err.Error()), http.StatusInternalServerError
}
if err = cg.createCacheGroupFallbacks(); err != nil {
return nil, errors.New("creating cachegroup: creating cache group fallbacks: " + err.Error()), http.StatusInternalServerError
}
return nil, nil, http.StatusOK
}
func (cg *TOCacheGroup) createLocalizationMethods() error {
q := `DELETE FROM cachegroup_localization_method WHERE cachegroup = $1`
if _, err := cg.ReqInfo.Tx.Tx.Exec(q, *cg.ID); err != nil {
return fmt.Errorf("unable to delete cachegroup_localization_methods for cachegroup %d: %s", *cg.ID, err.Error())
}
if cg.LocalizationMethods != nil {
q = `INSERT INTO cachegroup_localization_method (cachegroup, method) VALUES ($1, $2)`
for _, method := range *cg.LocalizationMethods {
if _, err := cg.ReqInfo.Tx.Tx.Exec(q, *cg.ID, method.String()); err != nil {
return fmt.Errorf("unable to insert cachegroup_localization_methods for cachegroup %d: %s", *cg.ID, err.Error())
}
}
}
return nil
}
func (cg *TOCacheGroup) createCacheGroupFallbacks() error {
deleteCgfQuery := `DELETE FROM cachegroup_fallbacks WHERE primary_cg = $1`
if _, err := cg.ReqInfo.Tx.Tx.Exec(deleteCgfQuery, *cg.ID); err != nil {
return fmt.Errorf("unable to delete cachegroup_fallbacks for cachegroup %d: %s", *cg.ID, err.Error())
}
if cg.Fallbacks == nil {
return nil
}
insertCgfQuery := `INSERT INTO cachegroup_fallbacks (primary_cg, backup_cg, set_order) VALUES ($1, (SELECT cachegroup.id FROM cachegroup WHERE cachegroup.name = $2), $3)`
for orderIndex, fallback := range *cg.Fallbacks {
if _, err := cg.ReqInfo.Tx.Tx.Exec(insertCgfQuery, *cg.ID, fallback, orderIndex); err != nil {
return fmt.Errorf("unable to insert cachegroup_fallbacks for cachegroup %d: %s", *cg.ID, err.Error())
}
}
return nil
}
func (cg *TOCacheGroup) isValidCacheGroupFallback(fallbackName string) (bool, error) {
var isValid bool
query := `SELECT(
SELECT cachegroup.id
FROM cachegroup
JOIN type on type.id = cachegroup.type
WHERE cachegroup.name = $1
AND (type.name = 'EDGE_LOC')
) IS NOT NULL;`
err := cg.ReqInfo.Tx.Tx.QueryRow(query, fallbackName).Scan(&isValid)
if err != nil {
log.Errorf("received error: %++v from cachegroup fallback validation query execution", err)
return false, err
}
return isValid, nil
}
func (cg *TOCacheGroup) isAllowedToFallback(cacheGroupType int) (bool, error) {
var isValid bool
query := `SELECT(
SELECT type.name
FROM type
WHERE type.id = $1
AND (type.name = 'EDGE_LOC')
) IS NOT NULL;`
err := cg.ReqInfo.Tx.Tx.QueryRow(query, cacheGroupType).Scan(&isValid)
if err != nil {
log.Errorf("received error: %++v from cachegroup fallback validation query execution", err)
return false, err
}
return isValid, nil
}
func (cg *TOCacheGroup) createCoordinate() (*int, error) {
var coordinateID *int
if cg.Latitude != nil && cg.Longitude != nil {
q := `INSERT INTO coordinate (name, latitude, longitude) VALUES ($1, $2, $3) RETURNING id`
if err := cg.ReqInfo.Tx.Tx.QueryRow(q, tc.CachegroupCoordinateNamePrefix+*cg.Name, *cg.Latitude, *cg.Longitude).Scan(&coordinateID); err != nil {
return nil, err
}
}
return coordinateID, nil
}
const numberOfDuplicatesQuery = `
SELECT COUNT(*)
FROM public.coordinate
WHERE id NOT IN (
SELECT coordinate
FROM public.cachegroup
WHERE id = $1
)
AND name = $2`
type userError string
func (e userError) Error() string {
return string(e)
}
const duplicateExist userError = "cachegroup name already exists, please choose a different name"
func (cg *TOCacheGroup) updateCoordinate() error {
if cg.Latitude != nil && cg.Longitude != nil {
var count uint
if err := cg.ReqInfo.Tx.Tx.QueryRow(numberOfDuplicatesQuery, *cg.ID, tc.CachegroupCoordinateNamePrefix+*cg.Name).Scan(&count); err != nil {
return fmt.Errorf("getting coordinate for Cache Group '%s': %w", *cg.Name, err)
}
if count > 0 {
return duplicateExist
}
q := `UPDATE coordinate SET name = $1, latitude = $2, longitude = $3 WHERE id = (SELECT coordinate FROM cachegroup WHERE id = $4)`
result, err := cg.ReqInfo.Tx.Tx.Exec(q, tc.CachegroupCoordinateNamePrefix+*cg.Name, *cg.Latitude, *cg.Longitude, *cg.ID)
if err != nil {
return fmt.Errorf("updating coordinate for cachegroup '%s': %w", *cg.Name, err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("updating coordinate for cachegroup '%s', getting rows affected: %w", *cg.Name, err)
}
if rowsAffected == 0 {
return fmt.Errorf("updating coordinate for cachegroup '%s', zero rows affected", *cg.Name)
}
}
return nil
}
func (cg *TOCacheGroup) deleteCoordinate(coordinateID int) error {
q := `UPDATE cachegroup SET coordinate = NULL WHERE id = $1`
result, err := cg.ReqInfo.Tx.Tx.Exec(q, *cg.ID)
if err != nil {
return fmt.Errorf("updating cachegroup %d coordinate to null: %s", *cg.ID, err.Error())
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("updating cachegroup %d coordinate to null, getting rows affected: %s", *cg.ID, err.Error())
}
if rowsAffected == 0 {
return fmt.Errorf("updating cachegroup %d coordinate to null, zero rows affected", *cg.ID)
}
q = `DELETE FROM coordinate WHERE id = $1`
result, err = cg.ReqInfo.Tx.Tx.Exec(q, coordinateID)
if err != nil {
return fmt.Errorf("delete coordinate %d for cachegroup %d: %s", coordinateID, *cg.ID, err.Error())
}
rowsAffected, err = result.RowsAffected()
if err != nil {
return fmt.Errorf("delete coordinate %d for cachegroup %d, getting rows affected: %s", coordinateID, *cg.ID, err.Error())
}
if rowsAffected == 0 {
return fmt.Errorf("delete coordinate %d for cachegroup %d, zero rows affected", coordinateID, *cg.ID)
}
return nil
}
func GetCacheGroupsByName(names []string, Tx *sqlx.Tx) (map[string]tc.CacheGroupNullable, error, error, int) {
query := SelectQuery() + multipleCacheGroupsWhere()
namesPqArray := pq.Array(names)
rows, err := Tx.Query(query, namesPqArray)
if err != nil {
userErr, sysErr, errCode := api.ParseDBError(err)
return nil, userErr, sysErr, errCode
}
defer log.Close(rows, "unable to close DB connection")
cacheGroupMap := map[string]tc.CacheGroupNullable{}
for rows.Next() {
var s tc.CacheGroupNullable
lms := make([]tc.LocalizationMethod, 0)
cgfs := make([]string, 0)
if err = rows.Scan(
&s.ID,
&s.Name,
&s.ShortName,
&s.Latitude,
&s.Longitude,
pq.Array(&lms),
&s.ParentCachegroupID,
&s.ParentName,
&s.SecondaryParentCachegroupID,
&s.SecondaryParentName,
&s.Type,
&s.TypeID,
&s.LastUpdated,
pq.Array(&cgfs),
&s.FallbackToClosest,
); err != nil {
return nil, nil, errors.New("cachegroup read: scanning: " + err.Error()), http.StatusInternalServerError
}
s.LocalizationMethods = &lms
s.Fallbacks = &cgfs
cacheGroupMap[*s.Name] = s
}
return cacheGroupMap, nil, nil, http.StatusOK
}
func (cg *TOCacheGroup) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
var maxTime time.Time
var runSecond bool
cacheGroups := []interface{}{}
// Query Parameters to Database Query column mappings
// see the fields mapped in the SQL query
queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
"id": {Column: "cachegroup.id", Checker: api.IsInt},
"name": {Column: "cachegroup.name"},
"shortName": {Column: "cachegroup.short_name"},
"type": {Column: "cachegroup.type"},
"topology": {Column: "topology_cachegroup.topology"},
}
where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(cg.ReqInfo.Params, queryParamsToQueryCols)
if len(errs) > 0 {
return nil, util.JoinErrs(errs), nil, http.StatusBadRequest, nil
}
if useIMS {
runSecond, maxTime = ims.TryIfModifiedSinceQuery(cg.APIInfo().Tx, h, queryValues, selectMaxLastUpdatedQuery(where))
if !runSecond {
log.Debugln("IMS HIT")
return cacheGroups, nil, nil, http.StatusNotModified, &maxTime
}
log.Debugln("IMS MISS")
} else {
log.Debugln("Non IMS request")
}
baseSelect := SelectQuery()
if _, ok := cg.ReqInfo.Params["topology"]; ok {
baseSelect += `
LEFT JOIN topology_cachegroup ON cachegroup.name = topology_cachegroup.cachegroup
`
}
// If the type cannot be converted to an int, return 400
if cgType, ok := cg.ReqInfo.Params["type"]; ok {
_, err := strconv.Atoi(cgType)
if err != nil {
return nil, errors.New("cachegroup read: converting cachegroup type to integer " + err.Error()), nil, http.StatusBadRequest, nil
}
}
query := baseSelect + where + orderBy + pagination
rows, err := cg.ReqInfo.Tx.NamedQuery(query, queryValues)
if err != nil {
return nil, nil, errors.New("cachegroup read: querying: " + err.Error()), http.StatusInternalServerError, nil
}
defer rows.Close()
for rows.Next() {
var s TOCacheGroup
lms := make([]tc.LocalizationMethod, 0)
cgfs := make([]string, 0)
if err = rows.Scan(
&s.ID,
&s.Name,
&s.ShortName,
&s.Latitude,
&s.Longitude,
pq.Array(&lms),
&s.ParentCachegroupID,
&s.ParentName,
&s.SecondaryParentCachegroupID,
&s.SecondaryParentName,
&s.Type,
&s.TypeID,
&s.LastUpdated,
pq.Array(&cgfs),
&s.FallbackToClosest,
); err != nil {
return nil, nil, errors.New("cachegroup read: scanning: " + err.Error()), http.StatusInternalServerError, nil
}
s.LocalizationMethods = &lms
s.Fallbacks = &cgfs
cacheGroups = append(cacheGroups, s)
}
return cacheGroups, nil, nil, http.StatusOK, &maxTime
}
func selectMaxLastUpdatedQuery(where string) string {
return `SELECT max(t) from (
SELECT max(cachegroup.last_updated) as t FROM cachegroup
LEFT JOIN coordinate ON coordinate.id = cachegroup.coordinate
INNER JOIN type ON cachegroup.type = type.id
LEFT JOIN cachegroup AS cgp ON cachegroup.parent_cachegroup_id = cgp.id
LEFT JOIN cachegroup AS cgs ON cachegroup.secondary_parent_cachegroup_id = cgs.id ` + where +
` UNION ALL
select max(last_updated) as t from last_deleted l where l.table_name='cachegroup') as res`
}
//The TOCacheGroup implementation of the Updater interface
func (cg *TOCacheGroup) Update(h http.Header) (error, error, int) {
if cg.Latitude == nil {
cg.Latitude = util.FloatPtr(0.0)
}
if cg.Longitude == nil {
cg.Longitude = util.FloatPtr(0.0)
}
if cg.LocalizationMethods == nil {
cg.LocalizationMethods = &[]tc.LocalizationMethod{}
}
if cg.Fallbacks == nil {
cg.Fallbacks = &[]string{}
}
if cg.FallbackToClosest == nil {
fbc := true
cg.FallbackToClosest = &fbc
}
userErr, sysErr, errCode := api.CheckIfUnModified(h, cg.ReqInfo.Tx, *cg.ID, "cachegroup")
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
// CheckIfCurrentUserCanModifyCachegroup
userErr, sysErr, errCode = dbhelpers.CheckIfCurrentUserCanModifyCachegroup(cg.ReqInfo.Tx.Tx, *cg.ID, cg.ReqInfo.User.UserName)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
coordinateID, userErr, sysErr, errCode := cg.handleCoordinateUpdate()
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
userErr = cg.ValidateTypeInTopology()
if userErr != nil {
return userErr, nil, http.StatusBadRequest
}
err := cg.ReqInfo.Tx.Tx.QueryRow(
UpdateQuery(),
cg.Name,
cg.ShortName,
coordinateID,
cg.ParentCachegroupID,
cg.SecondaryParentCachegroupID,
cg.TypeID,
cg.FallbackToClosest,
cg.ID,
).Scan(
&cg.Type,
&cg.ParentName,
&cg.SecondaryParentName,
&cg.LastUpdated,
)
if err != nil {
return api.ParseDBError(err)
}
if err = cg.createLocalizationMethods(); err != nil {
return nil, errors.New("cachegroup update: creating localization methods: " + err.Error()), http.StatusInternalServerError
}
if err = cg.createCacheGroupFallbacks(); err != nil {
return nil, errors.New("cachegroup update: creating cache group fallbacks: " + err.Error()), http.StatusInternalServerError
}
return nil, nil, http.StatusOK
}
func (cg *TOCacheGroup) handleCoordinateUpdate() (*int, error, error, int) {
coordinateID, err := cg.getCoordinateID()
// This is not a logic error. Because the coordinate id is recieved from the
// cachegroup table, not being able to find the coordinate is equivalent to
// not being able to find the cachegroup.
if err == sql.ErrNoRows {
return nil, fmt.Errorf("no cachegroup with id %d found", *cg.ID), nil, http.StatusNotFound
}
if err != nil {
return nil, nil, err, http.StatusInternalServerError
}
// If partial coordinate information is given or the coordinate information is wholly
// nullified, it is invalid and we zero the reference to the coordinate in the database.
// For now I am also nullifying both the longitude and latitude if one of them is nil
// because a non-nil returned value would have no meaning.
//
// TODO: Find references that talk about longitude and latidude as required fields
// - Making longitude and latitude required would prevent this odd case
// - Longitude and latitude probably aren't required for versioning reasons
// - We've recently had a discussion on versioning that may be related
// TODO: In the meantime should an error be returned for partial coordinate information?
// - Probably not
//
if cg.Latitude == nil || cg.Longitude == nil {
if err = cg.deleteCoordinate(*coordinateID); err != nil {
return nil, nil, err, http.StatusInternalServerError
}
cg.Latitude = nil
cg.Longitude = nil
return nil, nil, nil, http.StatusOK
}
err = cg.updateCoordinate()
if err != nil {
if errors.Is(err, duplicateExist) {
return nil, err, err, http.StatusBadRequest
}
return nil, err, err, http.StatusInternalServerError
}
return coordinateID, nil, nil, http.StatusOK
}
func (cg *TOCacheGroup) getCoordinateID() (*int, error) {
q := `SELECT coordinate FROM cachegroup WHERE id = $1`
var coordinateID *int
if err := cg.ReqInfo.Tx.Tx.QueryRow(q, *cg.ID).Scan(&coordinateID); err != nil {
return nil, err
}
return coordinateID, nil
}
//The CacheGroup implementation of the Deleter interface
//all implementations of Deleter should use transactions and return the proper errorType
func (cg *TOCacheGroup) Delete() (error, error, int) {
inUse, err := isUsed(cg.ReqInfo.Tx, *cg.ID)
if inUse {
return err, nil, http.StatusBadRequest
}
if err != nil {
return nil, errors.New("cachegroup delete: checking use: " + err.Error()), http.StatusInternalServerError
}
coordinateID, err := cg.getCoordinateID()
if err == sql.ErrNoRows {
return errors.New("no cachegroup with that id found"), nil, http.StatusNotFound
}
if err != nil {
return nil, errors.New("cachegroup delete: getting coord: " + err.Error()), http.StatusInternalServerError
}
// CheckIfCurrentUserCanModifyCachegroup
userErr, sysErr, errCode := dbhelpers.CheckIfCurrentUserCanModifyCachegroup(cg.ReqInfo.Tx.Tx, *cg.ID, cg.ReqInfo.User.UserName)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
if err = cg.deleteCoordinate(*coordinateID); err != nil {
return nil, errors.New("cachegroup delete: deleting coord: " + err.Error()), http.StatusInternalServerError
}
result, err := cg.ReqInfo.Tx.Exec(DeleteQuery(), *cg.ID)
if err != nil {
return nil, errors.New("cachegroup delete: " + err.Error()), http.StatusInternalServerError
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, fmt.Errorf("getting rows affected: %v", err), http.StatusInternalServerError
}
// In the zero case, either err != nil occurs (from the Exec) or we got sql.ErrNoRows above
if rowsAffected != 1 {
return nil, errors.New("cachegroup delete affected multiple rows"), http.StatusInternalServerError
}
return nil, nil, http.StatusOK
}
func InsertQuery() string {
return `INSERT INTO cachegroup (
name,
short_name,
type,
parent_cachegroup_id,
secondary_parent_cachegroup_id,
fallback_to_closest
) VALUES($1,$2,$3,$4,$5,$6)
RETURNING
id,
(SELECT name FROM type WHERE cachegroup.type = type.id),
(SELECT name FROM cachegroup parent
WHERE cachegroup.parent_cachegroup_id = parent.id),
(SELECT name FROM cachegroup secondary_parent
WHERE cachegroup.secondary_parent_cachegroup_id = secondary_parent.id)`
}
func SelectQuery() string {
// the 'type_name' and 'type_id' aliases on the 'type.name'
// and cachegroup.type' fields are needed
// to disambiguate the struct scan, see also the
// tc.CacheGroupNullable struct 'db' metadata
return `SELECT
cachegroup.id,
cachegroup.name,
cachegroup.short_name,
coordinate.latitude,
coordinate.longitude,
(SELECT COALESCE(array_agg(CAST(method as text)), '{}') AS localization_methods FROM cachegroup_localization_method clm WHERE clm.cachegroup = cachegroup.id),
cachegroup.parent_cachegroup_id,
cgp.name AS parent_cachegroup_name,
cachegroup.secondary_parent_cachegroup_id,
cgs.name AS secondary_parent_cachegroup_name,
type.name AS type_name,
cachegroup.type AS type_id,
cachegroup.last_updated,
(SELECT COALESCE(array_agg(CAST(cg2.name as text) ORDER BY cgf.set_order ASC), '{}') AS fallbacks FROM cachegroup cg2 INNER JOIN cachegroup_fallbacks cgf ON cgf.backup_cg = cg2.id WHERE cgf.primary_cg = cachegroup.id),
cachegroup.fallback_to_closest
FROM cachegroup
LEFT JOIN coordinate ON coordinate.id = cachegroup.coordinate
INNER JOIN type ON cachegroup.type = type.id
LEFT JOIN cachegroup AS cgp ON cachegroup.parent_cachegroup_id = cgp.id
LEFT JOIN cachegroup AS cgs ON cachegroup.secondary_parent_cachegroup_id = cgs.id
`
}
func multipleCacheGroupsWhere() string {
return `
WHERE
cachegroup.name = ANY ($1)
`
}
func UpdateQuery() string {
// to disambiguate struct scans, the named
// parameter 'type_id' is an alias to cachegroup.type
//see also the tc.CacheGroupNullable struct 'db' metadata
return `UPDATE
cachegroup SET
name=$1,
short_name=$2,
coordinate=$3,
parent_cachegroup_id=$4,
secondary_parent_cachegroup_id=$5,
type=$6,
fallback_to_closest=$7
WHERE id=$8
RETURNING
(SELECT name FROM type WHERE cachegroup.type = type.id),
(SELECT name FROM cachegroup parent
WHERE cachegroup.parent_cachegroup_id = parent.id),
(SELECT name FROM cachegroup secondary_parent
WHERE cachegroup.secondary_parent_cachegroup_id = secondary_parent.id),
last_updated`
}
func DeleteQuery() string {
return `DELETE FROM cachegroup WHERE id=$1`
}