This is an automated email from the ASF dual-hosted git repository. rob pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-trafficcontrol.git
commit 394ba97e884517e9621582dcc30201cefd4da3ee Author: ASchmidt <andrew_schm...@comcast.com> AuthorDate: Fri May 4 14:53:55 2018 -0600 added DeliveryServicesService Get API in Go --- lib/go-tc/deliveryservice_servers.go | 193 +++++++++++++ .../deliveryservice/servers/servers.go | 300 +++++++++++++++++++++ .../deliveryservice/servers/servers_test.go | 111 ++++++++ 3 files changed, 604 insertions(+) diff --git a/lib/go-tc/deliveryservice_servers.go b/lib/go-tc/deliveryservice_servers.go new file mode 100644 index 0000000..9c7e6ae --- /dev/null +++ b/lib/go-tc/deliveryservice_servers.go @@ -0,0 +1,193 @@ +package tc + +/* + + 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. +*/ + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + log "github.com/apache/incubator-trafficcontrol/lib/go-log" +) + +// IDNoMod type is used to suppress JSON unmarshalling +type IDNoMod int + +// DeliveryServiceRequest is used as part of the workflow to create, +// modify, or delete a delivery service. +type DeliveryServiceRequest struct { + AssigneeID int `json:"assigneeId,omitempty"` + Assignee string `json:"assignee,omitempty"` + AuthorID IDNoMod `json:"authorId"` + Author string `json:"author"` + ChangeType string `json:"changeType"` + CreatedAt *TimeNoMod `json:"createdAt"` + ID int `json:"id"` + LastEditedBy string `json:"lastEditedBy,omitempty"` + LastEditedByID IDNoMod `json:"lastEditedById,omitempty"` + LastUpdated *TimeNoMod `json:"lastUpdated"` + DeliveryService DeliveryService `json:"deliveryService"` + Status RequestStatus `json:"status"` + XMLID string `json:"-" db:"xml_id"` +} + +// DeliveryServiceRequestNullable is used as part of the workflow to create, +// modify, or delete a delivery service. +type DeliveryServiceRequestNullable struct { + AssigneeID *int `json:"assigneeId,omitempty" db:"assignee_id"` + Assignee *string `json:"assignee,omitempty"` + AuthorID *IDNoMod `json:"authorId" db:"author_id"` + Author *string `json:"author"` + ChangeType *string `json:"changeType" db:"change_type"` + CreatedAt *TimeNoMod `json:"createdAt" db:"created_at"` + ID *int `json:"id" db:"id"` + LastEditedBy *string `json:"lastEditedBy"` + LastEditedByID *IDNoMod `json:"lastEditedById" db:"last_edited_by_id"` + LastUpdated *TimeNoMod `json:"lastUpdated" db:"last_updated"` + DeliveryService *DeliveryServiceNullable `json:"deliveryService" db:"deliveryservice"` + Status *RequestStatus `json:"status" db:"status"` + XMLID *string `json:"-" db:"xml_id"` +} + +// UnmarshalJSON implements the json.Unmarshaller interface to suppress unmarshalling for IDNoMod +func (a *IDNoMod) UnmarshalJSON([]byte) error { + return nil +} + +// RequestStatus captures where in the workflow this request is +type RequestStatus string + +const ( + // RequestStatusInvalid -- invalid state + RequestStatusInvalid = RequestStatus("invalid") + // RequestStatusDraft -- newly created; not ready to be reviewed + RequestStatusDraft = RequestStatus("draft") + // RequestStatusSubmitted -- newly created; ready to be reviewed + RequestStatusSubmitted = RequestStatus("submitted") + // RequestStatusRejected -- reviewed, but problems found + RequestStatusRejected = RequestStatus("rejected") + // RequestStatusPending -- reviewed and locked; ready to be implemented + RequestStatusPending = RequestStatus("pending") + // RequestStatusComplete -- implemented and locked + RequestStatusComplete = RequestStatus("complete") +) + +// RequestStatuses -- user-visible string associated with each of the above +var RequestStatuses = []RequestStatus{ + // "invalid" -- don't list here.. + "draft", + "submitted", + "rejected", + "pending", + "complete", +} + +// UnmarshalJSON implements json.Unmarshaller +func (r *RequestStatus) UnmarshalJSON(b []byte) error { + u, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + + // just check to see if the string represents a valid requeststatus + _, err = RequestStatusFromString(u) + if err != nil { + return err + } + return json.Unmarshal(b, (*string)(r)) +} + +// MarshalJSON implements json.Marshaller +func (r RequestStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(string(r)) +} + +// Value implements driver.Valuer +func (r *RequestStatus) Value() (driver.Value, error) { + v, err := json.Marshal(r) + log.Debugf("value is %v; err is %v", v, err) + v = []byte(strings.Trim(string(v), `"`)) + return v, err +} + +// Scan implements sql.Scanner +func (r *RequestStatus) Scan(src interface{}) error { + b, ok := src.([]byte) + if !ok { + return fmt.Errorf("expected requeststatus in byte array form; got %T", src) + } + b = []byte(`"` + string(b) + `"`) + return json.Unmarshal(b, r) +} + +// RequestStatusFromString gets the status enumeration from a string +func RequestStatusFromString(rs string) (RequestStatus, error) { + if rs == "" { + return RequestStatusDraft, nil + } + for _, s := range RequestStatuses { + if string(s) == rs { + return s, nil + } + } + return RequestStatusInvalid, errors.New(rs + " is not a valid RequestStatus name") +} + +// ValidTransition returns nil if the transition is allowed for the workflow, an error if not +func (r RequestStatus) ValidTransition(to RequestStatus) error { + if r == RequestStatusRejected || r == RequestStatusComplete { + // once rejected or completed, no changes allowed + return errors.New(string(r) + " request cannot be changed") + } + + if r == to { + // no change -- always allowed + return nil + } + + // indicate if valid transitioning to this RequestStatus + switch to { + case RequestStatusDraft: + // can go back to draft if submitted or rejected + if r == RequestStatusSubmitted { + return nil + } + case RequestStatusSubmitted: + // can go be submitted if draft or rejected + if r == RequestStatusDraft { + return nil + } + case RequestStatusRejected: + // only submitted can be rejected + if r == RequestStatusSubmitted { + return nil + } + case RequestStatusPending: + // only submitted can move to pending + if r == RequestStatusSubmitted { + return nil + } + case RequestStatusComplete: + // only pending can be completed. Completed can never change. + if r == RequestStatusPending { + return nil + } + } + return errors.New("invalid transition from " + string(r) + " to " + string(to)) +} diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go new file mode 100644 index 0000000..463ff06 --- /dev/null +++ b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go @@ -0,0 +1,300 @@ +package profileparameter + +/* + * 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" + + "github.com/apache/incubator-trafficcontrol/lib/go-log" + tc "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" +) + +const ( + ProfileIDQueryParam = "profileId" + ParameterIDQueryParam = "parameterId" +) + +//we need a type alias to define functions on +type TOProfileParameter v13.ProfileParameterNullable + +//the refType is passed into the handlers where a copy of its type is used to decode the json. +var refType = TOProfileParameter(v13.ProfileParameterNullable{}) + +func GetRefType() *TOProfileParameter { + return &refType +} + +func (pp TOProfileParameter) GetKeyFieldsInfo() []api.KeyFieldInfo { + return []api.KeyFieldInfo{{ProfileIDQueryParam, api.GetIntKey}, {ParameterIDQueryParam, api.GetIntKey}} +} + +//Implementation of the Identifier, Validator interface functions +func (pp TOProfileParameter) GetKeys() (map[string]interface{}, bool) { + if pp.ProfileID == nil { + return map[string]interface{}{ProfileIDQueryParam: 0}, false + } + if pp.ParameterID == nil { + return map[string]interface{}{ParameterIDQueryParam: 0}, false + } + keys := make(map[string]interface{}) + profileID := *pp.ProfileID + parameterID := *pp.ParameterID + + keys[ProfileIDQueryParam] = profileID + keys[ParameterIDQueryParam] = parameterID + return keys, true +} + +func (pp *TOProfileParameter) GetAuditName() string { + if pp.ProfileID != nil { + return strconv.Itoa(*pp.ProfileID) + "-" + strconv.Itoa(*pp.ParameterID) + } + return "unknown" +} + +func (pp *TOProfileParameter) GetType() string { + return "profileParameter" +} + +func (pp *TOProfileParameter) SetKeys(keys map[string]interface{}) { + profId, _ := keys[ProfileIDQueryParam].(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. + pp.ProfileID = &profId + + paramId, _ := keys[ParameterIDQueryParam].(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. + pp.ParameterID = ¶mId +} + +// Validate fulfills the api.Validator interface +func (pp *TOProfileParameter) Validate(db *sqlx.DB) []error { + + errs := validation.Errors{ + "profile": validation.Validate(pp.ProfileID, validation.Required), + "parameter": validation.Validate(pp.ParameterID, validation.Required), + } + + return tovalidate.ToErrors(errs) +} + +//The TOProfileParameter implementation of the Creator interface +//all implementations of Creator should use transactions and return the proper errorType +//ParsePQUniqueConstraintError is used to determine if a profileparameter 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 profile and lastUpdated values of the newly inserted profileparameter and have +//to be added to the struct +func (pp *TOProfileParameter) 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(), pp) + if err != nil { + if pqErr, ok := err.(*pq.Error); ok { + err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr) + if eType == tc.DataConflictError { + return errors.New("a parameter with " + err.Error()), eType + } + return err, eType + } + log.Errorf("received non pq error: %++v from create execution", err) + return tc.DBError, tc.SystemError + } + defer resultRows.Close() + + var profile int + var parameter int + var lastUpdated tc.TimeNoMod + rowsAffected := 0 + for resultRows.Next() { + rowsAffected++ + if err := resultRows.Scan(&profile, ¶meter, &lastUpdated); err != nil { + log.Error.Printf("could not scan profile from insert: %s\n", err) + return tc.DBError, tc.SystemError + } + } + if rowsAffected == 0 { + err = errors.New("no profile_parameter was inserted, no profile+parameter was returned") + log.Errorln(err) + return tc.DBError, tc.SystemError + } + if rowsAffected > 1 { + err = errors.New("too many ids returned from parameter insert") + log.Errorln(err) + return tc.DBError, tc.SystemError + } + + pp.SetKeys(map[string]interface{}{ProfileIDQueryParam: profile, ParameterIDQueryParam: parameter}) + pp.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 insertQuery() string { + query := `INSERT INTO profile_parameter ( +profile, +parameter) VALUES ( +:profile_id, +:parameter_id) RETURNING profile, parameter, last_updated` + return query +} + +func (pp *TOProfileParameter) 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{ + "profileId": dbhelpers.WhereColumnInfo{"pp.profile", nil}, + "parameterId": dbhelpers.WhereColumnInfo{"pp.parameter", nil}, + "lastUpdated": dbhelpers.WhereColumnInfo{"pp.last_updated", 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 Parameters: %v", err) + return nil, []error{tc.DBError}, tc.SystemError + } + defer rows.Close() + + params := []interface{}{} + for rows.Next() { + var p v13.ProfileParameterNullable + if err = rows.StructScan(&p); err != nil { + log.Errorf("error parsing pp rows: %v", err) + return nil, []error{tc.DBError}, tc.SystemError + } + params = append(params, p) + } + + return params, []error{}, tc.NoError + +} + +//The Parameter implementation of the Deleter interface +//all implementations of Deleter should use transactions and return the proper errorType +func (pp *TOProfileParameter) 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 parameter: %++v", deleteQuery(), pp) + result, err := tx.NamedExec(deleteQuery(), pp) + 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 { + return errors.New("no parameter with that id found"), tc.DataMissingError + } + if rowsAffected > 1 { + 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 selectQuery() string { + + query := `SELECT +pp.last_updated, +pp.profile profile_id, +pp.parameter parameter_id, +prof.name profile, +param.name parameter +FROM profile_parameter pp +JOIN profile prof ON prof.id = pp.profile +JOIN parameter param ON param.id = pp.parameter` + return query +} + +func updateQuery() string { + query := `UPDATE +profile_parameter SET +profile=:profile_id, +parameter=:parameter_id +WHERE profile=:profile_id AND + parameter = :parameter_id + RETURNING last_updated` + return query +} + +func deleteQuery() string { + query := `DELETE FROM profile_parameter + WHERE profile=:profile_id and parameter=:parameter_id` + return query +} diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers_test.go new file mode 100644 index 0000000..9e7ce20 --- /dev/null +++ b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers_test.go @@ -0,0 +1,111 @@ +package profileparameter + +/* + * 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 ( + "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 getTestProfileParameters() []v13.ProfileParameterNullable { + pps := []v13.ProfileParameterNullable{} + lastUpdated := tc.TimeNoMod{} + lastUpdated.Scan(time.Now()) + profileID := 1 + parameterID := 1 + + pp := v13.ProfileParameterNullable{ + LastUpdated: &lastUpdated, + ProfileID: &profileID, + ParameterID: ¶meterID, + } + pps = append(pps, pp) + + pp2 := pp + pp2.ProfileID = &profileID + pp2.ParameterID = ¶meterID + pps = append(pps, pp2) + + return pps +} + +func TestGetProfileParameters(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() + + testPPs := getTestProfileParameters() + cols := test.ColsFromStructByTag("db", v13.ProfileParameterNullable{}) + rows := sqlmock.NewRows(cols) + + for _, ts := range testPPs { + rows = rows.AddRow( + ts.LastUpdated, + ts.Profile, + ts.ProfileID, + ts.Parameter, + ts.ParameterID, + ) + } + mock.ExpectQuery("SELECT").WillReturnRows(rows) + v := map[string]string{"profile": "1"} + + pps, errs, _ := refType.Read(db, v, auth.CurrentUser{}) + if len(errs) > 0 { + t.Errorf("profileparameter.Read expected: no errors, actual: %v", errs) + } + + if len(pps) != 2 { + t.Errorf("profileparameter.Read expected: len(pps) == 2, actual: %v", len(pps)) + } + +} + +func TestInterfaces(t *testing.T) { + var i interface{} + i = &TOProfileParameter{} + + if _, ok := i.(api.Creator); !ok { + t.Errorf("ProfileParameter must be Creator") + } + if _, ok := i.(api.Reader); !ok { + t.Errorf("ProfileParameter must be Reader") + } + if _, ok := i.(api.Deleter); !ok { + t.Errorf("ProfileParameter must be Deleter") + } + if _, ok := i.(api.Identifier); !ok { + t.Errorf("ProfileParameter must be Identifier") + } +} -- To stop receiving notification emails like this one, please contact r...@apache.org.