You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ra...@apache.org on 2018/08/09 18:54:14 UTC

[trafficcontrol] 01/10: Handlers and Types for cdns/:name/federations

This is an automated email from the ASF dual-hosted git repository.

rawlin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git

commit 41e9fd7e5c579e5a8fb2d5cc2d938c90d8abba3a
Author: moltzaum <ma...@moltzau.net>
AuthorDate: Sat Jul 14 20:18:57 2018 -0600

    Handlers and Types for cdns/:name/federations
    
    This is the majority of the infrastructure, it mostly just doesn't include the client and the tests.
---
 lib/go-tc/v13/federations.go                       |  75 ++++++
 .../cdnfederation/cdnfederations.go                | 295 +++++++++++++++++++++
 traffic_ops/traffic_ops_golang/routes.go           |   9 +
 3 files changed, 379 insertions(+)

diff --git a/lib/go-tc/v13/federations.go b/lib/go-tc/v13/federations.go
new file mode 100644
index 0000000..d2d67d3
--- /dev/null
+++ b/lib/go-tc/v13/federations.go
@@ -0,0 +1,75 @@
+package v13
+
+/*
+ * 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.
+ */
+
+// Other endpoints define their own alert. Why not just use tc.Alert?
+type FederationAlert struct {
+	Level string `json:"level"`
+	Text  string `json:"text"`
+}
+
+type CDNFederationResponse struct {
+	Response []CDNFederation `json:"response"`
+}
+
+type CreateCDNFederationResponse struct {
+	Response CDNFederation     `json:"response"`
+	Alerts   []FederationAlert `json:"alerts"`
+}
+
+type UpdateCDNFederationResponse struct {
+	Reponse CDNFederation     `json:"response"`
+	Alerts  []FederationAlert `json:"alerts"`
+}
+
+type DeleteCDNFederationResponse struct {
+	Alerts []FederationAlert `json:"alerts"`
+}
+
+type CDNFederation struct {
+	ID                  int    `json:"id"`
+	CName               string `json:"cname"`
+	Ttl                 int    `json:"ttl"`
+	Description         string `json:"description"`
+	*DeliveryServiceIDs `json:"deliveryService,omitempty"`
+}
+
+type CDNFederationNullable struct {
+	ID          *int    `json:"id" db:"id"`
+	CName       *string `json:"cname" db:"cname"`
+	Ttl         *int    `json:"ttl" db:"ttl"`
+	Description *string `json:"description" db:"description"`
+
+	//omitempty only works with primitive types and pointers
+	*DeliveryServiceIDsNullable `json:"deliveryService,omitempty"`
+
+	//Extra datapoint for ease in Read function (cannot Scan after StructScan)
+	Name *string `json:"-" db:"cdn_name"`
+}
+
+type DeliveryServiceIDs struct {
+	ID    int    `json:"id,omitempty"`
+	XmlId string `json:"xmlId,omitempty"`
+}
+
+type DeliveryServiceIDsNullable struct {
+	ID    *int    `json:"id,omitempty" db:"ds_id"`
+	XmlId *string `json:"xmlId,omitempty" db:"xml_id"`
+}
diff --git a/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go b/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go
new file mode 100644
index 0000000..75b6494
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go
@@ -0,0 +1,295 @@
+package cdnfederation
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
+	"github.com/apache/trafficcontrol/lib/go-tc/v13"
+	"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/go-ozzo/ozzo-validation"
+	"github.com/lib/pq"
+)
+
+//we need a type alias to define functions on
+type TOCDNFederation struct {
+	ReqInfo *api.APIInfo `json:"-"`
+	v13.CDNFederationNullable
+}
+
+//Used for all CRUD routes
+func GetTypeSingleton() api.CRUDFactory {
+	return func(reqInfo *api.APIInfo) api.CRUDer {
+		toReturn := TOCDNFederation{reqInfo, v13.CDNFederationNullable{}}
+		return &toReturn
+	}
+}
+
+//Fufills `Identifier' interface
+func (fed TOCDNFederation) GetKeyFieldsInfo() []api.KeyFieldInfo {
+	return []api.KeyFieldInfo{{Field: "id", Func: api.GetIntKey}}
+}
+
+//Fufills `Identifier' interface
+func (fed TOCDNFederation) GetKeys() (map[string]interface{}, bool) {
+	if fed.ID == nil {
+		return map[string]interface{}{"id": 0}, false
+	}
+	return map[string]interface{}{"id": *fed.ID}, true
+}
+
+//Fufills `Identifier' interface
+func (fed TOCDNFederation) GetAuditName() string {
+	if fed.CName != nil {
+		return *fed.CName
+	}
+	if fed.ID != nil {
+		return strconv.Itoa(*fed.ID)
+	}
+	return "unknown"
+}
+
+//Fufills `Identifier' interface
+func (fed TOCDNFederation) GetType() string {
+	return "cdnfederation"
+}
+
+//Fufills `Create' interface
+func (fed *TOCDNFederation) SetKeys(keys map[string]interface{}) {
+	i, _ := keys["id"].(int) //non-panicking type assertion
+	fed.ID = &i
+}
+
+//Fulfills `Validate' interface
+func (fed *TOCDNFederation) Validate() error {
+
+	noSpaces := validation.NewStringRule(tovalidate.NoSpaces, "cannot contain spaces")
+	endsWithDot := validation.NewStringRule(
+		func(str string) bool {
+			return strings.HasSuffix(str, ".")
+		}, "must end with a period")
+
+	//cname regex: (^\S*\.$), ttl regex: (^\d+$)
+	validateErrs := validation.Errors{
+		"cname": validation.Validate(fed.CName, validation.Required, endsWithDot, noSpaces),
+		"ttl":   validation.Validate(fed.Ttl, validation.Required),
+	}
+	return util.JoinErrs(tovalidate.ToErrors(validateErrs))
+}
+
+//This separates out errors depending on whether or not some constraint prevented
+//the operation from occuring.
+func parseQueryError(parseErr error, method string) (error, tc.ApiErrorType) {
+	if pqErr, ok := parseErr.(*pq.Error); ok {
+		err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
+		if eType == tc.DataConflictError {
+			return errors.New("a federation with " + err.Error()), eType
+		}
+		return err, eType
+	} else {
+		log.Errorf("received error: %++v from %s execution", parseErr, method)
+		return tc.DBError, tc.SystemError
+	}
+}
+
+//fed.ReqInfo.Params["name"] is not used on creation, rather the cdn name
+//is connected when the federations/:id/deliveryservice links a federation
+//Note: cdns and deliveryservies have a 1-1 relationship
+func (fed *TOCDNFederation) Create() (error, tc.ApiErrorType) {
+
+	if fed.ReqInfo.User.PrivLevel < auth.PrivLevelAdmin {
+		return errors.New("Forbidden"), tc.ForbiddenError
+	}
+
+	// Boilerplate code below
+	resultRows, err := fed.ReqInfo.Tx.NamedQuery(insertQuery(), fed)
+	if err != nil {
+		return parseQueryError(err, "create")
+	}
+	defer resultRows.Close()
+
+	var id int
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err = resultRows.Scan(&id); 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 federation 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 fed insert")
+		log.Errorln(err)
+		return tc.DBError, tc.SystemError
+	}
+	fed.SetKeys(map[string]interface{}{"id": id})
+
+	return nil, tc.NoError
+}
+
+//Concerning efficiency, maybe it would be better to conditionally run different queries?
+//This way the handler wouldn't be doing a full outer join for every query.
+func (fed *TOCDNFederation) Read(parameters map[string]string) ([]interface{}, []error, tc.ApiErrorType) {
+
+	_, ok_id := parameters["id"]
+	queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
+		//db tag                                         symbol from query
+		"id":          dbhelpers.WhereColumnInfo{Column: "federation.id", Checker: api.IsInt},
+		"cname":       dbhelpers.WhereColumnInfo{Column: "cname", Checker: nil},
+		"ttl":         dbhelpers.WhereColumnInfo{Column: "ttl", Checker: api.IsInt},
+		"description": dbhelpers.WhereColumnInfo{Column: "description", Checker: nil},
+		"xmlId":       dbhelpers.WhereColumnInfo{Column: "xml_id", Checker: nil},
+		"ds_id":       dbhelpers.WhereColumnInfo{Column: "deliveryservice.id", Checker: api.IsInt},
+
+		//"name":     dbhelpers.WhereColumnInfo{Column: "cdn.name", Checker: nil}, //would narrow scope of search
+		"cdn_name": dbhelpers.WhereColumnInfo{Column: "cdn.name", Checker: nil}, //used to verify cdn name exists
+	}
+
+	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 := fed.ReqInfo.Tx.NamedQuery(query, queryValues)
+	if err != nil {
+		log.Errorf("Error querying federations: %v", err)
+		return nil, []error{tc.DBError}, tc.SystemError
+	}
+	defer rows.Close()
+
+	cdnFound := false
+	federations := []interface{}{}
+	for rows.Next() {
+		var fed v13.CDNFederationNullable
+		if err = rows.StructScan(&fed); err != nil {
+			log.Errorf("error parsing federation rows: %v", err)
+			return nil, []error{tc.DBError}, tc.SystemError
+		}
+
+		//if we have an id, the cdn name is fooable
+		if ok_id || fed.Name != nil && *fed.Name == parameters["name"] {
+			cdnFound = true
+
+			//if we are getting by id, there may not be an attached deliveryservice
+			//DeliveryServiceIDsNullable will not be nil itself, due to the struct scan
+			if ok_id && fed.DeliveryServiceIDsNullable.ID == nil {
+				fed.DeliveryServiceIDsNullable = nil
+			}
+
+			//Never return cnd and deliveryService only information. Just federation data.
+			if fed.ID != nil {
+				federations = append(federations, fed)
+			}
+		}
+	}
+
+	if !cdnFound {
+		return nil, []error{errors.New("Resource not found.")}, tc.DataMissingError
+	}
+
+	return federations, []error{}, tc.NoError
+}
+
+func (fed *TOCDNFederation) Update() (error, tc.ApiErrorType) {
+
+	if fed.ReqInfo.User.PrivLevel < auth.PrivLevelAdmin {
+		return errors.New("Forbidden"), tc.ForbiddenError
+	}
+
+	result, err := fed.ReqInfo.Tx.NamedExec(updateQuery(), fed)
+	if err != nil {
+		return parseQueryError(err, "update")
+	}
+
+	rowsAffected, err := result.RowsAffected()
+	if err != nil {
+		return tc.DBError, tc.SystemError
+	}
+
+	if rowsAffected != 1 {
+		if rowsAffected < 1 {
+			return errors.New("no federation found with this id"), tc.DataMissingError
+		} else {
+			return fmt.Errorf("this update affected too many rows: %d", rowsAffected), tc.SystemError
+		}
+	}
+	return nil, tc.NoError
+}
+
+//In the perl version, :name is ignored. It is not even verified whether or not
+//:name is a real cdn that exists. This mimicks the perl behavior.
+func (fed *TOCDNFederation) Delete() (error, tc.ApiErrorType) {
+
+	if fed.ReqInfo.User.PrivLevel < auth.PrivLevelAdmin {
+		return errors.New("Forbidden"), tc.ForbiddenError
+	}
+
+	log.Debugf("about to run exec query: %s with federation: %++v", deleteQuery(), fed)
+	result, err := fed.ReqInfo.Tx.NamedExec(deleteQuery(), fed)
+	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 federation with that id found"), tc.DataMissingError
+		} else {
+			return fmt.Errorf("this delete affected too many rows: %d", rowsAffected), tc.SystemError
+		}
+	}
+	return nil, tc.NoError
+}
+
+//Full outer joins are used so that we can get a federation without an attached deliveryservice (fetching by id)
+//and so that we can retrieve all cdn names to determine if the requested cdn exists.
+func selectQuery() string {
+	return `
+  SELECT federation.id as id, cname, ttl, description, deliveryservice.id as ds_id, xml_id, cdn.name as cdn_name FROM federation
+  FULL OUTER JOIN federation_deliveryservice as fd ON federation.id = fd.federation
+  FULL OUTER JOIN deliveryservice ON deliveryservice.id = fd.deliveryservice
+  FULL OUTER JOIN cdn ON cdn.id = cdn_id`
+}
+
+func updateQuery() string {
+	return `
+  UPDATE federation SET
+  cname=:cname
+  ttl=:ttl
+  description=:description
+  WHERE id=:id`
+}
+
+func insertQuery() string {
+	return `
+  INSERT INTO federation (
+    cname,
+    ttl,
+    description
+  ) VALUES (
+    :cname,
+    :ttl,
+    :description
+  ) RETURNING id`
+}
+
+func deleteQuery() string {
+	return `DELETE FROM federation WHERE id = :id`
+}
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index 2f12491..a177b8e 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -40,6 +40,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cachegroup"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cdn"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cdnfederation"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/coordinate"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/crconfig"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice"
@@ -291,6 +292,14 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{1.3, http.MethodPut, `deliveryservices/{xmlID}/urisignkeys$`, saveDeliveryServiceURIKeysHandler, auth.PrivLevelAdmin, Authenticated, nil},
 		{1.3, http.MethodDelete, `deliveryservices/{xmlID}/urisignkeys$`, removeDeliveryServiceURIKeysHandler, auth.PrivLevelAdmin, Authenticated, nil},
 
+		// Federations by CDN (the actual table for `federation')
+		//Should this be 1.2 or 1.1?
+		{1.2, http.MethodGet, `cdns/{name}/federations/?(\.json)?$`, api.ReadHandler(cdnfederation.GetTypeSingleton()), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.2, http.MethodGet, `cdns/{name}/federations/{id}$`, api.ReadHandler(cdnfederation.GetTypeSingleton()), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.2, http.MethodPost, `cdns/{name}/federations/?(\.json)?$`, api.CreateHandler(cdnfederation.GetTypeSingleton()), auth.PrivLevelOperations, Authenticated, nil},
+		{1.2, http.MethodPut, `cdns/{name}/federations/?(\.json)?$`, api.UpdateHandler(cdnfederation.GetTypeSingleton()), auth.PrivLevelOperations, Authenticated, nil},
+		{1.2, http.MethodDelete, `cdns/{name}/federations/{id}$`, api.DeleteHandler(cdnfederation.GetTypeSingleton()), auth.PrivLevelOperations, Authenticated, nil},
+
 		//Origins
 		{1.3, http.MethodGet, `origins/?(\.json)?$`, api.ReadHandler(origin.GetTypeSingleton()), auth.PrivLevelReadOnly, Authenticated, nil},
 		{1.3, http.MethodGet, `origins/?$`, api.ReadHandler(origin.GetTypeSingleton()), auth.PrivLevelReadOnly, Authenticated, nil},