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 2021/06/03 20:13:45 UTC

[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #5895: Add the ability to CRD CDN Locks

ocket8888 commented on a change in pull request #5895:
URL: https://github.com/apache/trafficcontrol/pull/5895#discussion_r645088439



##########
File path: traffic_ops/traffic_ops_golang/cdn_lock/cdn_lock.go
##########
@@ -0,0 +1,168 @@
+package cdn_lock
+
+/*
+ * 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"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"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"
+)
+
+const readQuery = `SELECT username, cdn, message, soft, last_updated FROM cdn_lock`
+const insertQuery = `INSERT INTO cdn_lock (username, cdn, message, soft) VALUES (:username, :cdn, :message, :soft) RETURNING username, cdn, message, soft, last_updated`
+const deleteQuery = `DELETE FROM cdn_lock WHERE cdn=$1 AND username=$2 RETURNING username, cdn, message, soft, last_updated`
+const deleteAdminQuery = `DELETE FROM cdn_lock WHERE cdn=$1 RETURNING username, cdn, message, soft, last_updated`
+
+func Read(w http.ResponseWriter, r *http.Request) {

Review comment:
       Should this endpoint support IMS?

##########
File path: lib/go-tc/cdn_lock.go
##########
@@ -0,0 +1,48 @@
+package 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.
+ */
+
+import (
+	"time"
+)
+
+// CDNLock is a struct to store the details of a lock that a user wishes to acquire on a CDN.
+type CDNLock struct {
+	UserName    string    `json:"userName" db:"username"`
+	CDN         string    `json:"cdn" db:"cdn"`
+	Message     *string   `json:"message" db:"message"`
+	Soft        *bool     `json:"soft" db:"soft"`
+	LastUpdated time.Time `json:"lastUpdated" db:"last_updated"`
+}
+
+// CdnLockCreateResponse is a struct to store the response of a CREATE operation on a lock.
+type CdnLockCreateResponse struct {
+	Response CDNLock `json:"response"`
+	Alerts
+}
+
+// CdnLocksGetResponse is a struct to store the response of a GET operation on locks.
+type CdnLocksGetResponse struct {
+	Response []CDNLock `json:"response"`
+	Alerts
+}
+
+// CdnLockDeleteResponse is a struct to store the response of a DELETE operation on a lock.
+type CdnLockDeleteResponse CdnLockCreateResponse

Review comment:
       These three types should use `CDN...` for their names, to be consistent with `CDNLock`.

##########
File path: traffic_ops/traffic_ops_golang/cdn_lock/cdn_lock.go
##########
@@ -0,0 +1,168 @@
+package cdn_lock
+
+/*
+ * 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"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"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"
+)
+
+const readQuery = `SELECT username, cdn, message, soft, last_updated FROM cdn_lock`
+const insertQuery = `INSERT INTO cdn_lock (username, cdn, message, soft) VALUES (:username, :cdn, :message, :soft) RETURNING username, cdn, message, soft, last_updated`
+const deleteQuery = `DELETE FROM cdn_lock WHERE cdn=$1 AND username=$2 RETURNING username, cdn, message, soft, last_updated`
+const deleteAdminQuery = `DELETE FROM cdn_lock WHERE cdn=$1 RETURNING username, cdn, message, soft, last_updated`
+
+func Read(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	cols := map[string]dbhelpers.WhereColumnInfo{
+		"cdn":      {Column: "cdn_lock.cdn", Checker: nil},
+		"username": {Column: "cdn_lock.username", Checker: nil},
+	}
+
+	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, cols)
+	if len(errs) > 0 {
+		errCode = http.StatusBadRequest
+		userErr = util.JoinErrs(errs)
+		api.HandleErr(w, r, tx, errCode, userErr, nil)
+		return
+	}
+
+	cdnLock := []tc.CDNLock{}
+	query := readQuery + where + orderBy + pagination
+	rows, err := inf.Tx.NamedQuery(query, queryValues)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("querying cdn locks: "+err.Error()))
+		return
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var cLock tc.CDNLock
+		if err = rows.Scan(&cLock.UserName, &cLock.CDN, &cLock.Message, &cLock.Soft, &cLock.LastUpdated); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning cdn locks: "+err.Error()))
+			return
+		}
+		cdnLock = append(cdnLock, cLock)
+	}
+
+	api.WriteResp(w, r, cdnLock)
+}
+
+func Create(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+	tx := inf.Tx.Tx
+	if inf.User == nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("couldn't get user for the current request"))
+	}
+	var cdnLock tc.CDNLock
+	if err := json.NewDecoder(r.Body).Decode(&cdnLock); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	if cdnLock.CDN == "" {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("field 'cdn' must be present"), nil)
+		return
+	}
+	// by default, always create soft (or shared) locks
+	if cdnLock.Soft == nil {

Review comment:
       The blueprint listed the `soft` property as required and the database migration restricts it to `NOT NULL` - is there a reason to allow it to be null or undefined?

##########
File path: traffic_ops/traffic_ops_golang/cdn_lock/cdn_lock.go
##########
@@ -0,0 +1,168 @@
+package cdn_lock
+
+/*
+ * 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"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"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"
+)
+
+const readQuery = `SELECT username, cdn, message, soft, last_updated FROM cdn_lock`
+const insertQuery = `INSERT INTO cdn_lock (username, cdn, message, soft) VALUES (:username, :cdn, :message, :soft) RETURNING username, cdn, message, soft, last_updated`
+const deleteQuery = `DELETE FROM cdn_lock WHERE cdn=$1 AND username=$2 RETURNING username, cdn, message, soft, last_updated`
+const deleteAdminQuery = `DELETE FROM cdn_lock WHERE cdn=$1 RETURNING username, cdn, message, soft, last_updated`
+
+func Read(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	cols := map[string]dbhelpers.WhereColumnInfo{
+		"cdn":      {Column: "cdn_lock.cdn", Checker: nil},
+		"username": {Column: "cdn_lock.username", Checker: nil},
+	}
+
+	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, cols)
+	if len(errs) > 0 {
+		errCode = http.StatusBadRequest
+		userErr = util.JoinErrs(errs)
+		api.HandleErr(w, r, tx, errCode, userErr, nil)
+		return
+	}
+
+	cdnLock := []tc.CDNLock{}
+	query := readQuery + where + orderBy + pagination
+	rows, err := inf.Tx.NamedQuery(query, queryValues)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("querying cdn locks: "+err.Error()))
+		return
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var cLock tc.CDNLock
+		if err = rows.Scan(&cLock.UserName, &cLock.CDN, &cLock.Message, &cLock.Soft, &cLock.LastUpdated); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning cdn locks: "+err.Error()))
+			return
+		}
+		cdnLock = append(cdnLock, cLock)
+	}
+
+	api.WriteResp(w, r, cdnLock)
+}
+
+func Create(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+	tx := inf.Tx.Tx
+	if inf.User == nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("couldn't get user for the current request"))
+	}
+	var cdnLock tc.CDNLock
+	if err := json.NewDecoder(r.Body).Decode(&cdnLock); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	if cdnLock.CDN == "" {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("field 'cdn' must be present"), nil)
+		return
+	}
+	// by default, always create soft (or shared) locks
+	if cdnLock.Soft == nil {
+		cdnLock.Soft = util.BoolPtr(true)
+	}
+	cdnLock.UserName = inf.User.UserName
+	resultRows, err := inf.Tx.NamedQuery(insertQuery, cdnLock)
+	if err != nil {
+		userErr, sysErr, errCode := api.ParseDBError(err)
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer resultRows.Close()
+
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err := resultRows.Scan(&cdnLock.UserName, &cdnLock.CDN, &cdnLock.Message, &cdnLock.Soft, &cdnLock.LastUpdated); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("cdn lock create: scanning locks: "+err.Error()))
+			return
+		}
+	}
+	if rowsAffected == 0 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("cdn lock create: lock couldn't be acquired"))
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "CDN lock acquired!")
+	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, cdnLock)
+
+	changeLogMsg := fmt.Sprintf("USER: %s, CDN: %s, ACTION: Lock Acquired", inf.User.UserName, cdnLock.CDN)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func Delete(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"cdn"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	cdn := inf.Params["cdn"]
+	tx := inf.Tx.Tx
+	if inf.User == nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("couldn't get user for the current request"))
+	}
+	var result tc.CDNLock
+	var err error
+	if inf.User.PrivLevel == auth.PrivLevelAdmin {
+		err = inf.Tx.Tx.QueryRow(deleteAdminQuery, cdn).Scan(&result.UserName, &result.CDN, &result.Message, &result.Soft, &result.LastUpdated)
+	} else {
+		err = inf.Tx.Tx.QueryRow(deleteQuery, cdn, inf.User.UserName).Scan(&result.UserName, &result.CDN, &result.Message, &result.Soft, &result.LastUpdated)
+	}
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("deleting cdn lock with cdn name %s: lock not found", cdn), nil)
+			return
+		}
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("deleting cdn lock with cdn name %s : %v", cdn, err.Error()))

Review comment:
       formatting parameters for errors should look like `fmt.Errorf("%w", err)` rather than `fmt.Errorf("%v", err.Error())` - `%w` is a special formatting directive for errors that only `Errorf` provides, which "wraps" the error it formats. Wrapping errors is depended upon by things like `errors.Is`.

##########
File path: traffic_ops/testing/api/v4/cdn_locks_test.go
##########
@@ -0,0 +1,201 @@
+package v4
+
+/*
+
+   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 (
+	"fmt"
+	"net/http"
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
+	client "github.com/apache/trafficcontrol/traffic_ops/v4-client"
+)
+
+func TestCDNLocks(t *testing.T) {
+	WithObjs(t, []TCObj{Tenants, Roles, Users, CDNs}, func() {
+		CRDCdnLocks(t)
+		AdminCdnLocks(t)
+	})
+}
+
+func getCDNName(t *testing.T) string {
+	cdnResp, _, err := TOSession.GetCDNs(client.RequestOptions{})
+	if err != nil {
+		t.Fatalf("couldn't get CDNs: %v", err)
+	}
+	if len(cdnResp.Response) < 1 {
+		t.Fatalf("no valid CDNs in response")
+	}
+	return cdnResp.Response[0].Name
+}
+
+func CRDCdnLocks(t *testing.T) {
+	cdn := getCDNName(t)
+	// CREATE
+	var cdnLock tc.CDNLock
+	cdnLock.CDN = cdn
+	cdnLock.UserName = TOSession.UserName
+	cdnLock.Message = util.StrPtr("snapping cdn")
+	cdnLock.Soft = util.BoolPtr(true)
+	cdnLockResp, _, err := TOSession.CreateCdnLock(cdnLock, client.RequestOptions{})
+	if err != nil {
+		t.Fatalf("couldn't create cdn lock: %v", err)
+	}
+	if cdnLockResp.Response.UserName != cdnLock.UserName {
+		t.Errorf("expected username %v, got %v", cdnLock.UserName, cdnLockResp.Response.UserName)
+	}
+	if cdnLockResp.Response.CDN != cdnLock.CDN {
+		t.Errorf("expected cdn %v, got %v", cdnLock.CDN, cdnLockResp.Response.CDN)
+	}
+	if cdnLockResp.Response.Message == nil {
+		t.Errorf("expected a valid message, but got nothing")
+	}
+	if cdnLockResp.Response.Message != nil && *cdnLockResp.Response.Message != *cdnLock.Message {
+		t.Errorf("expected Message %v, got %v", *cdnLock.Message, *cdnLockResp.Response.Message)
+	}
+	if cdnLockResp.Response.Soft == nil {
+		t.Errorf("expected a valid soft/hard setting, but got nothing")
+	}
+	if cdnLockResp.Response.Soft != nil && *cdnLockResp.Response.Soft != *cdnLock.Soft {
+		t.Errorf("expected 'Soft' to be %v, got %v", *cdnLock.Soft, *cdnLockResp.Response.Soft)
+	}
+
+	// READ
+	cdnLocksReadResp, _, err := TOSession.GetCdnLocks(client.RequestOptions{})
+	if err != nil {
+		t.Fatalf("could not get CDN Locks: %v", err)
+	}
+	if len(cdnLocksReadResp.Response) != 1 {
+		t.Fatalf("expected to get back one CDN lock, but got %d instead", len(cdnLocksReadResp.Response))
+	}
+	if cdnLocksReadResp.Response[0].UserName != cdnLock.UserName {
+		t.Errorf("expected username %v, got %v", cdnLock.UserName, cdnLocksReadResp.Response[0].UserName)
+	}
+	if cdnLocksReadResp.Response[0].CDN != cdnLock.CDN {
+		t.Errorf("expected cdn %v, got %v", cdnLock.CDN, cdnLocksReadResp.Response[0].CDN)
+	}
+	if cdnLocksReadResp.Response[0].Message == nil {
+		t.Errorf("expected a valid message, but got nothing")
+	}
+	if cdnLocksReadResp.Response[0].Message != nil && *cdnLocksReadResp.Response[0].Message != *cdnLock.Message {
+		t.Errorf("expected Message %v, got %v", *cdnLock.Message, *cdnLocksReadResp.Response[0].Message)
+	}
+	if cdnLocksReadResp.Response[0].Soft == nil {
+		t.Errorf("expected a valid soft/hard setting, but got nothing")
+	}
+	if cdnLocksReadResp.Response[0].Soft != nil && *cdnLocksReadResp.Response[0].Soft != *cdnLock.Soft {
+		t.Errorf("expected 'Soft' to be %v, got %v", *cdnLock.Soft, *cdnLocksReadResp.Response[0].Soft)
+	}
+
+	// DELETE
+	_, reqInf, err := TOSession.DeleteCdnLocks(client.RequestOptions{QueryParameters: url.Values{"cdn": []string{cdnLock.CDN}}})
+	if err != nil {
+		t.Fatalf("couldn't delete cdn lock, err: %v", err)
+	}
+	if reqInf.StatusCode != http.StatusOK {
+		t.Errorf("expected status code of 200, but got %d instead", reqInf.StatusCode)
+	}
+
+}
+
+func AdminCdnLocks(t *testing.T) {
+	resp, _, err := TOSession.GetTenants(client.RequestOptions{})
+	if err != nil {
+		t.Fatalf("could not GET tenants: %v", err)
+	}
+	if len(resp.Response) == 0 {
+		t.Fatalf("didn't get any tenant in response")
+	}
+
+	// Create a new user with operations level privileges
+	user1 := tc.User{
+		Username:             util.StrPtr("lock_user1"),
+		RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		RoleName:             util.StrPtr("operations"),
+	}
+	user1.Email = util.StrPtr("lockuseremail@domain.com")
+	user1.TenantID = util.IntPtr(resp.Response[0].ID)
+	user1.FullName = util.StrPtr("firstName LastName")
+	_, _, err = TOSession.CreateUser(user1, client.RequestOptions{})
+	if err != nil {
+		t.Fatalf("could not create test user with username: %s", *user1.Username)
+	}
+	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
+
+	// Create another new user with operations level privileges
+	user2 := tc.User{
+		Username:             util.StrPtr("lock_user2"),
+		RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
+		LocalPassword:        util.StrPtr("test_pa$$word2"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word2"),
+		RoleName:             util.StrPtr("operations"),
+	}
+	user2.Email = util.StrPtr("newlockuseremail@domain.com")
+	user2.TenantID = util.IntPtr(resp.Response[0].ID)
+	user2.FullName = util.StrPtr("firstName2 LastName2")
+	_, _, err = TOSession.CreateUser(user2, client.RequestOptions{})
+	if err != nil {
+		fmt.Println(err)
+		t.Fatalf("could not create test user with username: %s", *user2.Username)
+	}
+	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user2"})
+
+	// Establish a session with the newly created non admin level user
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	if err != nil {
+		t.Fatalf("could not login with user lock_user1: %v", err)
+	}
+
+	// Establish another session with the newly created non admin level user
+	userSession2, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user2.Username, *user2.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	if err != nil {
+		t.Fatalf("could not login with user lock_user1: %v", err)
+	}
+
+	cdn := getCDNName(t)
+	// Create a lock for this user
+	_, _, err = userSession.CreateCdnLock(tc.CDNLock{
+		CDN:     cdn,
+		Message: util.StrPtr("test lock"),
+		Soft:    util.BoolPtr(true),
+	}, client.RequestOptions{})
+	if err != nil {
+		t.Fatalf("couldn't create cdn lock: %v", err)
+	}
+
+	// Non admin user trying to delete another user's lock -> this should fail
+	_, reqInf, err := userSession2.DeleteCdnLocks(client.RequestOptions{QueryParameters: url.Values{"cdn": []string{cdn}}})
+	if err == nil {
+		t.Fatalf("expected error when a non admin user tries to delete another user's lock, but got nothing")
+	}
+	if reqInf.StatusCode != http.StatusNotFound {

Review comment:
       Shouldn't this be a `403 Forbidden` if the user doesn't have permission to delete the lock?

##########
File path: traffic_ops/v4-client/cdn_lock.go
##########
@@ -0,0 +1,48 @@
+package client
+
+/*
+
+   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 (
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops/toclientlib"
+)
+
+// apiCDNLocks is the API version-relative path for the /cdn_locks API endpoint.
+const apiCDNLocks = "/cdn_locks"
+
+// apiAdminCDNLocks is the API version-relative path for the /cdn_locks/admin API endpoint.
+const apiAdminCDNLocks = "/cdn_locks/admin"
+
+// CreateCdnLock creates a CDN Lock.
+func (to *Session) CreateCdnLock(cdnLock tc.CDNLock, opts RequestOptions) (tc.CdnLockCreateResponse, toclientlib.ReqInf, error) {
+	var response tc.CdnLockCreateResponse
+	reqInf, err := to.post(apiCDNLocks, opts, cdnLock, &response)
+	return response, reqInf, err
+}
+
+// GetCdnLocks retrieves the CDN locks based on the passed in parameters.
+func (to *Session) GetCdnLocks(opts RequestOptions) (tc.CdnLocksGetResponse, toclientlib.ReqInf, error) {
+	var data tc.CdnLocksGetResponse
+	reqInf, err := to.get(apiCDNLocks, opts, &data)
+	return data, reqInf, err
+}
+
+// DeleteCdnLocks deletes the CDN lock of a particular(requesting) user.
+func (to *Session) DeleteCdnLocks(opts RequestOptions) (tc.CdnLockDeleteResponse, toclientlib.ReqInf, error) {
+	var data tc.CdnLockDeleteResponse
+	reqInf, err := to.del(apiCDNLocks, opts, &data)
+	return data, reqInf, err
+}

Review comment:
       nit but CDN should be capitalized in these exported symbols, since it's an initialism.




-- 
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.

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