Skip to content
This repository was archived by the owner on Nov 24, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions traffic_ops/client/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,37 @@ func (to *Session) GetServerByHostName(hostName string) ([]tc.Server, ReqInf, er
return data.Response, reqInf, nil
}

// GetServersBySearch GETs Servers by searching.
func (to *Session) GetServersBySearch(search, searchVal, searchOp *string) ([]tc.Server, ReqInf, error) {
v := url.Values{}
if search != nil {
v.Add("search", *search)
}
if searchVal != nil {
v.Add("searchValue", *searchVal)
}
if searchOp != nil {
v.Add("searchOperator", *searchOp)
}
url := API_SERVERS
if qStr := v.Encode(); len(qStr) > 0 {
url = fmt.Sprintf("%s?%s", url, qStr)
}
resp, remoteAddr, err := to.request(http.MethodGet, url, nil)
reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
if err != nil {
return nil, reqInf, err
}
defer resp.Body.Close()

var data tc.ServersResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, reqInf, err
}

return data.Response, reqInf, nil
}

// DeleteServerByID DELETEs a Server by ID.
func (to *Session) DeleteServerByID(id int) (tc.Alerts, ReqInf, error) {
route := fmt.Sprintf("%s/%d", API_SERVERS, id)
Expand Down
53 changes: 53 additions & 0 deletions traffic_ops/testing/api/v2/servers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ package v2
*/

import (
"fmt"
"strings"
"testing"

"github.com/apache/trafficcontrol/lib/go-util"

"github.com/apache/trafficcontrol/lib/go-tc"
)

func TestServers(t *testing.T) {
WithObjs(t, []TCObj{CDNs, Types, Tenants, Users, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, CacheGroups, DeliveryServices, Servers}, func() {
GetTestSearchServers(t)
UpdateTestServers(t)
GetTestServers(t)
})
Expand Down Expand Up @@ -51,6 +56,54 @@ func GetTestServers(t *testing.T) {
}
}

func GetTestSearchServers(t *testing.T) {
var testCases = []struct {
description string
searchValue *string
search *string
searchOp *string
checkFunc func(s tc.Server) error
}{
{
description: "Success: domainName search",
searchValue: util.StrPtr("kabletown"),
search: util.StrPtr("hostName"),
checkFunc: func(s tc.Server) error {
if !strings.Contains(s.DomainName, "kabletown") {
return fmt.Errorf("expected kabletown to be contained in %s domainName: %s", s.HostName, s.DomainName)
}
return nil
},
},
{
description: "Success: OR Operator search",
searchValue: util.StrPtr("1"), //cdn - cdn1 and cachegroup - cachegroup1 will both be caught
search: util.StrPtr("cdn,cachegroup"),
searchOp: util.StrPtr("OR"),
checkFunc: func(s tc.Server) error {
if !(strings.Contains(s.CDNName, "1") || strings.Contains(s.Cachegroup, "1")) {
return fmt.Errorf("expected 1 to be contained in %s cdn: %s or cachegroup: %s", s.HostName, s.CDNName, s.Cachegroup)
}
return nil
},
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
t.Log("Starting test scenario: ", tc.description)
resp, _, err := TOSession.GetServersBySearch(tc.search, tc.searchValue, tc.searchOp)
if err != nil {
t.Errorf("cannot GET Server by search: %v", err)
}
for _, s := range resp {
if e := tc.checkFunc(s); e != nil {
t.Error(e)
}
}
})
}
}

func UpdateTestServers(t *testing.T) {

firstServer := testData.Servers[0]
Expand Down
66 changes: 66 additions & 0 deletions traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ const BaseWhere = "\nWHERE"
const BaseOrderBy = "\nORDER BY"
const BaseLimit = "\nLIMIT"
const BaseOffset = "\nOFFSET"
const SearchColumnsParam = "search"
const SearchValueParam = "searchValue"
const SearchFilterParam = "searchOperator"

const getDSTenantIDFromXMLIDQuery = `
SELECT deliveryservice.tenant_id
Expand Down Expand Up @@ -221,6 +224,69 @@ func parseCriteriaAndQueryValues(queryParamsToSQLCols map[string]WhereColumnInfo
return criteria, queryValues, errs
}

// AddSearchableWhereClause takes in an existing where clause and adds the searchable parameters to it
func AddSearchableWhereClause(whereClause string, queryValues map[string]interface{}, parameters map[string]string, searchQueryParamsToSQLCols map[string]WhereColumnInfo) (string, map[string]interface{}, []error) {
searchColParams, scExists := parameters[SearchColumnsParam]
searchVal, svExists := parameters[SearchValueParam]

var criteria string
var errs []error

if !svExists && !scExists {
return whereClause, queryValues, errs
} else if !svExists || !scExists {
return whereClause, queryValues, append(errs, fmt.Errorf("both %s and %s query parameters must be supplied if conducting a search", SearchValueParam, SearchColumnsParam))
}

criteria, queryValues, errs = parseSearchCriteriaAndQueryValues(searchQueryParamsToSQLCols, queryValues, parameters, searchColParams, searchVal)

if len(errs) > 0 {
return whereClause, queryValues, errs
}
if len(queryValues) > 0 {
if len(whereClause) == 0 {
whereClause = BaseWhere + " " + criteria
} else {
whereClause += " AND (" + criteria + ") "
}
}
return whereClause, queryValues, errs
}

func parseSearchCriteriaAndQueryValues(searchQueryParamsToSQLCols map[string]WhereColumnInfo, queryValues map[string]interface{}, parameters map[string]string, searchColParams, searchVal string) (string, map[string]interface{}, []error) {
var criteria string
searchVal = "%" + searchVal + "%"
var criteriaArgs []string
errs := []error{}
if queryValues == nil {
queryValues = map[string]interface{}{}
}
for _, searchColParam := range strings.Split(searchColParams, ",") {
colInfo, ok := searchQueryParamsToSQLCols[searchColParam]
if !ok {
continue
}
searchColParam += "_search"
criteria = "UPPER(" + colInfo.Column + ") LIKE UPPER(:" + searchColParam + ")"
criteriaArgs = append(criteriaArgs, criteria)
queryValues[searchColParam] = searchVal
}

filter := "OR"

if searchOperator, exists := parameters[SearchFilterParam]; exists {
if strings.ToLower(searchOperator) != "and" && strings.ToLower(searchOperator) != "or" {
errs = append(errs, fmt.Errorf("searchOperator parameter %s can only be set to AND or OR", searchOperator))
} else {
filter = searchOperator
}
}

criteria = strings.Join(criteriaArgs, fmt.Sprintf(" %s ", filter))

return criteria, queryValues, errs
}

// AddTenancyCheck takes a WHERE clause (can be ""), the associated queryValues (can be empty),
// a tenantColumnName that should provide a bigint corresponding to the tenantID of the object being checked (this may require a CAST),
// and an array of the tenantIDs the user has access to; it returns a where clause and associated queryValues including filtering based on tenancy.
Expand Down
135 changes: 135 additions & 0 deletions traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"testing"
"unicode"

"github.com/apache/trafficcontrol/lib/go-util"

"github.com/jmoiron/sqlx"
"gopkg.in/DATA-DOG/go-sqlmock.v1"
)
Expand Down Expand Up @@ -81,6 +83,139 @@ FROM table t

}

func TestAddSearchableWhereClause(t *testing.T) {
var testCases = []struct {
description string
expectErrors bool
expectedWhereClause string
startingWhereClause string
parameters map[string]string
searchQueryParamsToSQLCols map[string]WhereColumnInfo
}{
{
description: "Failure: Missing search parameter",
expectErrors: true,
startingWhereClause: "",
expectedWhereClause: "",
parameters: map[string]string{
SearchValueParam: "test",
},
searchQueryParamsToSQLCols: map[string]WhereColumnInfo{},
},
{
description: "Failure: Missing search parameter value",
expectErrors: true,
startingWhereClause: "",
expectedWhereClause: "",
parameters: map[string]string{
SearchColumnsParam: "key1",
},
searchQueryParamsToSQLCols: map[string]WhereColumnInfo{},
},
{
description: "Success: Existing Where Clause",
expectErrors: false,
startingWhereClause: "\nWHERE foo=:bar",
expectedWhereClause: "\nWHERE foo=:bar AND (UPPER(t.col) LIKE UPPER(:key1_search) OR UPPER(t1.col) LIKE UPPER(:key2_search)) ",
parameters: map[string]string{
SearchValueParam: "test",
SearchColumnsParam: "key1,key2",
},
searchQueryParamsToSQLCols: map[string]WhereColumnInfo{
"key1": WhereColumnInfo{"t.col", nil},
"key2": WhereColumnInfo{"t1.col", nil},
},
},
{
description: "Success: New Where Clause",
expectErrors: false,
startingWhereClause: "",
expectedWhereClause: "\nWHERE UPPER(t.col) LIKE UPPER(:key1_search) OR UPPER(t1.col) LIKE UPPER(:key2_search)",
parameters: map[string]string{
SearchValueParam: "test",
SearchColumnsParam: "key1,key2",
},
searchQueryParamsToSQLCols: map[string]WhereColumnInfo{
"key1": WhereColumnInfo{"t.col", nil},
"key2": WhereColumnInfo{"t1.col", nil},
},
},
{
description: "Success: No search parameters",
expectErrors: false,
startingWhereClause: "",
expectedWhereClause: "",
parameters: map[string]string{},
searchQueryParamsToSQLCols: map[string]WhereColumnInfo{
"key1": WhereColumnInfo{"t.col", nil},
"key2": WhereColumnInfo{"t1.col", nil},
},
},
{
description: "Success: Defined join operator - AND",
expectErrors: false,
startingWhereClause: "",
expectedWhereClause: "\nWHERE UPPER(t.col) LIKE UPPER(:key1_search) AND UPPER(t1.col) LIKE UPPER(:key2_search)",
parameters: map[string]string{
SearchValueParam: "test",
SearchColumnsParam: "key1,key2",
SearchFilterParam: "AND",
},
searchQueryParamsToSQLCols: map[string]WhereColumnInfo{
"key1": WhereColumnInfo{"t.col", nil},
"key2": WhereColumnInfo{"t1.col", nil},
},
},
{
description: "Success: Defined join operator - OR",
expectErrors: false,
startingWhereClause: "",
expectedWhereClause: "\nWHERE UPPER(t.col) LIKE UPPER(:key1_search) OR UPPER(t1.col) LIKE UPPER(:key2_search)",
parameters: map[string]string{
SearchValueParam: "test",
SearchColumnsParam: "key1,key2",
SearchFilterParam: "OR",
},
searchQueryParamsToSQLCols: map[string]WhereColumnInfo{
"key1": WhereColumnInfo{"t.col", nil},
"key2": WhereColumnInfo{"t1.col", nil},
},
},
{
description: "Failure: Invalid join operator",
expectErrors: true,
startingWhereClause: "",
expectedWhereClause: "",
parameters: map[string]string{
SearchValueParam: "test",
SearchColumnsParam: "key1,key2",
SearchFilterParam: "Bogus",
},
searchQueryParamsToSQLCols: map[string]WhereColumnInfo{
"key1": WhereColumnInfo{"t.col", nil},
"key2": WhereColumnInfo{"t1.col", nil},
},
},
}

for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
t.Log("Starting test scenario: ", tc.description)
where, _, errors := AddSearchableWhereClause(tc.startingWhereClause, nil, tc.parameters, tc.searchQueryParamsToSQLCols)
if len(errors) == 0 && tc.expectErrors {
t.Error("expected errors on building search where clause and received non")
return
} else if len(errors) != 0 && !tc.expectErrors {
t.Errorf("expected no errors on building search where clause and received: %v", util.JoinErrs(errors))
return
}
if where != tc.expectedWhereClause {
t.Errorf("expected resulting WHERE Clause %v received %v", tc.expectedWhereClause, where)
}
})
}
}

func TestGetCacheGroupByName(t *testing.T) {
var testCases = []struct {
description string
Expand Down
Loading