This is an automated email from the ASF dual-hosted git repository. dangogh pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-trafficcontrol.git
commit ef3fb6fa1737d001f211e93122ec2a61092bbeb3 Author: Dewayne Richardson <dewr...@apache.org> AuthorDate: Mon Feb 19 13:30:02 2018 -0700 updated to implement the CUD for statuses --- traffic_ops/client/status.go | 132 ++++++++++++ traffic_ops/testing/api/status_test.go | 108 ++++++++++ traffic_ops/testing/api/v13/traffic_control.go | 9 +- traffic_ops/traffic_ops_golang/status/statuses.go | 240 +++++++++++++++++++++- 4 files changed, 484 insertions(+), 5 deletions(-) diff --git a/traffic_ops/client/status.go b/traffic_ops/client/status.go new file mode 100644 index 0000000..1145d86 --- /dev/null +++ b/traffic_ops/client/status.go @@ -0,0 +1,132 @@ +/* + + 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. +*/ + +package client + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + + "github.com/apache/incubator-trafficcontrol/lib/go-tc" +) + +const ( + API_v13_STATUSES = "/api/1.3/statuses" +) + +// Create a Status +func (to *Session) CreateStatus(status tc.Status) (tc.Alerts, ReqInf, error) { + + var remoteAddr net.Addr + reqBody, err := json.Marshal(status) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return tc.Alerts{}, reqInf, err + } + resp, remoteAddr, err := to.request(http.MethodPost, API_v13_STATUSES, reqBody) + if err != nil { + return tc.Alerts{}, reqInf, err + } + defer resp.Body.Close() + var alerts tc.Alerts + err = json.NewDecoder(resp.Body).Decode(&alerts) + return alerts, reqInf, nil +} + +// Update a Status by ID +func (to *Session) UpdateStatusByID(id int, status tc.Status) (tc.Alerts, ReqInf, error) { + + var remoteAddr net.Addr + reqBody, err := json.Marshal(status) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return tc.Alerts{}, reqInf, err + } + route := fmt.Sprintf("%s/%d", API_v13_STATUSES, id) + resp, remoteAddr, err := to.request(http.MethodPut, route, reqBody) + if err != nil { + return tc.Alerts{}, reqInf, err + } + defer resp.Body.Close() + var alerts tc.Alerts + err = json.NewDecoder(resp.Body).Decode(&alerts) + return alerts, reqInf, nil +} + +// Returns a list of Statuses +func (to *Session) GetStatuses() ([]tc.Status, ReqInf, error) { + resp, remoteAddr, err := to.request(http.MethodGet, API_v13_STATUSES, nil) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return nil, reqInf, err + } + defer resp.Body.Close() + + var data tc.StatusesResponse + err = json.NewDecoder(resp.Body).Decode(&data) + return data.Response, reqInf, nil +} + +// GET a Status by the Status id +func (to *Session) GetStatusByID(id int) ([]tc.Status, ReqInf, error) { + route := fmt.Sprintf("%s/%d", API_v13_STATUSES, id) + resp, remoteAddr, err := to.request(http.MethodGet, route, nil) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return nil, reqInf, err + } + defer resp.Body.Close() + + var data tc.StatusesResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, reqInf, err + } + + return data.Response, reqInf, nil +} + +// GET a Status by the Status name +func (to *Session) GetStatusByName(name string) ([]tc.Status, ReqInf, error) { + url := fmt.Sprintf("%s?name=%s", API_v13_STATUSES, name) + 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.StatusesResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, reqInf, err + } + + return data.Response, reqInf, nil +} + +// DELETE a Status by id +func (to *Session) DeleteStatusByID(id int) (tc.Alerts, ReqInf, error) { + route := fmt.Sprintf("%s/%d", API_v13_STATUSES, id) + resp, remoteAddr, err := to.request(http.MethodDelete, route, nil) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return tc.Alerts{}, reqInf, err + } + defer resp.Body.Close() + var alerts tc.Alerts + err = json.NewDecoder(resp.Body).Decode(&alerts) + return alerts, reqInf, nil +} diff --git a/traffic_ops/testing/api/status_test.go b/traffic_ops/testing/api/status_test.go new file mode 100644 index 0000000..994c361 --- /dev/null +++ b/traffic_ops/testing/api/status_test.go @@ -0,0 +1,108 @@ +/* + + 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. +*/ + +package api + +import ( + "fmt" + "testing" + + "github.com/apache/incubator-trafficcontrol/lib/go-log" + "github.com/apache/incubator-trafficcontrol/lib/go-tc" +) + +func TestStatuses(t *testing.T) { + + CreateTestStatuses(t) + UpdateTestStatuses(t) + GetTestStatuses(t) + DeleteTestStatuses(t) + +} + +func CreateTestStatuses(t *testing.T) { + for _, status := range testData.Statuses { + fmt.Printf("status ---> %v\n", status) + resp, _, err := TOSession.CreateStatus(status) + log.Debugln("Response: ", resp) + if err != nil { + t.Errorf("could not CREATE statuss: %v\n", err) + } + } + +} + +func UpdateTestStatuses(t *testing.T) { + + firstStatus := testData.Statuses[0] + // Retrieve the Status by status so we can get the id for the Update + resp, _, err := TOSession.GetStatusByName(firstStatus.Name) + if err != nil { + t.Errorf("cannot GET Status by status: %v - %v\n", firstStatus.Name, err) + } + remoteStatus := resp[0] + expectedStatus := "OFFLINE-TEST" + remoteStatus.Name = expectedStatus + var alert tc.Alerts + alert, _, err = TOSession.UpdateStatusByID(remoteStatus.ID, remoteStatus) + if err != nil { + t.Errorf("cannot UPDATE Status by id: %v - %v\n", err, alert) + } + + // Retrieve the Status to check status got updated + resp, _, err = TOSession.GetStatusByID(remoteStatus.ID) + if err != nil { + t.Errorf("cannot GET Status by status: %v - %v\n", firstStatus.Name, err) + } + respStatus := resp[0] + if respStatus.Name != expectedStatus { + t.Errorf("results do not match actual: %s, expected: %s\n", respStatus.Name, expectedStatus) + } + +} + +func GetTestStatuses(t *testing.T) { + for _, status := range testData.Statuses { + resp, _, err := TOSession.GetStatusByName(status.Name) + if err != nil { + t.Errorf("cannot GET Status by status: %v - %v\n", err, resp) + } + } +} + +func DeleteTestStatuses(t *testing.T) { + + status := testData.Statuses[1] + // Retrieve the Status by name so we can get the id + resp, _, err := TOSession.GetStatusByName(status.Name) + if err != nil { + t.Errorf("cannot GET Status by name: %v - %v\n", status.Name, err) + } + respStatus := resp[0] + + delResp, _, err := TOSession.DeleteStatusByID(respStatus.ID) + if err != nil { + t.Errorf("cannot DELETE Status by status: %v - %v\n", err, delResp) + } + + // Retrieve the Status to see if it got deleted + statusResp, _, err := TOSession.GetStatusByName(status.Name) + if err != nil { + t.Errorf("error deleting Status status: %s\n", err.Error()) + } + if len(statusResp) > 0 { + t.Errorf("expected Status status: %s to be deleted\n", status.Name) + } +} diff --git a/traffic_ops/testing/api/v13/traffic_control.go b/traffic_ops/testing/api/v13/traffic_control.go index 68dae1c..968b9e4 100644 --- a/traffic_ops/testing/api/v13/traffic_control.go +++ b/traffic_ops/testing/api/v13/traffic_control.go @@ -19,7 +19,14 @@ import tcapi "github.com/apache/incubator-trafficcontrol/lib/go-tc" // TrafficControl - maps to the tc-fixtures.json file type TrafficControl struct { + ASNs []tc.ASN `json:"asns"` CDNs []tcapi.CDN `json:"cdns"` - DeliveryServices []tcapi.DeliveryService `json:"deliveryservices"` + Cachegroups []tc.CacheGroup `json:"cachegroups"` DeliveryServiceRequests []tcapi.DeliveryServiceRequest `json:"deliveryServiceRequests"` + DeliveryServices []tc.DeliveryService `json:"deliveryservices"` + DeliveryServices []tcapi.DeliveryService `json:"deliveryservices"` + Divisions []tc.Division `json:"divisions"` + Regions []tc.Region `json:"regions"` + Statuses []tc.Status `json:"statuses"` + Tenants []tc.Tenant `json:"tenants"` } diff --git a/traffic_ops/traffic_ops_golang/status/statuses.go b/traffic_ops/traffic_ops_golang/status/statuses.go index a281a7d..8aca45c 100644 --- a/traffic_ops/traffic_ops_golang/status/statuses.go +++ b/traffic_ops/traffic_ops_golang/status/statuses.go @@ -20,12 +20,16 @@ package status */ import ( + "errors" + "fmt" + "github.com/apache/incubator-trafficcontrol/lib/go-log" "github.com/apache/incubator-trafficcontrol/lib/go-tc" "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/jmoiron/sqlx" + "github.com/lib/pq" ) //we need a type alias to define functions on @@ -38,7 +42,32 @@ func GetRefType() *TOStatus { return &refType } -func (cdn *TOStatus) Read(db *sqlx.DB, parameters map[string]string, user auth.CurrentUser) ([]interface{}, []error, tc.ApiErrorType) { +//Implementation of the Identifier, Validator interface functions +func (status *TOStatus) GetID() int { + return status.ID +} + +func (status *TOStatus) GetAuditName() string { + return status.Name +} + +func (status *TOStatus) GetType() string { + return "status" +} + +func (status *TOStatus) SetID(i int) { + status.ID = i +} + +func (status *TOStatus) Validate(db *sqlx.DB) []error { + errs := []error{} + if len(status.Name) < 1 { + errs = append(errs, errors.New(`Status 'name' is required.`)) + } + return errs +} + +func (status *TOStatus) 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 @@ -63,17 +92,17 @@ func (cdn *TOStatus) Read(db *sqlx.DB, parameters map[string]string, user auth.C } defer rows.Close() - status := []interface{}{} + st := []interface{}{} for rows.Next() { var s tc.Status if err = rows.StructScan(&s); err != nil { log.Errorf("error parsing Status rows: %v", err) return nil, []error{tc.DBError}, tc.SystemError } - status = append(status, s) + st = append(st, s) } - return status, []error{}, tc.NoError + return st, []error{}, tc.NoError } func selectQuery() string { @@ -87,3 +116,206 @@ name FROM status s` return query } + +//The TOStatus implementation of the Updater interface +//all implementations of Updater should use transactions and return the proper errorType +//ParsePQUniqueConstraintError is used to determine if a status 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 (status *TOStatus) 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 status: %++v", updateQuery(), status) + resultRows, err := tx.NamedQuery(updateQuery(), status) + if err != nil { + if pqErr, ok := err.(*pq.Error); ok { + err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr) + if eType == tc.DataConflictError { + return errors.New("a status 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.Time + 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) + status.LastUpdated = lastUpdated + if rowsAffected != 1 { + if rowsAffected < 1 { + return errors.New("no status 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 TOStatus implementation of the Inserter interface +//all implementations of Inserter should use transactions and return the proper errorType +//ParsePQUniqueConstraintError is used to determine if a status 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 status and have +//to be added to the struct +func (status *TOStatus) Insert(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(), status) + if err != nil { + if pqErr, ok := err.(*pq.Error); ok { + err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr) + if eType == tc.DataConflictError { + return errors.New("a status 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.Time + 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 status 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 status insert") + log.Errorln(err) + return tc.DBError, tc.SystemError + } + status.SetID(id) + status.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 +} + +//The Status implementation of the Deleter interface +//all implementations of Deleter should use transactions and return the proper errorType +func (status *TOStatus) 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 status: %++v", deleteQuery(), status) + result, err := tx.NamedExec(deleteQuery(), status) + 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 status with that id found"), tc.DataMissingError + } else { + return fmt.Errorf("this create 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 updateQuery() string { + query := `UPDATE +status SET +name=:name, +description=:description +WHERE id=:id RETURNING last_updated` + return query +} + +func insertQuery() string { + query := `INSERT INTO status ( +name, +description) VALUES ( +:name, +:description) RETURNING id,last_updated` + return query +} + +func deleteQuery() string { + query := `DELETE FROM status +WHERE id=:id` + return query +} -- To stop receiving notification emails like this one, please contact dang...@apache.org.