You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@trafficcontrol.apache.org by GitBox <gi...@apache.org> on 2018/06/26 18:54:50 UTC

[GitHub] dewrich closed pull request #2088: TO Go: implement tenants CRUD

dewrich closed pull request #2088: TO Go: implement tenants CRUD
URL: https://github.com/apache/trafficcontrol/pull/2088
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/lib/go-tc/tenants.go b/lib/go-tc/tenants.go
index 232d617c8..e825945a5 100644
--- a/lib/go-tc/tenants.go
+++ b/lib/go-tc/tenants.go
@@ -1,26 +1,30 @@
 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.
-*/
-
-// GetTenantsResponse ...
+ * 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.
+ */
+
+// GetTenantsResponse is the response for a request for a group of tenants
 type GetTenantsResponse struct {
 	Response []Tenant `json:"response"`
 }
 
-// TenantResponse ...
+// TenantResponse is the response to a create/update
 type TenantResponse struct {
 	Response Tenant        `json:"response"`
 	Alerts   []TenantAlert `json:"alerts"`
@@ -28,11 +32,22 @@ type TenantResponse struct {
 
 // Tenant ...
 type Tenant struct {
-	ID         int    `json:"id,omitempty"`
-	Name       string `json:"name,omitempty"`
-	Active     bool   `json:"active,omitempty"`
-	ParentID   int    `json:"parentId"`
-	ParentName string `json:"parentName,omitempty"`
+	Active      bool      `json:"active"`
+	ID          int       `json:"id"`
+	LastUpdated TimeNoMod `json:"lastUpdated" db:"last_updated"`
+	Name        string    `json:"name"`
+	ParentID    int       `json:"parentId"`
+	ParentName  string    `json:"parentName,omitempty" db:"parent_name"`
+}
+
+// TenantNullable ...
+type TenantNullable struct {
+	ID          *int       `json:"id" db:"id"`
+	Name        *string    `json:"name" db:"name"`
+	Active      *bool      `json:"active" db:"active"`
+	LastUpdated *TimeNoMod `json:"lastUpdated" db:"last_updated"`
+	ParentID    *int       `json:"parentId" db:"parent_id"`
+	ParentName  *string    `json:"parentName,omitempty" db:"parent_name"`
 }
 
 // DeleteTenantResponse ...
diff --git a/lib/go-tc/tovalidate/db.go b/lib/go-tc/tovalidate/db.go
new file mode 100644
index 000000000..dd14b9bff
--- /dev/null
+++ b/lib/go-tc/tovalidate/db.go
@@ -0,0 +1,125 @@
+package tovalidate
+
+/*
+ * 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 (
+	"database/sql"
+	"errors"
+	"fmt"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	validation "github.com/go-ozzo/ozzo-validation"
+	"github.com/jmoiron/sqlx"
+)
+
+// DBExistsRule checks if value is in column in this table
+// TODO: DBExistsRule ?   what about DBUniqueRule?
+type DBExistsRule struct {
+	db      *sqlx.DB
+	table   string
+	column  string
+	message string
+}
+
+// NewDBExistsRule is a validation rule that checks if the given value is in the column in this table
+func NewDBExistsRule(db *sqlx.DB, table string, column string) *DBExistsRule {
+	return &DBExistsRule{
+		db:      db,
+		table:   table,
+		column:  column,
+		message: fmt.Sprintf("No rows with value in %s.%s", table, column),
+	}
+}
+
+// Validate checks if the given value is valid or not.
+func (r *DBExistsRule) Validate(value interface{}) error {
+	if r.db == nil {
+		return nil
+	}
+	value, isNil := validation.Indirect(value)
+	if isNil || validation.IsEmpty(value) {
+		return nil
+	}
+
+	query := `SELECT COUNT(*) FROM ` + r.table + ` WHERE ` + r.column + `= $1`
+	row := r.db.QueryRow(query, value)
+	var cnt int
+	err := row.Scan(&cnt)
+	log.Debugln("**** QUERY **** ", query)
+	log.Debugf(" value %d err %++v", cnt, err)
+	if err != nil {
+		return errors.New(r.message)
+	}
+	return nil
+}
+
+// Error sets the error message for the rule.
+func (r *DBExistsRule) Error(message string) *DBExistsRule {
+	r.message = message
+	return r
+}
+
+// DBUniqueRule checks if value is in column in this table
+// TODO: DBUniqueRule ?   what about DBUniqueRule?
+type DBUniqueRule struct {
+	db      *sqlx.DB
+	table   string
+	column  string
+	idCheck func(int) bool
+	message string
+}
+
+// NewDBUniqueRule is a validation rule that checks if the given value is in the column in this table
+func NewDBUniqueRule(db *sqlx.DB, table string, column string, idCheck func(int) bool) *DBUniqueRule {
+	return &DBUniqueRule{
+		db:      db,
+		table:   table,
+		column:  column,
+		idCheck: idCheck,
+		message: column + ` must be unique in ` + table,
+	}
+}
+
+// Validate returns an error if the value already exists in the table in this column
+func (r *DBUniqueRule) Validate(value interface{}) error {
+	if r.db == nil {
+		return nil
+	}
+	value, isNil := validation.Indirect(value)
+	if isNil || validation.IsEmpty(value) {
+		return nil
+	}
+
+	query := `SELECT id FROM ` + r.table
+	row := r.db.QueryRowx(query, map[string]interface{}{r.column: value})
+	var id int
+	err := row.Scan(&id)
+	// ok if no rows found or only one belongs to row being updated
+	if err == sql.ErrNoRows || r.idCheck(id) {
+		return nil
+	}
+	return errors.New(r.message)
+}
+
+// Error sets the error message for the rule.
+func (r *DBUniqueRule) Error(message string) *DBUniqueRule {
+	r.message = message
+	return r
+}
diff --git a/traffic_ops/client/v13/tenant.go b/traffic_ops/client/v13/tenant.go
index 123828eed..ac9bd4c63 100644
--- a/traffic_ops/client/v13/tenant.go
+++ b/traffic_ops/client/v13/tenant.go
@@ -17,6 +17,7 @@ package v13
 
 import (
 	"encoding/json"
+	"net/url"
 
 	tc "github.com/apache/trafficcontrol/lib/go-tc"
 )
@@ -43,6 +44,22 @@ func (to *Session) Tenant(id string) (*tc.Tenant, ReqInf, error) {
 	return &data.Response[0], reqInf, nil
 }
 
+// TenantByName gets the Tenant for the name it's passed
+func (to *Session) TenantByName(name string) (*tc.Tenant, ReqInf, error) {
+	var data tc.GetTenantsResponse
+	query := tenantsEp() + "?name=" + url.QueryEscape(name)
+	reqInf, err := get(to, query, &data)
+	if err != nil {
+		return nil, reqInf, err
+	}
+
+	var ten *tc.Tenant
+	if len(data.Response) > 0 {
+		ten = &data.Response[0]
+	}
+	return ten, reqInf, nil
+}
+
 // CreateTenant creates the Tenant it's passed
 func (to *Session) CreateTenant(t *tc.Tenant) (*tc.TenantResponse, error) {
 	var data tc.TenantResponse
diff --git a/traffic_ops/testing/api/v13/deliveryservice_requests_test.go b/traffic_ops/testing/api/v13/deliveryservice_requests_test.go
index 6f83fb17c..2d0a61c05 100644
--- a/traffic_ops/testing/api/v13/deliveryservice_requests_test.go
+++ b/traffic_ops/testing/api/v13/deliveryservice_requests_test.go
@@ -39,6 +39,7 @@ func TestDeliveryServiceRequests(t *testing.T) {
 	GetTestDeliveryServiceRequests(t)
 	UpdateTestDeliveryServiceRequests(t)
 	DeleteTestDeliveryServiceRequests(t)
+
 	DeleteTestTypes(t)
 	DeleteTestCDNs(t)
 
diff --git a/traffic_ops/testing/api/v13/tc-fixtures.json b/traffic_ops/testing/api/v13/tc-fixtures.json
index 6ce32b163..d71bdd95a 100644
--- a/traffic_ops/testing/api/v13/tc-fixtures.json
+++ b/traffic_ops/testing/api/v13/tc-fixtures.json
@@ -1037,20 +1037,15 @@
         }
     ],
     "tenants": [
-        {
-            "active": true,
-            "name": "root",
-            "parentTenantName": null
-        },
         {
             "active": true,
             "name": "tenant1",
-            "parentTenantName": "root"
+            "parentName": "root"
         },
         {
             "active": false,
             "name": "tenant2",
-            "parentTenantName": "root"
+            "parentName": "root"
         }
     ],
     "types": [
@@ -1246,5 +1241,143 @@
             "name": "TRAFFIC_MONITOR",
             "useInTable": "server"
         }
+    ],
+    "users": [
+        {
+            "addressLine1": "address of admin",
+            "addressLine2": "",
+            "city": "Anywhere",
+            "company": "Comcast",
+            "country": "USA",
+            "email": "admin@example.com",
+            "fullName": "Fred the admin",
+            "gid": 0,
+            "localPasswd": "pa$$word",
+            "newUser": false,
+            "phoneNumber": "810-555-9876",
+            "postalCode": "55443",
+            "publicSshKey": "",
+            "role": 4,
+            "rolename": "admin",
+            "stateOrProvince": "LA",
+            "tenant": "root",
+            "uid": 0,
+            "username": "adminuser"
+        },
+        {
+            "addressLine1": "address of disallowed",
+            "addressLine2": "place",
+            "city": "somewhere",
+            "company": "else",
+            "country": "UK",
+            "email": "disallowed@example.com",
+            "fullName": "Me me",
+            "gid": 0,
+            "localPasswd": "pa$$word",
+            "newUser": false,
+            "phoneNumber": "",
+            "postalCode": "",
+            "publicSshKey": "",
+            "registrationSent": "",
+            "role": 1,
+            "roleName": "disallowed",
+            "stateOrProvince": "",
+            "tenant": "root",
+            "tenant": 3,
+            "uid": 0,
+            "username": "disalloweduser"
+        },
+        {
+            "addressLine1": "address of read-only",
+            "addressLine2": "",
+            "city": "",
+            "company": "",
+            "country": "",
+            "email": "readonly@example.com",
+            "fullName": "",
+            "gid": 0,
+            "localPasswd": "pa$$word",
+            "newUser": false,
+            "phoneNumber": "",
+            "postalCode": "",
+            "publicSshKey": "",
+            "registrationSent": "",
+            "role": 2,
+            "roleName": "read-only user",
+            "stateOrProvince": "",
+            "tenant": "root",
+            "tenant": 3,
+            "uid": 0,
+            "username": "readuser"
+        },
+        {
+            "addressLine1": "address of operations",
+            "addressLine2": "",
+            "city": "",
+            "company": "",
+            "country": "",
+            "email": "operations@example.com",
+            "fullName": "",
+            "gid": 0,
+            "localPasswd": "pa$$word",
+            "newUser": false,
+            "phoneNumber": "",
+            "postalCode": "",
+            "publicSshKey": "",
+            "registrationSent": "",
+            "role": 3,
+            "roleName": "operations",
+            "stateOrProvince": "",
+            "tenant": "root",
+            "tenant": 4,
+            "uid": 0,
+            "username": "operationsuser"
+        },
+        {
+            "addressLine1": "",
+            "addressLine2": "",
+            "city": "",
+            "company": "",
+            "country": "",
+            "email": "portal@example.com",
+            "fullName": "",
+            "gid": 0,
+            "localPasswd": "pa$$word",
+            "newUser": false,
+            "phoneNumber": "",
+            "postalCode": "",
+            "publicSshKey": "",
+            "registrationSent": "",
+            "role": 5,
+            "roleName": "portal",
+            "stateOrProvince": "",
+            "tenant": "root",
+            "tenant": 2,
+            "uid": 0,
+            "username": "portaluser"
+        },
+        {
+            "addressLine1": "",
+            "addressLine2": "",
+            "city": "",
+            "company": "",
+            "country": "",
+            "email": "federation@example.com",
+            "fullName": "",
+            "gid": 0,
+            "localPasswd": "pa$$word",
+            "newUser": false,
+            "phoneNumber": "",
+            "postalCode": "",
+            "publicSshKey": "",
+            "registrationSent": "",
+            "role": 7,
+            "roleName": "federation",
+            "stateOrProvince": "",
+            "tenant": "root",
+            "tenant": 4,
+            "uid": 0,
+            "username": "federationuser"
+        }
     ]
 }
diff --git a/traffic_ops/testing/api/v13/tenants_test.go b/traffic_ops/testing/api/v13/tenants_test.go
new file mode 100644
index 000000000..99bbbf240
--- /dev/null
+++ b/traffic_ops/testing/api/v13/tenants_test.go
@@ -0,0 +1,142 @@
+package v13
+
+/*
+
+   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 (
+	"strconv"
+	"strings"
+	"testing"
+)
+
+func TestTenants(t *testing.T) {
+
+	CreateTestTenants(t)
+	UpdateTestTenants(t)
+	GetTestTenants(t)
+	DeleteTestTenants(t)
+}
+
+func CreateTestTenants(t *testing.T) {
+	for _, ten := range testData.Tenants {
+		// testData does not define ParentID -- look up by name and fill in
+		if ten.ParentID == 0 {
+			parent, _, err := TOSession.TenantByName(ten.ParentName)
+			if err != nil {
+				t.Errorf("parent tenant %s: %++v", ten.ParentName, err)
+				continue
+			}
+			ten.ParentID = parent.ID
+		}
+		resp, err := TOSession.CreateTenant(&ten)
+		t.Logf("response: %++v", resp)
+
+		if err != nil {
+			t.Errorf("could not CREATE tenant %s: %v\n", ten.Name, err)
+		}
+	}
+}
+
+func GetTestTenants(t *testing.T) {
+	resp, _, err := TOSession.Tenants()
+	if err != nil {
+		t.Errorf("cannot GET all tenants: %v - %v\n", err, resp)
+		return
+	}
+
+	// expect root and badTenant (defined in todb.go) + all defined in testData.Tenants
+	if len(resp) != 2+len(testData.Tenants) {
+		t.Errorf("expected %d tenants,  got %d", 2+len(testData.Tenants), len(resp))
+	}
+
+	for _, ten := range testData.Tenants {
+		resp, _, err := TOSession.TenantByName(ten.Name)
+		if err != nil {
+			t.Errorf("cannot GET Tenant by name: %v - %v\n", err, resp)
+			continue
+		}
+		if resp.Name != ten.Name {
+			t.Errorf("expected tenant %s,  got %s", ten.Name, resp.Name)
+			continue
+		}
+	}
+}
+
+func UpdateTestTenants(t *testing.T) {
+
+	// Retrieve the Tenant by name so we can get the id for the Update
+	name := "tenant2"
+	parentName := "tenant1"
+	modTenant, _, err := TOSession.TenantByName(name)
+	if err != nil {
+		t.Errorf("cannot GET Tenant by name: %s - %v\n", name, err)
+	}
+
+	newParent, _, err := TOSession.TenantByName(parentName)
+	if err != nil {
+		t.Errorf("cannot GET Tenant by name: %s - %v\n", parentName, err)
+	}
+	modTenant.ParentID = newParent.ID
+
+	resp, err := TOSession.UpdateTenant(strconv.Itoa(modTenant.ID), modTenant)
+	if err != nil {
+		t.Errorf("cannot UPDATE Tenant by id: %v\n", err)
+	}
+
+	// Retrieve the Tenant to check Tenant parent name got updated
+	respTenant, _, err := TOSession.Tenant(strconv.Itoa(modTenant.ID))
+	if err != nil {
+		t.Errorf("cannot GET Tenant by name: %v - %v\n", name, err)
+	}
+	t.Logf("modified: %++v", resp)
+	if respTenant.ParentName != parentName {
+		t.Errorf("results do not match actual: %s, expected: %s\n", respTenant.ParentName, parentName)
+	}
+
+}
+
+func DeleteTestTenants(t *testing.T) {
+
+	t1 := "tenant1"
+	tenant1, _, err := TOSession.TenantByName(t1)
+
+	if err != nil {
+		t.Errorf("cannot GET Tenant by name: %v - %v\n", t1, err)
+	}
+
+	_, err = TOSession.DeleteTenant(strconv.Itoa(tenant1.ID))
+	if err == nil {
+		t.Errorf("%s has child tenants -- should not be able to delete", t1)
+	}
+	expected := "Tenant 'tenant1' has child tenants"
+	if !strings.Contains(err.Error(), expected) {
+		t.Errorf("expected error: %s;  got %s", expected, err.Error())
+	}
+
+	t2 := "tenant2"
+	tenant2, _, err := TOSession.TenantByName(t2)
+	_, err = TOSession.DeleteTenant(strconv.Itoa(tenant2.ID))
+	if err != nil {
+		t.Errorf("error deleting tenant %s: %v", t2, err)
+	}
+
+	// Now should be able to delete t1
+	tenant1, _, err = TOSession.TenantByName(t1)
+	_, err = TOSession.DeleteTenant(strconv.Itoa(tenant1.ID))
+	if err != nil {
+		t.Errorf("error deleting tenant %s: %v", t1, err)
+	}
+
+}
diff --git a/traffic_ops/testing/api/v13/todb.go b/traffic_ops/testing/api/v13/todb.go
index 0081ee8dd..0ebea91a3 100644
--- a/traffic_ops/testing/api/v13/todb.go
+++ b/traffic_ops/testing/api/v13/todb.go
@@ -49,12 +49,6 @@ func OpenConnection() (*sql.DB, error) {
 func SetupTestData(*sql.DB) error {
 	var err error
 
-	err = SetupTenants(db)
-	if err != nil {
-		fmt.Printf("\nError setting up tenants %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err)
-		os.Exit(1)
-	}
-
 	err = SetupRoles(db)
 	if err != nil {
 		fmt.Printf("\nError setting up roles %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err)
@@ -73,6 +67,12 @@ func SetupTestData(*sql.DB) error {
 		os.Exit(1)
 	}
 
+	err = SetupTenants(db)
+	if err != nil {
+		fmt.Printf("\nError setting up tenant %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err)
+		os.Exit(1)
+	}
+
 	err = SetupTmusers(db)
 	if err != nil {
 		fmt.Printf("\nError setting up tm_user %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err)
@@ -136,12 +136,12 @@ func SetupTmusers(db *sql.DB) error {
 
 	// Creates users in different tenants
 	sqlStmt := `
-INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_id) VALUES ('` + Config.TrafficOps.Users.Disallowed + `','` + encryptedPassword + `','` + encryptedPassword + `', 1, 3);
-INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_id) VALUES ('` + Config.TrafficOps.Users.ReadOnly + `','` + encryptedPassword + `','` + encryptedPassword + `', 2, 3);
-INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_id) VALUES ('` + Config.TrafficOps.Users.Operations + `','` + encryptedPassword + `','` + encryptedPassword + `', 3, 3);
+INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_id) VALUES ('` + Config.TrafficOps.Users.Disallowed + `','` + encryptedPassword + `','` + encryptedPassword + `', 1, 2);
+INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_id) VALUES ('` + Config.TrafficOps.Users.ReadOnly + `','` + encryptedPassword + `','` + encryptedPassword + `', 2, 2);
+INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_id) VALUES ('` + Config.TrafficOps.Users.Operations + `','` + encryptedPassword + `','` + encryptedPassword + `', 3, 2);
 INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_id) VALUES ('` + Config.TrafficOps.Users.Admin + `','` + encryptedPassword + `','` + encryptedPassword + `', 4, 2);
-INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_id) VALUES ('` + Config.TrafficOps.Users.Portal + `','` + encryptedPassword + `','` + encryptedPassword + `', 5, 3);
-INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_id) VALUES ('` + Config.TrafficOps.Users.Federation + `','` + encryptedPassword + `','` + encryptedPassword + `', 6, 3);
+INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_id) VALUES ('` + Config.TrafficOps.Users.Portal + `','` + encryptedPassword + `','` + encryptedPassword + `', 5, 2);
+INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_id) VALUES ('` + Config.TrafficOps.Users.Federation + `','` + encryptedPassword + `','` + encryptedPassword + `', 6, 2);
 `
 	err = execSQL(db, sqlStmt, "tm_user")
 	if err != nil {
@@ -153,11 +153,11 @@ INSERT INTO tm_user (username, local_passwd, confirm_local_passwd, role, tenant_
 // SetupTenants ...
 func SetupTenants(db *sql.DB) error {
 
+	// TODO: root tenant must be present in initial database.  "badtenant" is needed for now so tests can be done
+	// with a tenant outside the user's tenant.  That should be removed once User API tests are in place rather than the SetupUsers defined above.
 	sqlStmt := `
 INSERT INTO tenant (id, name, active, parent_id, last_updated) VALUES (1, 'root', true, null, '2018-01-19 19:01:21.327262');
-INSERT INTO tenant (id, name, active, parent_id, last_updated) VALUES (2, 'grandparent tenant', true, 1, '2018-01-19 19:01:21.327262');
-INSERT INTO tenant (id, name, active, parent_id, last_updated) VALUES (3, 'parent tenant', true, 2, '2018-01-19 19:01:21.327262');
-INSERT INTO tenant (id, name, active, parent_id, last_updated) VALUES (4, 'child tenant', true, 3, '2018-01-19 19:01:21.327262');
+INSERT INTO tenant (id, name, active, parent_id, last_updated) VALUES (2, 'badtenant', true, 1, '2018-01-19 19:01:21.327262');
 `
 	err := execSQL(db, sqlStmt, "tenant")
 	if err != nil {
diff --git a/traffic_ops/traffic_ops_golang/api/shared_handlers.go b/traffic_ops/traffic_ops_golang/api/shared_handlers.go
index c6f645c15..c1cc8d39b 100644
--- a/traffic_ops/traffic_ops_golang/api/shared_handlers.go
+++ b/traffic_ops/traffic_ops_golang/api/shared_handlers.go
@@ -135,6 +135,7 @@ func ReadHandler(typeRef Reader, db *sqlx.DB) http.HandlerFunc {
 		if err != nil {
 			log.Errorf("unable to get parameters from request: %s", err)
 			handleErrs(http.StatusInternalServerError, err)
+			return
 		}
 
 		user, err := auth.GetCurrentUser(ctx)
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index 341b88ada..6664b3653 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -29,6 +29,7 @@ import (
 	"time"
 
 	tclog "github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/about"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/asn"
@@ -44,6 +45,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservicesregexes"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/division"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/hwinfo"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/origin"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/parameter"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/physlocation"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ping"
@@ -55,11 +57,10 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/staticdnsentry"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/status"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/systeminfo"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/types"
 
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/origin"
 	"github.com/basho/riak-go-client"
-	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 // Authenticated ...
@@ -169,12 +170,12 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{1.1, http.MethodDelete, `regions/{id}$`, api.DeleteHandler(region.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil},
 
 		// get all edge servers associated with a delivery service (from deliveryservice_server table)
-		{1.1, http.MethodGet, `deliveryserviceserver$`, dsserver.ReadDSSHandler(d.DB),auth.PrivLevelReadOnly, Authenticated, nil},
-		{1.1, http.MethodPost,`deliveryserviceserver$`, dsserver.GetReplaceHandler(d.DB),auth.PrivLevelOperations, Authenticated, nil},
-		{1.1, http.MethodPost,`deliveryservices/{xml_id}/servers$`, dsserver.GetCreateHandler( d.DB ) ,auth.PrivLevelOperations, Authenticated, nil},
-		{1.1, http.MethodGet, `servers/{id}/deliveryservices$`, api.ReadHandler(dsserver.GetDServiceRef(), d.DB),auth.PrivLevelReadOnly, Authenticated, nil},
-		{1.1, http.MethodGet, `deliveryservices/{id}/servers$`, dsserver.GetReadHandler(d.DB, tc.Assigned),auth.PrivLevelReadOnly, Authenticated, nil},
-		{1.1, http.MethodGet, `deliveryservices/{id}/unassigned_servers$`, dsserver.GetReadHandler(d.DB, tc.Unassigned),auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodGet, `deliveryserviceserver$`, dsserver.ReadDSSHandler(d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodPost, `deliveryserviceserver$`, dsserver.GetReplaceHandler(d.DB), auth.PrivLevelOperations, Authenticated, nil},
+		{1.1, http.MethodPost, `deliveryservices/{xml_id}/servers$`, dsserver.GetCreateHandler(d.DB), auth.PrivLevelOperations, Authenticated, nil},
+		{1.1, http.MethodGet, `servers/{id}/deliveryservices$`, api.ReadHandler(dsserver.GetDServiceRef(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodGet, `deliveryservices/{id}/servers$`, dsserver.GetReadHandler(d.DB, tc.Assigned), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodGet, `deliveryservices/{id}/unassigned_servers$`, dsserver.GetReadHandler(d.DB, tc.Unassigned), auth.PrivLevelReadOnly, Authenticated, nil},
 		//{1.1, http.MethodGet, `deliveryservices/{id}/servers/eligible$`, dsserver.GetReadHandler(d.DB, tc.Eligible),auth.PrivLevelReadOnly, Authenticated, nil},
 
 		//Server
@@ -282,6 +283,13 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{1.1, http.MethodPost, `parameterprofile/?$`, profileparameter.PostParamProfile(d.DB.DB), auth.PrivLevelOperations, Authenticated, nil},
 		{1.1, http.MethodDelete, `profileparameters/{profileId}/{parameterId}$`, api.DeleteHandler(profileparameter.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil},
 
+		//Tenants
+		{1.1, http.MethodGet, `tenants/?(\.json)?$`, api.ReadHandler(tenant.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodGet, `tenants/{id}$`, api.ReadHandler(tenant.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodPut, `tenants/{id}$`, api.UpdateHandler(tenant.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil},
+		{1.1, http.MethodPost, `tenants/?$`, api.CreateHandler(tenant.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil},
+		{1.1, http.MethodDelete, `tenants/{id}$`, api.DeleteHandler(tenant.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil},
+
 		//CRConfig
 		{1.1, http.MethodGet, `cdns/{cdn}/snapshot/?$`, crconfig.SnapshotGetHandler(d.DB, d.Config), crconfig.PrivLevel, Authenticated, nil},
 		{1.1, http.MethodGet, `cdns/{cdn}/snapshot/new/?$`, crconfig.Handler(d.DB, d.Config), crconfig.PrivLevel, Authenticated, nil},
diff --git a/traffic_ops/traffic_ops_golang/tenant/tenancy.go b/traffic_ops/traffic_ops_golang/tenant/tenancy.go
index 177a32d33..44f1363f6 100644
--- a/traffic_ops/traffic_ops_golang/tenant/tenancy.go
+++ b/traffic_ops/traffic_ops_golang/tenant/tenancy.go
@@ -24,25 +24,27 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"strconv"
 
 	"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-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"
+	validation "github.com/go-ozzo/ozzo-validation"
 	"github.com/jmoiron/sqlx"
+	"github.com/lib/pq"
 )
 
-type Tenant struct {
-	ID       int
-	Name     string
-	Active   bool
-	ParentID int
-}
+// TOTenant provides a local type against which to define methods
+type TOTenant tc.TenantNullable
 
+// DeliveryServiceTenantInfo provides only deliveryservice info needed here
 type DeliveryServiceTenantInfo tc.DeliveryServiceNullable
 
-// returns true if the user has tenant access on this deliveryservice
+// IsTenantAuthorized returns true if the user has tenant access on this tenant
 func (dsInfo DeliveryServiceTenantInfo) IsTenantAuthorized(user *auth.CurrentUser, tx *sql.Tx) (bool, error) {
 	if dsInfo.TenantID == nil {
 		return false, errors.New("TenantID is nil")
@@ -62,15 +64,16 @@ func GetDeliveryServiceTenantInfo(xmlID string, tx *sql.Tx) (*DeliveryServiceTen
 	return &ds, nil
 }
 
-// Check checks that the given user has access to the given XMLID. Returns a user error, system error, and the HTTP status code to be returned to the user if an error occurred. On success, the user error and system error will both be nil, and the error code should be ignored.
+// Check checks that the given user has access to the given XMLID. Returns a user error, system error,
+// and the HTTP status code to be returned to the user if an error occurred. On success, the user error
+// and system error will both be nil, and the error code should be ignored.
 func Check(user *auth.CurrentUser, XMLID string, tx *sql.Tx) (error, error, int) {
 	dsInfo, err := GetDeliveryServiceTenantInfo(XMLID, tx)
 	if err != nil {
 		if dsInfo == nil {
 			return nil, errors.New("deliveryservice lookup failure: " + err.Error()), http.StatusInternalServerError
-		} else {
-			return errors.New("no such deliveryservice: '" + XMLID + "'"), nil, http.StatusBadRequest
 		}
+		return errors.New("no such deliveryservice: '" + XMLID + "'"), nil, http.StatusBadRequest
 	}
 	hasAccess, err := dsInfo.IsTenantAuthorized(user, tx)
 	if err != nil {
@@ -86,35 +89,33 @@ func Check(user *auth.CurrentUser, XMLID string, tx *sql.Tx) (error, error, int)
 // NOTE: This method does not use the use_tenancy parameter and if this method is being used
 // to control tenancy the parameter must be checked. The method IsResourceAuthorizedToUser checks the use_tenancy parameter
 // and should be used for this purpose in most cases.
-func GetUserTenantList(user auth.CurrentUser, db *sqlx.DB) ([]Tenant, error) {
-	query := `WITH RECURSIVE q AS (SELECT id, name, active, parent_id FROM tenant WHERE id = $1
-	UNION SELECT t.id, t.name, t.active, t.parent_id  FROM tenant t JOIN q ON q.id = t.parent_id)
-	SELECT id, name, active, parent_id FROM q;`
+func GetUserTenantList(user auth.CurrentUser, db *sqlx.DB) ([]TOTenant, error) {
+	query := `WITH RECURSIVE q AS (SELECT id, name, active, parent_id, last_updated FROM tenant WHERE id = $1
+	UNION SELECT t.id, t.name, t.active, t.parent_id, t.last_updated  FROM tenant t JOIN q ON q.id = t.parent_id)
+	SELECT id, name, active, parent_id, last_updated FROM q;`
 
 	log.Debugln("\nQuery: ", query)
 
-	var tenantID int
-	var name string
-	var active bool
-	var parentID *int
-
+	var (
+		tenantID, parentID int
+		name               string
+		active             bool
+		lastUpdated        tc.TimeNoMod
+	)
 	rows, err := db.Query(query, user.TenantID)
 	if err != nil {
 		return nil, err
 	}
 	defer rows.Close()
 
-	tenants := []Tenant{}
+	tenants := []TOTenant{}
 
 	for rows.Next() {
-		if err := rows.Scan(&tenantID, &name, &active, &parentID); err != nil {
+		if err := rows.Scan(&tenantID, &name, &active, &parentID, &lastUpdated); err != nil {
 			return nil, err
 		}
-		if parentID != nil {
-			tenants = append(tenants, Tenant{ID: tenantID, Name: name, Active: active, ParentID: *parentID})
-		} else {
-			tenants = append(tenants, Tenant{ID: tenantID, Name: name, Active: active, ParentID: -1})
-		}
+
+		tenants = append(tenants, TOTenant{ID: &tenantID, Name: &name, Active: &active, ParentID: &parentID})
 	}
 
 	return tenants, nil
@@ -189,10 +190,8 @@ func IsResourceAuthorizedToUser(resourceTenantID int, user auth.CurrentUser, db
 		}
 		if active && tenantID == resourceTenantID {
 			return true, nil
-		} else {
-			fmt.Printf("default")
-			return false, nil
 		}
+		return false, nil
 	}
 }
 
@@ -228,3 +227,357 @@ func IsResourceAuthorizedToUserTx(resourceTenantID int, user *auth.CurrentUser,
 		}
 	}
 }
+
+// GetRefType allows shared handlers to decode JSON to the tenant type
+// Part of the Identifier interface
+func GetRefType() *TOTenant {
+	return &TOTenant{}
+}
+
+// GetID wraps the ID member with null checking
+// Part of the Identifier interface
+func (ten TOTenant) GetID() (int, bool) {
+	if ten.ID == nil {
+		return 0, false
+	}
+	return *ten.ID, true
+}
+
+// GetKeyFieldsInfo identifies types of the key fields
+func (ten TOTenant) GetKeyFieldsInfo() []api.KeyFieldInfo {
+	return []api.KeyFieldInfo{{"id", api.GetIntKey}}
+}
+
+// GetKeys returns values of keys
+func (ten TOTenant) GetKeys() (map[string]interface{}, bool) {
+	var id int
+	if ten.ID != nil {
+		id = *ten.ID
+	}
+	return map[string]interface{}{"id": id}, true
+}
+
+// GetAuditName returns a unique identifier
+// Part of the Identifier interface
+func (ten TOTenant) GetAuditName() string {
+	if ten.Name != nil {
+		return *ten.Name
+	}
+	id, _ := ten.GetID()
+	return strconv.Itoa(id)
+}
+
+// GetType returns the name of the type for messages
+// Part of the Identifier interface
+func (ten TOTenant) GetType() string {
+	return "tenant"
+}
+
+// SetKeys allows CreateHandler to assign id once object is created.
+// Part of the Identifier interface
+func (ten *TOTenant) 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.
+	ten.ID = &i
+}
+
+// Validate fulfills the api.Validator interface
+func (ten TOTenant) Validate(db *sqlx.DB) []error {
+	errs := validation.Errors{
+		"name":     validation.Validate(ten.Name, validation.Required),
+		"active":   validation.Validate(ten.Active), // only validate it's boolean
+		"parentId": validation.Validate(ten.ParentID, validation.Required, validation.Min(1)),
+	}
+	return tovalidate.ToErrors(errs)
+}
+
+// Create implements the Creator interface
+//all implementations of Creator should use transactions and return the proper errorType
+//ParsePQUniqueConstraintError is used to determine if a tenant 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 tenant and have
+//to be added to the struct
+func (ten *TOTenant) 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(), ten)
+	if err != nil {
+		if pqErr, ok := err.(*pq.Error); ok {
+			err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
+			if eType == tc.DataConflictError {
+				return errors.New("a tenant 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 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 tenant 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 tenant insert")
+		log.Errorln(err)
+		return tc.DBError, tc.SystemError
+	}
+	ten.SetKeys(map[string]interface{}{"id": id})
+	ten.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
+}
+
+// Read implements the tc.Reader interface
+func (ten *TOTenant) 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{
+		"active":      dbhelpers.WhereColumnInfo{Column: "t.active", Checker: nil},
+		"id":          dbhelpers.WhereColumnInfo{Column: "t.id", Checker: api.IsInt},
+		"name":        dbhelpers.WhereColumnInfo{Column: "t.name", Checker: nil},
+		"parent_id":   dbhelpers.WhereColumnInfo{Column: "t.parentID", Checker: api.IsInt},
+		"parent_name": dbhelpers.WhereColumnInfo{Column: "p.name", Checker: api.IsInt},
+	}
+	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 tenants: %v", err)
+		return nil, []error{tc.DBError}, tc.SystemError
+	}
+	defer rows.Close()
+
+	tenants := []interface{}{}
+	for rows.Next() {
+		var s TOTenant
+		if err = rows.StructScan(&s); err != nil {
+			log.Errorf("error parsing Tenant rows: %v", err)
+			return nil, []error{tc.DBError}, tc.SystemError
+		}
+		tenants = append(tenants, s)
+	}
+
+	return tenants, []error{}, tc.NoError
+}
+
+//The TOTenant implementation of the Updater interface
+//all implementations of Updater should use transactions and return the proper errorType
+//ParsePQUniqueConstraintError is used to determine if a tenant 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 (ten *TOTenant) 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 tenant: %++v", updateQuery(), ten)
+	resultRows, err := tx.NamedQuery(updateQuery(), ten)
+	if err != nil {
+		if pqErr, ok := err.(*pq.Error); ok {
+			err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
+			if eType == tc.DataConflictError {
+				return errors.New("a tenant with " + err.Error()), eType
+			}
+			return err, eType
+		}
+		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)
+	ten.LastUpdated = &lastUpdated
+	if rowsAffected != 1 {
+		if rowsAffected < 1 {
+			return errors.New("no tenant found with this id"), tc.DataMissingError
+		}
+		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
+}
+
+//Delete implements the Deleter interface
+//all implementations of Deleter should use transactions and return the proper errorType
+func (ten *TOTenant) Delete(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
+	if ten.ID == nil {
+		// should never happen...
+		return errors.New("invalid tenant: id is nil"), tc.SystemError
+	}
+	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 tenant: %++v", deleteQuery(), ten)
+	result, err := tx.NamedExec(deleteQuery(), ten)
+	if err != nil {
+		if pqErr, ok := err.(*pq.Error); ok {
+			err = fmt.Errorf("pqErr is %++v\n", pqErr)
+			var existing string
+			switch pqErr.Table {
+			case "tenant":
+				existing = "child tenants"
+			case "tm_user":
+				existing = "users"
+			case "deliveryservice":
+				existing = "deliveryservices"
+			case "origin":
+				existing = "origins"
+			default:
+				existing = pqErr.Table
+			}
+
+			// another query to get tenant name for the error message
+			name := strconv.Itoa(*ten.ID)
+			if err := db.QueryRow(`SELECT name FROM tenant WHERE id = $1`, *ten.ID).Scan(&name); err != nil {
+				// use ID as a backup for name the error -- this should never happen
+				log.Debugf("error getting tenant name: %++v", err)
+			}
+
+			err = errors.New("Tenant '" + name + "' has " + existing + ". Please update these " + existing + " and retry.")
+			return err, tc.DataConflictError
+		}
+		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 tenant with that id found"), tc.DataMissingError
+		}
+		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
+t.active AS active,
+t.name AS name,
+t.id AS id,
+t.last_updated AS last_updated,
+t.parent_id AS parent_id,
+p.name AS parent_name
+
+FROM tenant AS t
+LEFT OUTER JOIN tenant AS p
+ON t.parent_id = p.id`
+	return query
+}
+
+func updateQuery() string {
+	query := `UPDATE
+tenant SET
+active=:active,
+name=:name,
+parent_id=:parent_id
+
+WHERE id=:id RETURNING last_updated`
+	return query
+}
+
+func insertQuery() string {
+	query := `INSERT INTO tenant (
+name,
+active,
+parent_id
+) VALUES (
+:name,
+:active,
+:parent_id
+) RETURNING id,last_updated`
+	return query
+}
+
+func deleteQuery() string {
+	query := `DELETE FROM tenant
+WHERE id=:id`
+	return query
+}


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services