blob: 46023832c14427ec108613c3339d5c244b424de3 [file] [log] [blame]
package steeringtargets
/*
* 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 (
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/apache/trafficcontrol/lib/go-log"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
"github.com/apache/trafficcontrol/lib/go-tc"
"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/auth"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
"github.com/jmoiron/sqlx"
)
type TOSteeringTargetV11 struct {
api.APIInfoImpl `json:"-"`
tc.SteeringTargetNullable
DSTenantID *int `json:"-" db:"tenant"`
LastUpdated *tc.TimeNoMod `json:"-" db:"last_updated"`
}
func (st TOSteeringTargetV11) GetKeyFieldsInfo() []api.KeyFieldInfo {
return []api.KeyFieldInfo{
{Field: "deliveryservice", Func: api.GetIntKey},
{Field: "target", Func: api.GetIntKey},
}
}
func (st TOSteeringTargetV11) GetKeys() (map[string]interface{}, bool) {
keys := map[string]interface{}{}
valid := true
if st.DeliveryServiceID == nil {
keys["deliveryservice"] = 0
valid = false
} else {
keys["deliveryservice"] = int(*st.DeliveryServiceID)
}
if st.TargetID == nil {
keys["target"] = 0
valid = false
} else {
keys["target"] = int(*st.TargetID)
}
return keys, valid
}
func (st *TOSteeringTargetV11) SetKeys(keys map[string]interface{}) {
dsI, _ := keys["deliveryservice"].(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.
ds := uint64(dsI)
st.DeliveryServiceID = &ds
targetI, _ := keys["target"].(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.
target := uint64(targetI)
st.TargetID = &target
}
func (st TOSteeringTargetV11) GetAuditName() string {
if st.DeliveryService != nil && st.Target != nil {
return string(*st.DeliveryService) + `-` + string(*st.Target)
}
if st.DeliveryServiceID != nil && st.TargetID != nil {
return strconv.FormatUint(*st.DeliveryServiceID, 10) + `-` + strconv.FormatUint(*st.TargetID, 10)
}
return "unknown"
}
func (st TOSteeringTargetV11) GetType() string {
return "steeringtarget"
}
func (st TOSteeringTargetV11) Validate() (error, error) {
return st.SteeringTargetNullable.Validate(st.ReqInfo.Tx.Tx)
}
func (st *TOSteeringTargetV11) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
steeringTargets, userErr, sysErr, errCode, maxTime := read(h, st.ReqInfo.Tx, st.ReqInfo.Params, st.ReqInfo.User, useIMS)
if userErr != nil || sysErr != nil {
return nil, userErr, sysErr, errCode, nil
}
iSteeringTargets := make([]interface{}, len(steeringTargets), len(steeringTargets))
for i, steeringTarget := range steeringTargets {
iSteeringTargets[i] = steeringTarget
}
return iSteeringTargets, nil, nil, errCode, maxTime
}
func read(h http.Header, tx *sqlx.Tx, parameters map[string]string, user *auth.CurrentUser, useIMS bool) ([]tc.SteeringTargetNullable, error, error, int, *time.Time) {
var maxTime time.Time
var runSecond bool
queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
"deliveryservice": dbhelpers.WhereColumnInfo{Column: "st.deliveryservice", Checker: api.IsInt},
"target": dbhelpers.WhereColumnInfo{Column: "st.target", Checker: api.IsInt},
}
where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(parameters, queryParamsToQueryCols)
if len(errs) > 0 {
return nil, nil, util.JoinErrs(errs), http.StatusBadRequest, nil
}
if useIMS {
runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, h, queryValues, selectMaxLastUpdatedQuery(where))
if !runSecond {
log.Debugln("IMS HIT")
return []tc.SteeringTargetNullable{}, nil, nil, http.StatusNotModified, &maxTime
}
log.Debugln("IMS MISS")
} else {
log.Debugln("Non IMS request")
}
query := selectQuery() + where + orderBy + pagination
userTenants, err := tenant.GetUserTenantListTx(*user, tx.Tx)
if err != nil {
return nil, nil, errors.New("getting user tenant list: " + err.Error()), http.StatusInternalServerError, nil
}
rows, err := tx.NamedQuery(query, queryValues)
if err != nil {
return nil, nil, errors.New("steering targets querying: " + err.Error()), http.StatusInternalServerError, nil
}
defer rows.Close()
steeringTargets := []TOSteeringTargetV11{}
for rows.Next() {
s := TOSteeringTargetV11{}
if err = rows.StructScan(&s); err != nil {
return nil, nil, errors.New("steering targets parsing: " + err.Error()), http.StatusInternalServerError, nil
}
steeringTargets = append(steeringTargets, s)
}
tenantMap := map[int]struct{}{}
for _, ten := range userTenants {
if ten.ID == nil {
return nil, nil, errors.New("user tenant with nil ID"), http.StatusInternalServerError, nil
}
tenantMap[*ten.ID] = struct{}{}
}
filteredTargets := []tc.SteeringTargetNullable{}
for _, tr := range steeringTargets {
if tr.DSTenantID == nil {
filteredTargets = append(filteredTargets, tr.SteeringTargetNullable)
continue
}
if _, ok := tenantMap[int(*tr.DSTenantID)]; ok {
filteredTargets = append(filteredTargets, tr.SteeringTargetNullable)
continue
}
}
return filteredTargets, nil, nil, http.StatusOK, &maxTime
}
func selectMaxLastUpdatedQuery(where string) string {
return `SELECT max(t) from (
SELECT max(st.last_updated) as t FROM steering_target AS st
JOIN deliveryservice AS ds ON st.deliveryservice = ds.id
JOIN deliveryservice AS dst ON st.target = dst.id
JOIN type AS tp ON tp.id = st.type ` + where +
` UNION ALL
select max(last_updated) as t from last_deleted l where l.table_name='steering_target') as res`
}
func (st *TOSteeringTargetV11) Create() (error, error, int) {
dsIDInt, err := strconv.Atoi(st.ReqInfo.Params["deliveryservice"])
if err != nil {
return errors.New("delivery service ID must be an integer"), nil, http.StatusBadRequest
}
dsID := uint64(dsIDInt)
st.DeliveryServiceID = &dsID
// target can't be in the Validate func, because it's in the parameters of PUT, not the body (but it is in the body in the POST here).
if st.TargetID == nil {
return errors.New("missing target"), nil, http.StatusBadRequest
}
if userErr, sysErr, errCode := tenant.CheckID(st.ReqInfo.Tx.Tx, st.ReqInfo.User, int(*st.DeliveryServiceID)); userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
_, cdn, _, err := dbhelpers.GetDSNameAndCDNFromID(st.ReqInfo.Tx.Tx, int(dsID))
if err != nil {
return nil, errors.New("createSteeringTarget: getting CDN from DS ID " + err.Error()), http.StatusInternalServerError
}
if userErr, sysErr, errCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(st.ReqInfo.Tx.Tx, string(cdn), st.ReqInfo.User.UserName); userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
rows, err := st.ReqInfo.Tx.NamedQuery(insertQuery(), st)
if err != nil {
return api.ParseDBError(err)
}
defer rows.Close()
rowsAffected := 0
for rows.Next() {
rowsAffected++
if err = rows.StructScan(&st); err != nil {
return nil, errors.New("steering target create scanning: " + err.Error()), http.StatusInternalServerError
}
}
if rowsAffected == 0 {
return nil, errors.New("no " + st.GetType() + " was inserted, no id was returned"), http.StatusInternalServerError
} else if rowsAffected > 1 {
return nil, errors.New("too many ids returned from steering target insert"), http.StatusInternalServerError
}
return nil, nil, http.StatusOK
}
func (st *TOSteeringTargetV11) Update(h http.Header) (error, error, int) {
dsIDInt, err := strconv.Atoi(st.ReqInfo.Params["deliveryservice"])
if err != nil {
return errors.New("delivery service ID must be an integer"), nil, http.StatusBadRequest
}
_, cdn, _, err := dbhelpers.GetDSNameAndCDNFromID(st.ReqInfo.Tx.Tx, dsIDInt)
if err != nil {
return nil, errors.New("updateSteeringTarget: getting CDN from DS ID " + err.Error()), http.StatusInternalServerError
}
if userErr, sysErr, errCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(st.ReqInfo.Tx.Tx, string(cdn), st.ReqInfo.User.UserName); userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
dsID := uint64(dsIDInt)
// TODO determine if the CRUDer automatically does this
st.DeliveryServiceID = &dsID
targetIDInt, err := strconv.Atoi(st.ReqInfo.Params["target"])
if err != nil {
return errors.New("target ID must be an integer"), nil, http.StatusBadRequest
}
targetID := uint64(targetIDInt)
st.TargetID = &targetID
if userErr, sysErr, errCode := tenant.CheckID(st.ReqInfo.Tx.Tx, st.ReqInfo.User, int(*st.DeliveryServiceID)); userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
err, found, existingLastUpdated := CheckIfExistsBeforeUpdate(st.ReqInfo.Tx, st)
if err == nil && found == false {
return errors.New("no steering target found with this id"), nil, http.StatusNotFound
}
if err != nil {
return nil, err, http.StatusInternalServerError
}
if !api.IsUnmodified(h, *existingLastUpdated) {
return errors.New("resource was modified"), nil, http.StatusPreconditionFailed
}
rows, err := st.ReqInfo.Tx.NamedQuery(updateQuery(), st)
if err != nil {
return api.ParseDBError(err)
}
defer rows.Close()
lastUpdated := tc.TimeNoMod{}
rowsAffected := 0
for rows.Next() {
rowsAffected++
if err = rows.StructScan(&st); err != nil {
return nil, errors.New("steering target update scanning: " + err.Error()), http.StatusInternalServerError
}
}
st.LastUpdated = &lastUpdated
if rowsAffected != 1 {
if rowsAffected < 1 {
return errors.New("steering target not found"), nil, http.StatusNotFound
}
return nil, errors.New("too many ids returned from steering target update"), http.StatusInternalServerError
}
return nil, nil, http.StatusOK
}
func CheckIfExistsBeforeUpdate(tx *sqlx.Tx, st *TOSteeringTargetV11) (error, bool, *time.Time) {
found := false
lastUpdated := time.Time{}
rows, err := tx.NamedQuery(`select last_updated from steering_target where deliveryservice=:deliveryservice and target=:target`, st)
if err != nil {
return errors.New("querying last_updated: " + err.Error()), found, nil
}
defer rows.Close()
if !rows.Next() {
return nil, found, nil
}
found = true
if err := rows.Scan(&lastUpdated); err != nil {
return errors.New("scanning last_updated: " + err.Error()), found, nil
}
return nil, found, &lastUpdated
}
func (st *TOSteeringTargetV11) Delete() (error, error, int) {
if userErr, sysErr, errCode := tenant.CheckID(st.ReqInfo.Tx.Tx, st.ReqInfo.User, int(*st.DeliveryServiceID)); userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
_, cdn, _, err := dbhelpers.GetDSNameAndCDNFromID(st.ReqInfo.Tx.Tx, int(*st.DeliveryServiceID))
if err != nil {
return nil, errors.New("deleteSteeringTarget: getting CDN from DS ID " + err.Error()), http.StatusInternalServerError
}
if userErr, sysErr, errCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(st.ReqInfo.Tx.Tx, string(cdn), st.ReqInfo.User.UserName); userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
result, err := st.ReqInfo.Tx.NamedExec(deleteQuery(), st)
if err != nil {
return nil, errors.New("steering target delete exec: " + err.Error()), http.StatusInternalServerError
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, errors.New("steering target delete exec getting rows affected: " + err.Error()), http.StatusInternalServerError
}
if rowsAffected < 1 {
return errors.New("steering target not found"), nil, http.StatusNotFound
} else if rowsAffected != 1 {
return nil, fmt.Errorf("this create affected too many rows: %d", rowsAffected), http.StatusInternalServerError
}
return nil, nil, http.StatusOK
}
func selectQuery() string {
return `
SELECT
st.deliveryservice,
ds.xml_id as deliveryservice_name,
CAST(ds.tenant_id AS INTEGER) as tenant,
st.target,
dst.xml_id AS target_name,
st.type as type_id,
tp.name as type_name,
st.value
FROM steering_target AS st
JOIN deliveryservice AS ds ON st.deliveryservice = ds.id
JOIN deliveryservice AS dst ON st.target = dst.id
JOIN type AS tp ON tp.id = st.type
`
}
func insertQuery() string {
return `
WITH st AS (
INSERT INTO steering_target (deliveryservice, target, value, type)
VALUES (:deliveryservice, :target, :value, :type_id)
RETURNING deliveryservice, target, value, type
)
SELECT
st.deliveryservice,
ds.xml_id as deliveryservice_name,
st.target,
dst.xml_id AS target_name,
st.type as type_id,
tp.name as type_name,
st.value
FROM st
JOIN deliveryservice AS ds ON st.deliveryservice = ds.id
JOIN deliveryservice AS dst ON st.target = dst.id
JOIN type AS tp ON tp.id = st.type
`
}
func updateQuery() string {
return `
WITH st as (
UPDATE steering_target SET
value = :value,
type = :type_id
WHERE deliveryservice = :deliveryservice AND target = :target
RETURNING deliveryservice, target, value, type, last_updated
)
SELECT
st.deliveryservice,
ds.xml_id as deliveryservice_name,
st.target,
dst.xml_id AS target_name,
st.type as type_id,
tp.name as type_name,
st.value,
st.last_updated
FROM st
JOIN deliveryservice AS ds ON st.deliveryservice = ds.id
JOIN deliveryservice AS dst ON st.target = dst.id
JOIN type AS tp ON tp.id = st.type
`
}
func deleteQuery() string {
return `DELETE FROM steering_target WHERE deliveryservice = :deliveryservice AND target = :target`
}