This is an automated email from the ASF dual-hosted git repository. dewrich pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-trafficcontrol.git
commit 371979415af3ed627d8023b96e97274a36b5711e Author: Rawlin Peters <rawlin_pet...@comcast.com> AuthorDate: Fri Apr 6 12:11:00 2018 -0600 Add a Location API Adding a Location API is the first step to refactoring lat/lon out of the Cache Group API into its own entity - Location. With the Location API in place, the cachegroup table can be updated to reference a Location, and other future entities (such as Origins) can make use of Locations as well. This is part of the larger "geolocation-based client steering" effort. --- lib/go-tc/v13/locations.go | 98 ++++++ .../db/migrations/20180409000000_add_location.sql | 31 ++ .../traffic_ops_golang/location/locations.go | 369 +++++++++++++++++++++ .../traffic_ops_golang/location/locations_test.go | 176 ++++++++++ traffic_ops/traffic_ops_golang/routes.go | 8 + 5 files changed, 682 insertions(+) diff --git a/lib/go-tc/v13/locations.go b/lib/go-tc/v13/locations.go new file mode 100644 index 0000000..282c487 --- /dev/null +++ b/lib/go-tc/v13/locations.go @@ -0,0 +1,98 @@ +package v13 + +import tc "github.com/apache/incubator-trafficcontrol/lib/go-tc" + +/* + * 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. + */ + +// A List of Locations Response +// swagger:response LocationsResponse +// in: body +type LocationsResponse struct { + // in: body + Response []Location `json:"response"` +} + +// A Single Location Response for Update and Create to depict what changed +// swagger:response LocationResponse +// in: body +type LocationResponse struct { + // in: body + Response Location `json:"response"` +} + +// Location ... +type Location struct { + + // The Location to retrieve + // + // ID of the Location + // + // required: true + ID int `json:"id" db:"id"` + + // Name of the Location + // + // required: true + Name string `json:"name" db:"name"` + + // the latitude of the Location + // + // required: true + Latitude float64 `json:"latitude" db:"latitude"` + + // the latitude of the Location + // + // required: true + Longitude float64 `json:"longitude" db:"longitude"` + + // LastUpdated + // + LastUpdated tc.TimeNoMod `json:"lastUpdated" db:"last_updated"` +} + +// LocationNullable ... +type LocationNullable struct { + + // The Location to retrieve + // + // ID of the Location + // + // required: true + ID *int `json:"id" db:"id"` + + // Name of the Location + // + // required: true + Name *string `json:"name" db:"name"` + + // the latitude of the Location + // + // required: true + Latitude *float64 `json:"latitude" db:"latitude"` + + // the latitude of the Location + // + // required: true + Longitude *float64 `json:"longitude" db:"longitude"` + + // LastUpdated + // + LastUpdated *tc.TimeNoMod `json:"lastUpdated" db:"last_updated"` +} diff --git a/traffic_ops/app/db/migrations/20180409000000_add_location.sql b/traffic_ops/app/db/migrations/20180409000000_add_location.sql new file mode 100644 index 0000000..1a259be --- /dev/null +++ b/traffic_ops/app/db/migrations/20180409000000_add_location.sql @@ -0,0 +1,31 @@ +/* + + Licensed 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. +*/ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied + +CREATE TABLE location ( + id bigserial primary key NOT NULL, + name text UNIQUE NOT NULL, + latitude numeric NOT NULL DEFAULT 0.0, + longitude numeric NOT NULL DEFAULT 0.0, + last_updated timestamp WITH time zone NOT NULL DEFAULT now() +); + +CREATE TRIGGER on_update_current_timestamp BEFORE UPDATE ON location FOR EACH ROW EXECUTE PROCEDURE on_update_current_timestamp_last_updated(); + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back +DROP TABLE location; diff --git a/traffic_ops/traffic_ops_golang/location/locations.go b/traffic_ops/traffic_ops_golang/location/locations.go new file mode 100644 index 0000000..cab72d8 --- /dev/null +++ b/traffic_ops/traffic_ops_golang/location/locations.go @@ -0,0 +1,369 @@ +package location + +/* + * 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" + "strconv" + "strings" + + "github.com/apache/incubator-trafficcontrol/lib/go-log" + "github.com/apache/incubator-trafficcontrol/lib/go-tc" + "github.com/apache/incubator-trafficcontrol/lib/go-tc/v13" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tovalidate" + validation "github.com/go-ozzo/ozzo-validation" + "github.com/jmoiron/sqlx" + "github.com/lib/pq" +) + +//we need a type alias to define functions on +type TOLocation v13.LocationNullable + +//the refType is passed into the handlers where a copy of its type is used to decode the json. +var refType = TOLocation{} + +func GetRefType() *TOLocation { + return &refType +} + +func (location TOLocation) GetKeyFieldsInfo() []api.KeyFieldInfo { + return []api.KeyFieldInfo{{"id", api.GetIntKey}} +} + +//Implementation of the Identifier, Validator interface functions +func (location TOLocation) GetKeys() (map[string]interface{}, bool) { + if location.ID == nil { + return map[string]interface{}{"id": 0}, false + } + return map[string]interface{}{"id": *location.ID}, true +} + +func (location TOLocation) GetAuditName() string { + if location.Name != nil { + return *location.Name + } + if location.ID != nil { + return strconv.Itoa(*location.ID) + } + return "0" +} + +func (location TOLocation) GetType() string { + return "location" +} + +func (location *TOLocation) 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. + location.ID = &i +} + +func isValidLocationChar(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 +} + +// IsValidLocationName returns true if the name contains only characters valid for a Location name +func IsValidLocationName(str string) bool { + i := strings.IndexFunc(str, func(r rune) bool { return !isValidLocationChar(r) }) + return i == -1 +} + +// Validate fulfills the api.Validator interface +func (location TOLocation) Validate(db *sqlx.DB) []error { + validName := validation.NewStringRule(IsValidLocationName, "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(location.Name, validation.Required, validName), + "latitude": validation.Validate(location.Latitude, validation.Min(-90.0).Error(latitudeErr), validation.Max(90.0).Error(latitudeErr)), + "longitude": validation.Validate(location.Longitude, validation.Min(-180.0).Error(longitudeErr), validation.Max(180.0).Error(longitudeErr)), + } + return tovalidate.ToErrors(errs) +} + +//The TOLocation implementation of the Creator interface +//all implementations of Creator should use transactions and return the proper errorType +//ParsePQUniqueConstraintError is used to determine if a location with conflicting values exists +//if so, it will return an errorType of DataConflict and the type should be appended to the +//generic error message returned +//The insert sql returns the id and lastUpdated values of the newly inserted location and have +//to be added to the struct +func (location *TOLocation) Create(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) { + rollbackTransaction := true + tx, err := db.Beginx() + defer func() { + if tx == nil || !rollbackTransaction { + return + } + err := tx.Rollback() + if err != nil { + log.Errorln(errors.New("rolling back transaction: " + err.Error())) + } + }() + + if err != nil { + log.Error.Printf("could not begin transaction: %v", err) + return tc.DBError, tc.SystemError + } + resultRows, err := tx.NamedQuery(insertQuery(), location) + if err != nil { + if pqErr, ok := err.(*pq.Error); ok { + err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr) + if eType == tc.DataConflictError { + return errors.New("a location with " + err.Error()), eType + } + return err, eType + } else { + log.Errorf("received non pq error: %++v from create execution", err) + return tc.DBError, tc.SystemError + } + } + defer resultRows.Close() + + var id int + var lastUpdated tc.TimeNoMod + rowsAffected := 0 + for resultRows.Next() { + rowsAffected++ + if err := resultRows.Scan(&id, &lastUpdated); err != nil { + log.Error.Printf("could not scan id from insert: %s\n", err) + return tc.DBError, tc.SystemError + } + } + if rowsAffected == 0 { + err = errors.New("no location was inserted, no id was returned") + log.Errorln(err) + return tc.DBError, tc.SystemError + } else if rowsAffected > 1 { + err = errors.New("too many ids returned from location insert") + log.Errorln(err) + return tc.DBError, tc.SystemError + } + location.SetKeys(map[string]interface{}{"id": id}) + location.LastUpdated = &lastUpdated + err = tx.Commit() + if err != nil { + log.Errorln("Could not commit transaction: ", err) + return tc.DBError, tc.SystemError + } + rollbackTransaction = false + return nil, tc.NoError +} + +func (location *TOLocation) Read(db *sqlx.DB, parameters map[string]string, user auth.CurrentUser) ([]interface{}, []error, tc.ApiErrorType) { + var rows *sqlx.Rows + + // Query Parameters to Database Query column mappings + // see the fields mapped in the SQL query + queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{ + "id": dbhelpers.WhereColumnInfo{"id", api.IsInt}, + "name": dbhelpers.WhereColumnInfo{"name", nil}, + } + where, orderBy, queryValues, errs := dbhelpers.BuildWhereAndOrderBy(parameters, queryParamsToQueryCols) + if len(errs) > 0 { + return nil, errs, tc.DataConflictError + } + + query := selectQuery() + where + orderBy + log.Debugln("Query is ", query) + + rows, err := db.NamedQuery(query, queryValues) + if err != nil { + log.Errorf("Error querying Location: %v", err) + return nil, []error{tc.DBError}, tc.SystemError + } + defer rows.Close() + + Locations := []interface{}{} + for rows.Next() { + var s TOLocation + if err = rows.StructScan(&s); err != nil { + log.Errorf("error parsing Location rows: %v", err) + return nil, []error{tc.DBError}, tc.SystemError + } + Locations = append(Locations, s) + } + + return Locations, []error{}, tc.NoError +} + +//The TOLocation implementation of the Updater interface +//all implementations of Updater should use transactions and return the proper errorType +//ParsePQUniqueConstraintError is used to determine if a location with conflicting values exists +//if so, it will return an errorType of DataConflict and the type should be appended to the +//generic error message returned +func (location *TOLocation) Update(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) { + rollbackTransaction := true + tx, err := db.Beginx() + defer func() { + if tx == nil || !rollbackTransaction { + return + } + err := tx.Rollback() + if err != nil { + log.Errorln(errors.New("rolling back transaction: " + err.Error())) + } + }() + + if err != nil { + log.Error.Printf("could not begin transaction: %v", err) + return tc.DBError, tc.SystemError + } + log.Debugf("about to run exec query: %s with location: %++v", updateQuery(), location) + resultRows, err := tx.NamedQuery(updateQuery(), location) + if err != nil { + if pqErr, ok := err.(*pq.Error); ok { + err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr) + if eType == tc.DataConflictError { + return errors.New("a location with " + err.Error()), eType + } + return err, eType + } else { + log.Errorf("received error: %++v from update execution", err) + return tc.DBError, tc.SystemError + } + } + defer resultRows.Close() + + var lastUpdated tc.TimeNoMod + rowsAffected := 0 + for resultRows.Next() { + rowsAffected++ + if err := resultRows.Scan(&lastUpdated); err != nil { + log.Error.Printf("could not scan lastUpdated from insert: %s\n", err) + return tc.DBError, tc.SystemError + } + } + log.Debugf("lastUpdated: %++v", lastUpdated) + location.LastUpdated = &lastUpdated + if rowsAffected != 1 { + if rowsAffected < 1 { + return errors.New("no location found with this id"), tc.DataMissingError + } else { + return fmt.Errorf("this update affected too many rows: %d", rowsAffected), tc.SystemError + } + } + err = tx.Commit() + if err != nil { + log.Errorln("Could not commit transaction: ", err) + return tc.DBError, tc.SystemError + } + rollbackTransaction = false + return nil, tc.NoError +} + +//The Location implementation of the Deleter interface +//all implementations of Deleter should use transactions and return the proper errorType +func (location *TOLocation) Delete(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) { + rollbackTransaction := true + tx, err := db.Beginx() + defer func() { + if tx == nil || !rollbackTransaction { + return + } + err := tx.Rollback() + if err != nil { + log.Errorln(errors.New("rolling back transaction: " + err.Error())) + } + }() + + if err != nil { + log.Error.Printf("could not begin transaction: %v", err) + return tc.DBError, tc.SystemError + } + log.Debugf("about to run exec query: %s with location: %++v", deleteQuery(), location) + result, err := tx.NamedExec(deleteQuery(), location) + if err != nil { + log.Errorf("received error: %++v from delete execution", err) + return tc.DBError, tc.SystemError + } + rowsAffected, err := result.RowsAffected() + if err != nil { + return tc.DBError, tc.SystemError + } + if rowsAffected != 1 { + if rowsAffected < 1 { + return errors.New("no location with that id found"), tc.DataMissingError + } else { + return fmt.Errorf("this delete affected too many rows: %d", rowsAffected), tc.SystemError + } + } + err = tx.Commit() + if err != nil { + log.Errorln("Could not commit transaction: ", err) + return tc.DBError, tc.SystemError + } + rollbackTransaction = false + return nil, tc.NoError +} + +func selectQuery() string { + query := `SELECT +id, +latitude, +longitude, +last_updated, +name + +FROM location l` + return query +} + +func updateQuery() string { + query := `UPDATE +location SET +latitude=:latitude, +longitude=:longitude, +name=:name +WHERE id=:id RETURNING last_updated` + return query +} + +func insertQuery() string { + query := `INSERT INTO location ( +latitude, +longitude, +name) VALUES ( +:latitude, +:longitude, +:name) RETURNING id,last_updated` + return query +} + +func deleteQuery() string { + query := `DELETE FROM location +WHERE id=:id` + return query +} diff --git a/traffic_ops/traffic_ops_golang/location/locations_test.go b/traffic_ops/traffic_ops_golang/location/locations_test.go new file mode 100644 index 0000000..4ca6291 --- /dev/null +++ b/traffic_ops/traffic_ops_golang/location/locations_test.go @@ -0,0 +1,176 @@ +package location + +/* + * 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" + "reflect" + "strings" + "testing" + "time" + + "github.com/apache/incubator-trafficcontrol/lib/go-tc" + "github.com/apache/incubator-trafficcontrol/lib/go-tc/v13" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/test" + "github.com/jmoiron/sqlx" + + sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" +) + +func getTestLocations() []v13.Location { + locs := []v13.Location{} + testLoc1 := v13.Location{ + ID: 1, + Name: "location1", + Latitude: 38.7, + Longitude: 90.7, + LastUpdated: tc.TimeNoMod{Time: time.Now()}, + } + locs = append(locs, testLoc1) + + testLoc2 := v13.Location{ + ID: 2, + Name: "location2", + Latitude: 38.7, + Longitude: 90.7, + LastUpdated: tc.TimeNoMod{Time: time.Now()}, + } + locs = append(locs, testLoc2) + + return locs +} + +func TestReadLocations(t *testing.T) { + mockDB, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer mockDB.Close() + + db := sqlx.NewDb(mockDB, "sqlmock") + defer db.Close() + + refType := GetRefType() + + testLocs := getTestLocations() + cols := test.ColsFromStructByTag("db", v13.Location{}) + rows := sqlmock.NewRows(cols) + + for _, ts := range testLocs { + rows = rows.AddRow( + ts.ID, + ts.Name, + ts.Latitude, + ts.Longitude, + ts.LastUpdated, + ) + } + mock.ExpectQuery("SELECT").WillReturnRows(rows) + v := map[string]string{"id": "1"} + + locations, errs, _ := refType.Read(db, v, auth.CurrentUser{}) + if len(errs) > 0 { + t.Errorf("location.Read expected: no errors, actual: %v", errs) + } + + if len(locations) != 2 { + t.Errorf("location.Read expected: len(locations) == 2, actual: %v", len(locations)) + } +} + +func TestFuncs(t *testing.T) { + if strings.Index(selectQuery(), "SELECT") != 0 { + t.Errorf("expected selectQuery to start with SELECT") + } + if strings.Index(insertQuery(), "INSERT") != 0 { + t.Errorf("expected insertQuery to start with INSERT") + } + if strings.Index(updateQuery(), "UPDATE") != 0 { + t.Errorf("expected updateQuery to start with UPDATE") + } + if strings.Index(deleteQuery(), "DELETE") != 0 { + t.Errorf("expected deleteQuery to start with DELETE") + } +} + +func TestInterfaces(t *testing.T) { + var i interface{} + i = &TOLocation{} + + if _, ok := i.(api.Creator); !ok { + t.Errorf("location must be creator") + } + if _, ok := i.(api.Reader); !ok { + t.Errorf("location must be reader") + } + if _, ok := i.(api.Updater); !ok { + t.Errorf("location must be updater") + } + if _, ok := i.(api.Deleter); !ok { + t.Errorf("location must be deleter") + } + if _, ok := i.(api.Identifier); !ok { + t.Errorf("location must be Identifier") + } +} + +func TestValidate(t *testing.T) { + // invalid name, latitude, and longitude + id := 1 + nm := "not!a!valid!name" + la := -190.0 + lo := -190.0 + lu := tc.TimeNoMod{Time: time.Now()} + c := TOLocation{ID: &id, + Name: &nm, + Latitude: &la, + Longitude: &lo, + LastUpdated: &lu, + } + errs := test.SortErrors(c.Validate(nil)) + + expectedErrs := []error{ + errors.New(`'latitude' Must be a floating point number within the range +-90`), + errors.New(`'longitude' Must be a floating point number within the range +-180`), + errors.New(`'name' invalid characters found - Use alphanumeric . or - or _ .`), + } + + if !reflect.DeepEqual(expectedErrs, errs) { + t.Errorf("expected %s, got %s", expectedErrs, errs) + } + + // valid name, latitude, longitude + nm = "This.is.2.a-Valid---Location." + la = 90.0 + lo = 90.0 + c = TOLocation{ID: &id, + Name: &nm, + Latitude: &la, + Longitude: &lo, + LastUpdated: &lu, + } + expectedErrs = []error{} + errs = c.Validate(nil) + if !reflect.DeepEqual(expectedErrs, errs) { + t.Errorf("expected %s, got %s", expectedErrs, errs) + } +} diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go index 3453d19..01a830f 100644 --- a/traffic_ops/traffic_ops_golang/routes.go +++ b/traffic_ops/traffic_ops_golang/routes.go @@ -41,6 +41,7 @@ import ( "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice/request/comment" "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/division" "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/hwinfo" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/location" "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/parameter" "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/physlocation" "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/ping" @@ -206,6 +207,13 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) { {1.3, http.MethodPut, `deliveryservices/{xmlID}/urisignkeys$`, saveDeliveryServiceURIKeysHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil}, {1.3, http.MethodDelete, `deliveryservices/{xmlID}/urisignkeys$`, removeDeliveryServiceURIKeysHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil}, + //Locations + {1.3, http.MethodGet, `locations/?(\.json)?$`, api.ReadHandler(location.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil}, + {1.3, http.MethodGet, `locations/?$`, api.ReadHandler(location.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil}, + {1.3, http.MethodPut, `locations/?$`, api.UpdateHandler(location.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil}, + {1.3, http.MethodPost, `locations/?$`, api.CreateHandler(location.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil}, + {1.3, http.MethodDelete, `locations/?$`, api.DeleteHandler(location.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil}, + //Servers {1.3, http.MethodPost, `servers/{id}/deliveryservices$`, server.AssignDeliveryServicesToServerHandler(d.DB), auth.PrivLevelOperations, Authenticated, nil}, {1.3, http.MethodGet, `servers/{host_name}/update_status$`, server.GetServerUpdateStatusHandler(d.DB), auth.PrivLevelReadOnly, Authenticated, nil}, -- To stop receiving notification emails like this one, please contact dewr...@apache.org.