You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by de...@apache.org on 2018/05/24 15:31:23 UTC
[incubator-trafficcontrol] 01/08: Add an Origin API
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 e7ce8bec616256b7f1446ae099c41a7303381501
Author: Rawlin Peters <ra...@comcast.com>
AuthorDate: Fri Apr 13 15:25:54 2018 -0600
Add an Origin API
As part of the effort to remove Origin servers out of the Server table
into their own Origin table, create an Origin API. An Origin is
associated to a Delivery Service, and the Delivery Service API backend will
eventually make use of the Origin table for the 'orgServerFqdn' field.
---
lib/go-tc/v13/origins.go | 75 ++++
.../db/migrations/20180423000000_add_origin.sql | 49 +++
traffic_ops/traffic_ops_golang/origin/origins.go | 432 +++++++++++++++++++++
.../traffic_ops_golang/origin/origins_test.go | 320 +++++++++++++++
traffic_ops/traffic_ops_golang/routes.go | 8 +
5 files changed, 884 insertions(+)
diff --git a/lib/go-tc/v13/origins.go b/lib/go-tc/v13/origins.go
new file mode 100644
index 0000000..e84b44c
--- /dev/null
+++ b/lib/go-tc/v13/origins.go
@@ -0,0 +1,75 @@
+package v13
+
+import (
+ "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.
+ */
+
+type OriginsResponse struct {
+ Response []Origin `json:"response"`
+}
+
+// OriginDetailResponse is the JSON object returned for a single origin
+type OriginsDetailResponse struct {
+ Response Origin `json:"response"`
+}
+
+type Origin struct {
+ Cachegroup string `json:"cachegroup" db:"cachegroup"`
+ CachegroupID int `json:"cachegroupId" db:"cachegroup_id"`
+ Coordinate string `json:"coordinate" db:"coordinate"`
+ CoordinateID int `json:"coordinateId" db:"coordinate_id"`
+ DeliveryService string `json:"deliveryService" db:"deliveryservice"`
+ DeliveryServiceID int `json:"deliveryServiceId" db:"deliveryservice_id"`
+ FQDN string `json:"fqdn" db:"fqdn"`
+ ID int `json:"id" db:"id"`
+ IP6Address string `json:"ip6Address" db:"ip6_address"`
+ IPAddress string `json:"ipAddress" db:"ip_address"`
+ LastUpdated tc.TimeNoMod `json:"lastUpdated" db:"last_updated"`
+ Name string `json:"name" db:"name"`
+ Port int `json:"port" db:"port"`
+ Profile string `json:"profile" db:"profile"`
+ ProfileID int `json:"profileId" db:"profile_id"`
+ Protocol string `json:"protocol" db:"protocol"`
+ Tenant string `json:"tenant" db:"tenant"`
+ TenantID int `json:"tenantId" db:"tenant_id"`
+}
+
+type OriginNullable struct {
+ Cachegroup *string `json:"cachegroup" db:"cachegroup"`
+ CachegroupID *int `json:"cachegroupId" db:"cachegroup_id"`
+ Coordinate *string `json:"coordinate" db:"coordinate"`
+ CoordinateID *int `json:"coordinateId" db:"coordinate_id"`
+ DeliveryService *string `json:"deliveryService" db:"deliveryservice"`
+ DeliveryServiceID *int `json:"deliveryServiceId" db:"deliveryservice_id"`
+ FQDN *string `json:"fqdn" db:"fqdn"`
+ ID *int `json:"id" db:"id"`
+ IP6Address *string `json:"ip6Address" db:"ip6_address"`
+ IPAddress *string `json:"ipAddress" db:"ip_address"`
+ LastUpdated *tc.TimeNoMod `json:"lastUpdated" db:"last_updated"`
+ Name *string `json:"name" db:"name"`
+ Port *int `json:"port" db:"port"`
+ Profile *string `json:"profile" db:"profile"`
+ ProfileID *int `json:"profileId" db:"profile_id"`
+ Protocol *string `json:"protocol" db:"protocol"`
+ Tenant *string `json:"tenant" db:"tenant"`
+ TenantID *int `json:"tenantId" db:"tenant_id"`
+}
diff --git a/traffic_ops/app/db/migrations/20180423000000_add_origin.sql b/traffic_ops/app/db/migrations/20180423000000_add_origin.sql
new file mode 100644
index 0000000..293205e
--- /dev/null
+++ b/traffic_ops/app/db/migrations/20180423000000_add_origin.sql
@@ -0,0 +1,49 @@
+/*
+
+ 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 TYPE origin_protocol AS ENUM ('http', 'https');
+
+CREATE TABLE origin (
+ id bigserial PRIMARY KEY NOT NULL,
+ name text UNIQUE NOT NULL,
+ fqdn text NOT NULL,
+ protocol origin_protocol NOT NULL DEFAULT 'http',
+ port bigint,
+ ip_address text,
+ ip6_address text,
+ deliveryservice bigint REFERENCES deliveryservice(id) ON DELETE SET NULL,
+ coordinate bigint REFERENCES coordinate(id) ON DELETE SET NULL,
+ profile bigint REFERENCES profile(id) ON DELETE SET NULL,
+ cachegroup bigint REFERENCES cachegroup(id) ON DELETE SET NULL,
+ tenant bigint REFERENCES tenant(id) ON DELETE SET NULL,
+ last_updated timestamp WITH time zone NOT NULL DEFAULT now()
+);
+
+CREATE INDEX origin_deliveryservice_fkey ON origin USING btree (deliveryservice);
+CREATE INDEX origin_coordinate_fkey ON origin USING btree (coordinate);
+CREATE INDEX origin_profile_fkey ON origin USING btree (profile);
+CREATE INDEX origin_cachegroup_fkey ON origin USING btree (cachegroup);
+CREATE INDEX origin_tenant_fkey ON origin USING btree (tenant);
+
+CREATE TRIGGER on_update_current_timestamp BEFORE UPDATE ON origin 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 IF EXISTS origin;
+
+DROP TYPE IF EXISTS origin_protocol;
diff --git a/traffic_ops/traffic_ops_golang/origin/origins.go b/traffic_ops/traffic_ops_golang/origin/origins.go
new file mode 100644
index 0000000..d589913
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/origin/origins.go
@@ -0,0 +1,432 @@
+package origin
+
+/*
+ * 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"
+ "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/go-ozzo/ozzo-validation/is"
+ "github.com/jmoiron/sqlx"
+ "github.com/lib/pq"
+)
+
+//we need a type alias to define functions on
+type TOOrigin v13.OriginNullable
+
+//the refType is passed into the handlers where a copy of its type is used to decode the json.
+var refType = TOOrigin{}
+
+func GetRefType() *TOOrigin {
+ return &refType
+}
+
+func (origin *TOOrigin) SetID(i int) {
+ origin.ID = &i
+}
+
+func (origin TOOrigin) GetKeyFieldsInfo() []api.KeyFieldInfo {
+ return []api.KeyFieldInfo{{"id", api.GetIntKey}}
+}
+
+//Implementation of the Identifier, Validator interface functions
+func (origin TOOrigin) GetKeys() (map[string]interface{}, bool) {
+ if origin.ID == nil {
+ return map[string]interface{}{"id": 0}, false
+ }
+ return map[string]interface{}{"id": *origin.ID}, true
+}
+
+func (origin *TOOrigin) 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.
+ origin.ID = &i
+}
+
+func (origin *TOOrigin) GetAuditName() string {
+ if origin.Name != nil {
+ return *origin.Name
+ }
+ if origin.ID != nil {
+ return strconv.Itoa(*origin.ID)
+ }
+ return "unknown"
+}
+
+func (origin *TOOrigin) GetType() string {
+ return "origin"
+}
+
+func (origin *TOOrigin) Validate(db *sqlx.DB) []error {
+
+ noSpaces := validation.NewStringRule(tovalidate.NoSpaces, "cannot contain spaces")
+ validProtocol := validation.NewStringRule(tovalidate.IsOneOfStringICase("http", "https"), "must be http or https")
+ portErr := "must be a valid integer between 1 and 65535"
+
+ validateErrs := validation.Errors{
+ "cachegroupId": validation.Validate(origin.CachegroupID, validation.Min(1)),
+ "coordinateId": validation.Validate(origin.CoordinateID, validation.Min(1)),
+ "deliveryServiceId": validation.Validate(origin.DeliveryServiceID, validation.Min(1)),
+ "fqdn": validation.Validate(origin.FQDN, validation.Required, is.DNSName),
+ "ip6Address": validation.Validate(origin.IP6Address, validation.NilOrNotEmpty, is.IPv6),
+ "ipAddress": validation.Validate(origin.IPAddress, validation.NilOrNotEmpty, is.IPv4),
+ "name": validation.Validate(origin.Name, validation.Required, noSpaces),
+ "port": validation.Validate(origin.Port, validation.NilOrNotEmpty.Error(portErr), validation.Min(1).Error(portErr), validation.Max(65535).Error(portErr)),
+ "profileId": validation.Validate(origin.ProfileID, validation.Min(1)),
+ "protocol": validation.Validate(origin.Protocol, validation.Required, validProtocol),
+ "tenantId": validation.Validate(origin.TenantID, validation.Min(1)),
+ }
+ return tovalidate.ToErrors(validateErrs)
+}
+
+func (origin *TOOrigin) Read(db *sqlx.DB, params map[string]string, user auth.CurrentUser) ([]interface{}, []error, tc.ApiErrorType) {
+ returnable := []interface{}{}
+
+ privLevel := user.PrivLevel
+
+ origins, errs, errType := getOrigins(params, db, privLevel)
+ if len(errs) > 0 {
+ return nil, errs, errType
+ }
+
+ for _, origin := range origins {
+ returnable = append(returnable, origin)
+ }
+
+ return returnable, nil, tc.NoError
+}
+
+func getOrigins(params map[string]string, db *sqlx.DB, privLevel int) ([]v13.OriginNullable, []error, tc.ApiErrorType) {
+ var rows *sqlx.Rows
+ var err error
+
+ // Query Parameters to Database Query column mappings
+ // see the fields mapped in the SQL query
+ queryParamsToSQLCols := map[string]dbhelpers.WhereColumnInfo{
+ "cachegroup": dbhelpers.WhereColumnInfo{"o.cachegroup", api.IsInt},
+ "coordinate": dbhelpers.WhereColumnInfo{"o.coordinate", api.IsInt},
+ "deliveryservice": dbhelpers.WhereColumnInfo{"o.deliveryservice", api.IsInt},
+ "id": dbhelpers.WhereColumnInfo{"o.id", api.IsInt},
+ "name": dbhelpers.WhereColumnInfo{"o.name", nil},
+ "profileId": dbhelpers.WhereColumnInfo{"o.profile", api.IsInt},
+ "tenant": dbhelpers.WhereColumnInfo{"o.tenant", api.IsInt},
+ }
+
+ where, orderBy, queryValues, errs := dbhelpers.BuildWhereAndOrderBy(params, queryParamsToSQLCols)
+ 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 {
+ return nil, []error{fmt.Errorf("querying: %v", err)}, tc.SystemError
+ }
+ defer rows.Close()
+
+ origins := []v13.OriginNullable{}
+
+ for rows.Next() {
+ var s v13.OriginNullable
+ if err = rows.StructScan(&s); err != nil {
+ return nil, []error{fmt.Errorf("getting origins: %v", err)}, tc.SystemError
+ }
+ origins = append(origins, s)
+ }
+ return origins, nil, tc.NoError
+}
+
+func selectQuery() string {
+
+ selectStmt := `SELECT
+cg.name as cachegroup,
+o.cachegroup as cachegroup_id,
+o.coordinate as coordinate_id,
+c.name as coordinate,
+d.xml_id as deliveryservice,
+o.deliveryservice as deliveryservice_id,
+o.fqdn,
+o.id,
+o.ip6_address,
+o.ip_address,
+o.last_updated,
+o.name,
+o.port,
+p.name as profile,
+o.profile as profile_id,
+o.protocol as protocol,
+t.name as tenant,
+o.tenant as tenant_id
+
+FROM origin o
+
+LEFT JOIN cachegroup cg ON o.cachegroup = cg.id
+LEFT JOIN deliveryservice d ON o.deliveryservice = d.id
+LEFT JOIN coordinate c ON o.coordinate = c.id
+LEFT JOIN profile p ON o.profile = p.id
+LEFT JOIN tenant t ON o.tenant = t.id`
+
+ return selectStmt
+}
+
+//The TOOrigin implementation of the Updater interface
+//all implementations of Updater should use transactions and return the proper errorType
+//ParsePQUniqueConstraintError is used to determine if an origin 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 (origin *TOOrigin) 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 origin: %++v", updateQuery(), origin)
+ resultRows, err := tx.NamedQuery(updateQuery(), origin)
+ if err != nil {
+ if pqErr, ok := err.(*pq.Error); ok {
+ err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
+ if eType == tc.DataConflictError {
+ return errors.New("an origin 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
+ }
+ }
+
+ if rowsAffected == 0 {
+ err = errors.New("no origin 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 origin insert")
+ log.Errorln(err)
+ return tc.DBError, tc.SystemError
+ }
+ origin.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 updateQuery() string {
+ query := `UPDATE
+origin SET
+cachegroup=:cachegroup_id,
+coordinate=:coordinate_id,
+deliveryservice=:deliveryservice_id,
+fqdn=:fqdn,
+ip6_address=:ip6_address,
+ip_address=:ip_address,
+name=:name,
+port=:port,
+profile=:profile_id,
+protocol=:protocol,
+tenant=:tenant_id
+WHERE id=:id RETURNING last_updated`
+ return query
+}
+
+//The TOOrigin implementation of the Inserter interface
+//all implementations of Inserter should use transactions and return the proper errorType
+//ParsePQUniqueConstraintError is used to determine if an origin 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 origin and have
+//to be added to the struct
+func (origin *TOOrigin) 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(), origin)
+ if err != nil {
+ if pqErr, ok := err.(*pq.Error); ok {
+ err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
+ if eType == tc.DataConflictError {
+ return errors.New("an origin 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 origin 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 origin insert")
+ log.Errorln(err)
+ return tc.DBError, tc.SystemError
+ }
+ origin.SetKeys(map[string]interface{}{"id": id})
+ origin.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 origin (
+cachegroup,
+coordinate,
+deliveryservice,
+fqdn,
+ip6_address,
+ip_address,
+name,
+port,
+profile,
+protocol,
+tenant) VALUES (
+:cachegroup_id,
+:coordinate_id,
+:deliveryservice_id,
+:fqdn,
+:ip6_address,
+:ip_address,
+:name,
+:port,
+:profile_id,
+:protocol,
+:tenant_id) RETURNING id,last_updated`
+ return query
+}
+
+//The Origin implementation of the Deleter interface
+//all implementations of Deleter should use transactions and return the proper errorType
+func (origin *TOOrigin) 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 origin: %++v", deleteQuery(), origin)
+ result, err := tx.NamedExec(deleteQuery(), origin)
+ 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 origin 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 deleteQuery() string {
+ query := `DELETE FROM origin
+WHERE id=:id`
+ return query
+}
diff --git a/traffic_ops/traffic_ops_golang/origin/origins_test.go b/traffic_ops/traffic_ops_golang/origin/origins_test.go
new file mode 100644
index 0000000..2d6b1de
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/origin/origins_test.go
@@ -0,0 +1,320 @@
+package origin
+
+/*
+ * 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-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/test"
+
+ "github.com/jmoiron/sqlx"
+ "gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func getTestOrigins() []v13.Origin {
+ origins := []v13.Origin{}
+ testOrigin := v13.Origin{
+ Cachegroup: "Cachegroup",
+ CachegroupID: 1,
+ Coordinate: "originCoordinate",
+ CoordinateID: 1,
+ DeliveryService: "testDS",
+ DeliveryServiceID: 1,
+ FQDN: "origin.cdn.net",
+ ID: 1,
+ IP6Address: "dead:beef:cafe::42",
+ IPAddress: "10.2.3.4",
+ LastUpdated: tc.TimeNoMod{Time: time.Now()},
+ Name: "originName",
+ Port: 443,
+ Profile: "profile",
+ ProfileID: 1,
+ Protocol: "https",
+ Tenant: "tenantName",
+ TenantID: 1,
+ }
+ origins = append(origins, testOrigin)
+
+ testOrigin2 := testOrigin
+ testOrigin2.FQDN = "origin2.cdn.com"
+ testOrigin2.Name = "origin2"
+ origins = append(origins, testOrigin2)
+
+ testOrigin3 := testOrigin
+ testOrigin3.FQDN = "origin3.cdn.org"
+ testOrigin3.Name = "origin3"
+ origins = append(origins, testOrigin3)
+
+ return origins
+}
+
+func TestReadOrigins(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()
+
+ testOrigins := getTestOrigins()
+ cols := test.ColsFromStructByTag("db", v13.Origin{})
+ rows := sqlmock.NewRows(cols)
+
+ for _, to := range testOrigins {
+ rows = rows.AddRow(
+ to.Cachegroup,
+ to.CachegroupID,
+ to.Coordinate,
+ to.CoordinateID,
+ to.DeliveryService,
+ to.DeliveryServiceID,
+ to.FQDN,
+ to.ID,
+ to.IP6Address,
+ to.IPAddress,
+ to.LastUpdated,
+ to.Name,
+ to.Port,
+ to.Profile,
+ to.ProfileID,
+ to.Protocol,
+ to.Tenant,
+ to.TenantID,
+ )
+ }
+ mock.ExpectQuery("SELECT").WillReturnRows(rows)
+ v := map[string]string{}
+
+ origins, errs, errType := getOrigins(v, db, auth.PrivLevelAdmin)
+ log.Debugln("%v-->", origins)
+ if len(errs) > 0 {
+ t.Errorf("getOrigins expected: no errors, actual: %v with error type: %s", errs, errType.String())
+ }
+
+ if len(origins) != 3 {
+ t.Errorf("getOrigins expected: len(origins) == 3, actual: %v", len(origins))
+ }
+
+}
+
+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 = &TOOrigin{}
+
+ if _, ok := i.(api.Creator); !ok {
+ t.Errorf("origin must be creator")
+ }
+ if _, ok := i.(api.Reader); !ok {
+ t.Errorf("origin must be reader")
+ }
+ if _, ok := i.(api.Updater); !ok {
+ t.Errorf("origin must be updater")
+ }
+ if _, ok := i.(api.Deleter); !ok {
+ t.Errorf("origin must be deleter")
+ }
+ if _, ok := i.(api.Identifier); !ok {
+ t.Errorf("origin must be Identifier")
+ }
+}
+
+func TestValidate(t *testing.T) {
+ const portErr = `'port' must be a valid integer between 1 and 65535`
+ const protoErr = `'protocol' must be http or https`
+ const fqdnErr = `'fqdn' must be a valid DNS name`
+ const ipErr = `'ipAddress' must be a valid IPv4 address`
+ const ip6Err = `'ip6Address' must be a valid IPv6 address`
+
+ // verify that non-null fields are invalid
+ c := TOOrigin{ID: nil,
+ Name: nil,
+ FQDN: nil,
+ Protocol: nil,
+ }
+ errs := test.SortErrors(c.Validate(nil))
+
+ expectedErrs := []error{
+ errors.New(`'fqdn' cannot be blank`),
+ errors.New(`'name' cannot be blank`),
+ errors.New(`'protocol' cannot be blank`),
+ }
+
+ if !reflect.DeepEqual(expectedErrs, errs) {
+ t.Errorf("expected %s, got %s", expectedErrs, errs)
+ }
+
+ // all valid fields
+ id := 1
+ nm := "validname"
+ fqdn := "is.a.valid.hostname"
+ ip6 := "dead:beef::42"
+ ip := "1.2.3.4"
+ port := 65535
+ pro := "http"
+ lu := tc.TimeNoMod{Time: time.Now()}
+ c = TOOrigin{ID: &id,
+ Name: &nm,
+ FQDN: &fqdn,
+ IP6Address: &ip6,
+ IPAddress: &ip,
+ Port: &port,
+ Protocol: &pro,
+ LastUpdated: &lu,
+ }
+ expectedErrs = []error{}
+ errs = c.Validate(nil)
+ if !reflect.DeepEqual(expectedErrs, errs) {
+ t.Errorf("expected %s, got %s", expectedErrs, errs)
+ }
+
+ type testCase struct {
+ Int int
+ Str string
+ ExpectedErrors []error
+ }
+
+ type typedTestCases struct {
+ Type string
+ TestCases []testCase
+ }
+
+ nameTestCases := typedTestCases{
+ "name",
+ []testCase{
+ {Str: "", ExpectedErrors: []error{errors.New(`'name' cannot be blank`)}},
+ {Str: "invalid name", ExpectedErrors: []error{errors.New(`'name' cannot contain spaces`)}},
+ {Str: "valid-name", ExpectedErrors: []error{}},
+ },
+ }
+
+ portTestCases := typedTestCases{
+ "port",
+ []testCase{
+ {Int: -1, ExpectedErrors: []error{errors.New(portErr)}},
+ {Int: 0, ExpectedErrors: []error{errors.New(portErr)}},
+ {Int: 1, ExpectedErrors: []error{}},
+ },
+ }
+
+ protoTestCases := typedTestCases{
+ "protocol",
+ []testCase{
+ {Str: "foo", ExpectedErrors: []error{errors.New(protoErr)}},
+ {Str: "", ExpectedErrors: []error{errors.New(`'protocol' cannot be blank`)}},
+ {Str: "http", ExpectedErrors: []error{}},
+ {Str: "https", ExpectedErrors: []error{}},
+ },
+ }
+
+ fqdnTestCases := typedTestCases{
+ "fqdn",
+ []testCase{
+ {Str: "not.@.v@lid.#()stn@me", ExpectedErrors: []error{errors.New(fqdnErr)}},
+ {Str: "dead:beef::42", ExpectedErrors: []error{errors.New(fqdnErr)}},
+ {Str: "valid.hostname.net", ExpectedErrors: []error{}},
+ },
+ }
+
+ ipTestCases := typedTestCases{
+ "ip",
+ []testCase{
+ {Str: "not.@.v@lid.#()stn@me", ExpectedErrors: []error{errors.New(ipErr)}},
+ {Str: "dead:beef::42", ExpectedErrors: []error{errors.New(ipErr)}},
+ {Str: "1.2.3", ExpectedErrors: []error{errors.New(ipErr)}},
+ {Str: "", ExpectedErrors: []error{errors.New(`'ipAddress' cannot be blank`)}},
+ {Str: "1.2.3.4", ExpectedErrors: []error{}},
+ },
+ }
+
+ ip6TestCases := typedTestCases{
+ "ip6",
+ []testCase{
+ {Str: "not.@.v@lid.#()stn@me", ExpectedErrors: []error{errors.New(ip6Err)}},
+ {Str: "1.2.3.4", ExpectedErrors: []error{errors.New(ip6Err)}},
+ {Str: "beef", ExpectedErrors: []error{errors.New(ip6Err)}},
+ {Str: "", ExpectedErrors: []error{errors.New(`'ip6Address' cannot be blank`)}},
+ {Str: "dead:beef::42", ExpectedErrors: []error{}},
+ },
+ }
+
+ for _, ttc := range []typedTestCases{
+ nameTestCases,
+ portTestCases,
+ protoTestCases,
+ fqdnTestCases,
+ ipTestCases,
+ ip6TestCases,
+ } {
+ for _, tc := range ttc.TestCases {
+ var value interface{}
+ switch ttc.Type {
+ case "name":
+ c.Name = &tc.Str
+ value = tc.Str
+ case "port":
+ c.Port = &tc.Int
+ value = tc.Int
+ case "protocol":
+ c.Protocol = &tc.Str
+ value = tc.Str
+ case "fqdn":
+ c.FQDN = &tc.Str
+ value = tc.Str
+ case "ip":
+ c.IPAddress = &tc.Str
+ value = tc.Str
+ case "ip6":
+ c.IP6Address = &tc.Str
+ value = tc.Str
+ }
+ errs = test.SortErrors(c.Validate(nil))
+ if !reflect.DeepEqual(tc.ExpectedErrors, errs) {
+ t.Errorf("given: '%v', expected %s, got %s", value, tc.ExpectedErrors, errs)
+ }
+ }
+ }
+
+}
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index d8a76c8..9a9f43a 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -56,6 +56,7 @@ import (
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/systeminfo"
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/types"
+ "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/origin"
"github.com/basho/riak-go-client"
)
@@ -224,6 +225,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},
+ //Origins
+ {1.3, http.MethodGet, `origins/?(\.json)?$`, api.ReadHandler(origin.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+ {1.3, http.MethodGet, `origins/?$`, api.ReadHandler(origin.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+ {1.3, http.MethodPut, `origins/?$`, api.UpdateHandler(origin.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil},
+ {1.3, http.MethodPost, `origins/?$`, api.CreateHandler(origin.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil},
+ {1.3, http.MethodDelete, `origins/?$`, api.DeleteHandler(origin.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil},
+
//Roles
{1.3, http.MethodGet, `roles/?(\.json)?$`, api.ReadHandler(role.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
{1.3, http.MethodPut, `roles/?$`, api.UpdateHandler(role.GetRefType(), d.DB), auth.PrivLevelAdmin, Authenticated, nil},
--
To stop receiving notification emails like this one, please contact
dewrich@apache.org.