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 2022/05/10 17:01:30 UTC

[GitHub] [trafficcontrol] ocket8888 commented on a diff in pull request #6817: Prevent modifying and/or deleting reserved statuses

ocket8888 commented on code in PR #6817:
URL: https://github.com/apache/trafficcontrol/pull/6817#discussion_r869458440


##########
cache-config/testing/ort-tests/tcdata/statuses.go:
##########
@@ -19,6 +19,14 @@ import (
 	"testing"
 )
 
+var statusNameMap = map[string]bool{

Review Comment:
   I see this map repeated in 5 different places in this PR. I think the reason you're doing that is because it's mutable so exposing it as an exported member of `lib/go-tc` could be problematic. But functions are immutable, so you could just have one function in the lib that does this instead of repeating it 5 times:
   ```go
   // IsReservedStatus checks if the given status name is reserved; i.e. may not be
   // deleted or modified.
   func IsReservedStatus(status string) bool {
   	switch (status) {
   	case CacheStatusAdminDown:
   		fallthrough
   	case CacheStatusOffline:
   		fallthrough
   	case CacheStatusOnline:
   		fallthrough
   	case CacheStatusPreProd:
   		fallthrough
   	case CacheStatusReported:
   		return true
   	}
   	return false
   }
   ```



##########
cache-config/testing/ort-tests/tcdata/statuses.go:
##########
@@ -46,17 +54,43 @@ func (r *TCData) DeleteTestStatuses(t *testing.T) {
 		respStatus := resp[0]
 
 		delResp, _, err := TOSession.DeleteStatusByID(respStatus.ID)
-		if err != nil {
-			t.Errorf("cannot DELETE Status by name: %v - %v", err, delResp)
-		}
+		if _, ok := statusNameMap[*status.Name]; !ok {
+			if err != nil {
+				t.Errorf("cannot DELETE Status by name: %v - %v", err, delResp)
+			}
 
-		// Retrieve the Status to see if it got deleted
-		types, _, err := TOSession.GetStatusByName(*status.Name)
-		if err != nil {
-			t.Errorf("error deleting Status name: %v", err)
-		}
-		if len(types) > 0 {
-			t.Errorf("expected Status name: %s to be deleted", *status.Name)
+			// Retrieve the Status to see if it got deleted
+			types, _, err := TOSession.GetStatusByName(*status.Name)
+			if err != nil {
+				t.Errorf("error deleting Status name: %v", err)
+			}
+			if len(types) > 0 {
+				t.Errorf("expected Status name: %s to be deleted", *status.Name)
+			}
+		} else {
+			if err == nil {
+				t.Errorf("expected an error while trying to delete a reserved status, but got nothing")
+			}
 		}
 	}
+	r.ForceDeleteStatuses(t)
+}
+
+// ForceDeleteStatuses forcibly deletes the statuses from the db.
+func (r *TCData) ForceDeleteStatuses(t *testing.T) {

Review Comment:
   Instead of doing this, we should just treat the statuses as immutable, which they now are, and not create an invalid Traffic Ops instance by deleting them. That means removing the Statuses which already exist in every valid Traffic Ops instance from the testing data "fixtures".



##########
traffic_ops/app/db/migrations/2022050916074300_add_reserved_statuses.down.sql:
##########
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+DELETE FROM public.status WHERE name IN ('ONLINE', 'OFFLINE', 'REPORTED', 'ADMIN_DOWN', 'PRE_PROD');

Review Comment:
   This will break existing TO installations. If you have data unchanged from seeds.sql and then try to upgrade with this migration, then downgrade back, first of all it will probably fail to roll back because there will almost certainly be servers using at least one of these statuses, but then also your data will not be left in the same state as it was pre-upgrade.
   
   This file should be blank. In the worst case scenario, that would mean downgrading creates statuses that need to exist for ATC to work, so there's only benefit.



##########
traffic_ops/testing/api/v2/statuses_test.go:
##########
@@ -42,35 +50,44 @@ func CreateTestStatuses(t *testing.T) {
 
 func UpdateTestStatuses(t *testing.T) {
 
-	firstStatus := testData.Statuses[0]
-	if firstStatus.Name == nil {
-		t.Fatal("cannot update test statuses: first test data status must have a name")
-	}
-
-	// Retrieve the Status by name so we can get the id for the Update
-	resp, _, err := TOSession.GetStatusByName(*firstStatus.Name)
-	if err != nil {
-		t.Errorf("cannot GET Status by name: %v - %v", firstStatus.Name, err)
-	}
-	remoteStatus := resp[0]
-	expectedStatusDesc := "new description"
-	remoteStatus.Description = expectedStatusDesc
-	var alert tc.Alerts
-	alert, _, err = TOSession.UpdateStatusByID(remoteStatus.ID, remoteStatus)
-	if err != nil {
-		t.Errorf("cannot UPDATE Status by id: %v - %v", err, alert)
-	}
-
-	// Retrieve the Status to check Status name got updated
-	resp, _, err = TOSession.GetStatusByID(remoteStatus.ID)
-	if err != nil {
-		t.Errorf("cannot GET Status by ID: %v - %v", firstStatus.Description, err)
+	if len(testData.Statuses) < 1 {
+		t.Fatal("Need at least one Status to test updating a Status")
 	}
-	respStatus := resp[0]
-	if respStatus.Description != expectedStatusDesc {
-		t.Errorf("results do not match actual: %s, expected: %s", respStatus.Name, expectedStatusDesc)
+	for _, status := range testData.Statuses {
+		if status.Name == nil {
+			t.Fatal("cannot update test statuses: test data status must have a name")
+		}
+		// Retrieve the Status by name so we can get the id for the Update
+		resp, _, err := TOSession.GetStatusByName(*status.Name)
+		if err != nil {
+			t.Errorf("cannot GET Status by name: %v - %v", status.Name, err)
+		}
+		remoteStatus := resp[0]
+		expectedStatusDesc := "new description"
+		remoteStatus.Description = expectedStatusDesc
+		var alert tc.Alerts
+		alert, _, err = TOSession.UpdateStatusByID(remoteStatus.ID, remoteStatus)
+
+		if _, ok := statusNameMap[*status.Name]; ok {
+			if err == nil {
+				t.Errorf("expected an error about while updating a reserved status, but got nothing")
+			}
+		} else {
+			if err != nil {
+				t.Errorf("cannot UPDATE Status by id: %v - %v", err, alert)
+			}
+
+			// Retrieve the Status to check Status name got updated
+			resp, _, err = TOSession.GetStatusByID(remoteStatus.ID)
+			if err != nil {
+				t.Errorf("cannot GET Status by ID: %v - %v", status.Description, err)

Review Comment:
   Same as above; `status.Description` is a `*string`.



##########
cache-config/testing/ort-tests/tcdata/statuses.go:
##########
@@ -46,17 +54,43 @@ func (r *TCData) DeleteTestStatuses(t *testing.T) {
 		respStatus := resp[0]
 
 		delResp, _, err := TOSession.DeleteStatusByID(respStatus.ID)
-		if err != nil {
-			t.Errorf("cannot DELETE Status by name: %v - %v", err, delResp)
-		}
+		if _, ok := statusNameMap[*status.Name]; !ok {
+			if err != nil {
+				t.Errorf("cannot DELETE Status by name: %v - %v", err, delResp)
+			}
 
-		// Retrieve the Status to see if it got deleted
-		types, _, err := TOSession.GetStatusByName(*status.Name)
-		if err != nil {
-			t.Errorf("error deleting Status name: %v", err)
-		}
-		if len(types) > 0 {
-			t.Errorf("expected Status name: %s to be deleted", *status.Name)
+			// Retrieve the Status to see if it got deleted
+			types, _, err := TOSession.GetStatusByName(*status.Name)
+			if err != nil {
+				t.Errorf("error deleting Status name: %v", err)
+			}
+			if len(types) > 0 {
+				t.Errorf("expected Status name: %s to be deleted", *status.Name)
+			}
+		} else {
+			if err == nil {
+				t.Errorf("expected an error while trying to delete a reserved status, but got nothing")
+			}
 		}

Review Comment:
   nit:
   ```go
   ...
   } else {
       if condition {
           ...
       }
   }
   ```
   is equivalent to the shorter
   ```go
   ...
   } else if condition {
       ...
   }



##########
traffic_ops/traffic_ops_golang/status/statuses.go:
##########
@@ -119,9 +127,29 @@ func (st *TOStatus) Read(h http.Header, useIMS bool) ([]interface{}, error, erro
 	return readVals, nil, nil, errCode, maxTime
 }
 
-func (st *TOStatus) Update(h http.Header) (error, error, int) { return api.GenericUpdate(h, st) }
-func (st *TOStatus) Create() (error, error, int)              { return api.GenericCreate(st) }
-func (st *TOStatus) Delete() (error, error, int)              { return api.GenericDelete(st) }
+func (st *TOStatus) Update(h http.Header) (error, error, int) {
+	var statusName string
+	err := st.APIInfo().Tx.QueryRow(`SELECT name from status WHERE id = $1`, *st.ID).Scan(&statusName)
+	if err != nil {
+		return nil, fmt.Errorf("error querying status name from ID: %v", err), http.StatusInternalServerError

Review Comment:
   errors constructed from errors should wrap with `%w`.



##########
traffic_ops/testing/api/v2/statuses_test.go:
##########
@@ -42,35 +50,44 @@ func CreateTestStatuses(t *testing.T) {
 
 func UpdateTestStatuses(t *testing.T) {
 
-	firstStatus := testData.Statuses[0]
-	if firstStatus.Name == nil {
-		t.Fatal("cannot update test statuses: first test data status must have a name")
-	}
-
-	// Retrieve the Status by name so we can get the id for the Update
-	resp, _, err := TOSession.GetStatusByName(*firstStatus.Name)
-	if err != nil {
-		t.Errorf("cannot GET Status by name: %v - %v", firstStatus.Name, err)
-	}
-	remoteStatus := resp[0]
-	expectedStatusDesc := "new description"
-	remoteStatus.Description = expectedStatusDesc
-	var alert tc.Alerts
-	alert, _, err = TOSession.UpdateStatusByID(remoteStatus.ID, remoteStatus)
-	if err != nil {
-		t.Errorf("cannot UPDATE Status by id: %v - %v", err, alert)
-	}
-
-	// Retrieve the Status to check Status name got updated
-	resp, _, err = TOSession.GetStatusByID(remoteStatus.ID)
-	if err != nil {
-		t.Errorf("cannot GET Status by ID: %v - %v", firstStatus.Description, err)
+	if len(testData.Statuses) < 1 {
+		t.Fatal("Need at least one Status to test updating a Status")
 	}
-	respStatus := resp[0]
-	if respStatus.Description != expectedStatusDesc {
-		t.Errorf("results do not match actual: %s, expected: %s", respStatus.Name, expectedStatusDesc)
+	for _, status := range testData.Statuses {
+		if status.Name == nil {
+			t.Fatal("cannot update test statuses: test data status must have a name")
+		}
+		// Retrieve the Status by name so we can get the id for the Update
+		resp, _, err := TOSession.GetStatusByName(*status.Name)
+		if err != nil {
+			t.Errorf("cannot GET Status by name: %v - %v", status.Name, err)

Review Comment:
   `status.Name` is a pointer to a string, so using `%v` to format it is going to not be very helpful since [`%v` formats `*string`s as memory addresses](https://go.dev/play/p/YEGLp3CQe7x). This is why types should always use the most specific formatting parameter they can; if you use `%s` here it will fail `go vet` and thus the test won't build, which lets you know immediately that it's not printing what it should be.



##########
traffic_ops/testing/api/v2/statuses_test.go:
##########
@@ -106,17 +123,43 @@ func DeleteTestStatuses(t *testing.T) {
 		respStatus := resp[0]
 
 		delResp, _, err := TOSession.DeleteStatusByID(respStatus.ID)
-		if err != nil {
-			t.Errorf("cannot DELETE Status by name: %v - %v", err, delResp)
+		if _, ok := statusNameMap[*status.Name]; !ok {
+			if err != nil {
+				t.Errorf("cannot DELETE Status by name: %v - %v", err, delResp)
+			}
+
+			// Retrieve the Status to see if it got deleted
+			types, _, err := TOSession.GetStatusByName(*status.Name)
+			if err != nil {
+				t.Errorf("error deleting Status name: %s", err.Error())

Review Comment:
   nit: `.Error()` is redundant because `%s` formats errors as strings by calling their `Error` method.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org