You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2021/03/01 20:39:52 UTC
[trafficcontrol] branch master updated: added status endpoint for
async tasks (#5544)
This is an automated email from the ASF dual-hosted git repository.
ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new 04e0b03 added status endpoint for async tasks (#5544)
04e0b03 is described below
commit 04e0b03ae3320069a7a5fa0ab2ac829250db7ffe
Author: mattjackson220 <33...@users.noreply.github.com>
AuthorDate: Mon Mar 1 13:39:41 2021 -0700
added status endpoint for async tasks (#5544)
* added status endpoint for async tasks
* updated changelog
* cleaned up err.Error calls
* updated per comments and added tests
* updated per comment
---
CHANGELOG.md | 1 +
docs/source/api/v4/acme_autorenew.rst | 4 +-
docs/source/api/v4/async_status.rst | 63 ++++++++++
.../2021022300000000_add_async_status_table.sql | 43 +++++++
traffic_ops/traffic_ops_golang/api/async_status.go | 136 +++++++++++++++++++++
.../traffic_ops_golang/api/async_status_test.go | 114 +++++++++++++++++
.../deliveryservice/autorenewcerts.go | 58 ++++++++-
.../deliveryservice/letsencryptcert.go | 4 +-
traffic_ops/traffic_ops_golang/routing/routes.go | 1 +
9 files changed, 415 insertions(+), 9 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 849c009..b2e5f5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Added license files to the RPMs
- Added ACME certificate renewals and ACME account registration using external account binding
- Added functionality to automatically renew ACME certificates.
+- Added an endpoint for statuses on asynchronous jobs and applied it to the ACME renewal endpoint.
### Fixed
- [#5558](https://github.com/apache/trafficcontrol/issues/5558) - Fixed `TM UI` and `/api/cache-statuses` to report aggregate `bandwidth_kbps` correctly.
diff --git a/docs/source/api/v4/acme_autorenew.rst b/docs/source/api/v4/acme_autorenew.rst
index bf65a0a..249b803 100644
--- a/docs/source/api/v4/acme_autorenew.rst
+++ b/docs/source/api/v4/acme_autorenew.rst
@@ -13,7 +13,7 @@
.. limitations under the License.
..
-.. _to-api-acnme-autorenew:
+.. _to-api-acme-autorenew:
******************
``acme_autorenew``
@@ -43,7 +43,7 @@ Response Structure
{ "alerts": [
{
- "text": "Beginning async call to renew certificates. This may take a few minutes.",
+ "text": "Beginning async call to renew certificates. This may take a few minutes. Status updates can be found here: /api/4.0/async_status/1",
"level": "success"
}
]}
diff --git a/docs/source/api/v4/async_status.rst b/docs/source/api/v4/async_status.rst
new file mode 100644
index 0000000..6744cff
--- /dev/null
+++ b/docs/source/api/v4/async_status.rst
@@ -0,0 +1,63 @@
+..
+..
+.. 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.
+..
+
+.. _to-api-async_status:
+
+***********************
+``async_status/{{id}}``
+***********************
+
+``GET``
+=======
+Returns a status update for an asynchronous task.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Response Type: Object
+
+Request Structure
+-----------------
+.. table:: Request Path Parameters
+
+ +------+----------+--------------------------------------------------------------------------------------------------------------------------------------+
+ | Name | Required | Description |
+ +======+==========+======================================================================================================================================+
+ | id | yes | The integral, unique identifier for the desired asynchronous job status. This will be provided when the asynchronous job is started. |
+ +------+----------+--------------------------------------------------------------------------------------------------------------------------------------+
+
+
+Response Structure
+------------------
+:id: The integral, unique identifier for the asynchronous job status.
+:status: The status of the asynchronous job. This will be `PENDING`, `SUCCEEDED`, or `FAILED`.
+:start_time: The time the asynchronous job was started.
+:end_time: The time the asynchronous job completed. This will be `null` if it has not completed yet.
+:message: A message about the job status.
+
+.. code-block:: http
+ :caption: Response Example
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+
+ { "response":
+ {
+ "id":1,
+ "status":"PENDING",
+ "start_time":"2021-02-18T17:13:56.352261Z",
+ "end_time":null,
+ "message":"Async job has started."
+ }
+ }
diff --git a/traffic_ops/app/db/migrations/2021022300000000_add_async_status_table.sql b/traffic_ops/app/db/migrations/2021022300000000_add_async_status_table.sql
new file mode 100644
index 0000000..005dda0
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2021022300000000_add_async_status_table.sql
@@ -0,0 +1,43 @@
+/*
+
+ 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 SEQUENCE IF NOT EXISTS async_status_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+CREATE TABLE IF NOT EXISTS async_status (
+ id bigint NOT NULL DEFAULT nextval('async_status_id_seq'::regclass),
+ status TEXT NOT NULL,
+ message TEXT,
+ start_time timestamp with time zone DEFAULT now() NOT NULL,
+ end_time timestamp with time zone,
+
+ PRIMARY KEY (id)
+);
+
+ALTER SEQUENCE async_status_id_seq OWNED BY async_status.id;
+
+
+-- +goose Down
+-- SQL section 'Down' is executed when this migration is rolled back
+
+DROP TABLE IF EXISTS async_status;
+DROP SEQUENCE IF EXISTS async_status_id_seq;
diff --git a/traffic_ops/traffic_ops_golang/api/async_status.go b/traffic_ops/traffic_ops_golang/api/async_status.go
new file mode 100644
index 0000000..eb55879
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/api/async_status.go
@@ -0,0 +1,136 @@
+package api
+
+/*
+ * 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"
+ "net/http"
+ "time"
+
+ "github.com/jmoiron/sqlx"
+)
+
+const (
+ AsyncSucceeded = "SUCCEEDED"
+ AsyncFailed = "FAILED"
+ AsyncPending = "PENDING"
+)
+
+const CurrentAsyncEndpoint = "/api/4.0/async_status/"
+
+type AsyncStatus struct {
+ Id int `json:"id, omitempty" db:"id"`
+ Status string `json:"status, omitempty" db:"status"`
+ StartTime time.Time `json:"start_time, omitempty" db:"start_time"`
+ EndTime *time.Time `json:"end_time, omitempty" db:"end_time"`
+ Message *string `json:"message, omitempty" db:"message"`
+}
+
+const selectAsyncStatusQuery = `SELECT id, status, message, start_time, end_time from async_status WHERE id = $1`
+const insertAsyncStatusQuery = `INSERT INTO async_status (status, message) VALUES ($1, $2) RETURNING id`
+const updateAsyncStatusEndTimeQuery = `UPDATE async_status SET status = $1, message = $2, end_time = now() WHERE id = $3`
+const updateAsyncStatusQuery = `UPDATE async_status SET status = $1, message = $2 WHERE id = $3`
+
+// GetAsyncStatus returns the status of an asynchronous job.
+func GetAsyncStatus(w http.ResponseWriter, r *http.Request) {
+ inf, userErr, sysErr, errCode := NewInfo(r, []string{"id"}, []string{"id"})
+ if userErr != nil || sysErr != nil {
+ HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+ return
+ }
+ defer inf.Close()
+
+ asyncStatusId := inf.Params["id"]
+
+ rows, err := inf.Tx.Tx.Query(selectAsyncStatusQuery, asyncStatusId)
+ if err != nil {
+ HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+ return
+ }
+ defer rows.Close()
+
+ asyncStatus := AsyncStatus{}
+ rowCount := 0
+ for rows.Next() {
+ rowCount++
+ err := rows.Scan(&asyncStatus.Id, &asyncStatus.Status, &asyncStatus.Message, &asyncStatus.StartTime, &asyncStatus.EndTime)
+ if err != nil {
+ HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+ return
+ }
+ }
+
+ if rowCount == 0 {
+ HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, nil, errors.New("async status not found"))
+ return
+ }
+
+ WriteResp(w, r, asyncStatus)
+}
+
+// InsertAsyncStatus inserts a new status for an asynchronous job.
+func InsertAsyncStatus(tx *sql.Tx, message string) (int, int, error, error) {
+ defer tx.Commit()
+
+ resultRows, err := tx.Query(insertAsyncStatusQuery, AsyncPending, message)
+ if err != nil {
+ userErr, sysErr, errCode := ParseDBError(err)
+ return 0, errCode, userErr, sysErr
+ }
+ defer resultRows.Close()
+
+ var asyncStatusId int
+
+ rowsAffected := 0
+ for resultRows.Next() {
+ rowsAffected++
+ if err := resultRows.Scan(&asyncStatusId); err != nil {
+ return 0, http.StatusInternalServerError, nil, err
+ }
+ }
+ if rowsAffected == 0 {
+ return 0, http.StatusInternalServerError, nil, errors.New("async status create: no status was inserted, no id was returned")
+ } else if rowsAffected > 1 {
+ return 0, http.StatusInternalServerError, nil, errors.New("too many ids returned from async status insert")
+ }
+
+ return asyncStatusId, http.StatusOK, nil, nil
+}
+
+// UpdateAsyncStatus updates the status table for an asynchronous job.
+func UpdateAsyncStatus(db *sqlx.DB, newStatus string, newMessage string, asyncStatusId int, finished bool) error {
+ tx, err := db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Commit()
+
+ q := updateAsyncStatusQuery
+ if finished {
+ q = updateAsyncStatusEndTimeQuery
+ }
+ _, err = tx.Exec(q, newStatus, newMessage, asyncStatusId)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/traffic_ops/traffic_ops_golang/api/async_status_test.go b/traffic_ops/traffic_ops_golang/api/async_status_test.go
new file mode 100644
index 0000000..e2c269d
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/api/async_status_test.go
@@ -0,0 +1,114 @@
+package api
+
+/*
+ * 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 (
+ "net/http"
+ "testing"
+
+ "github.com/jmoiron/sqlx"
+
+ "gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func TestInsertAsyncStatus(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)
+ return
+ }
+ defer mockDB.Close()
+
+ db := sqlx.NewDb(mockDB, "sqlmock")
+ defer db.Close()
+
+ expectedMessage := "test async message"
+ mock.ExpectBegin()
+ rows := sqlmock.NewRows([]string{"id"})
+ rows.AddRow(1)
+ mock.ExpectQuery("INSERT").WithArgs(AsyncPending, expectedMessage).WillReturnRows(rows)
+
+ asyncId, errCode, userErr, sysErr := InsertAsyncStatus(db.MustBegin().Tx, expectedMessage)
+
+ if userErr != nil {
+ t.Fatalf("userError was expected to be nil but got %v", userErr)
+ return
+ }
+ if sysErr != nil {
+ t.Fatalf("sysErr was expected to be nil but got %v", sysErr)
+ return
+ }
+ if errCode != http.StatusOK {
+ t.Fatalf("errCode was expected to be %v but got %v", http.StatusOK, errCode)
+ return
+ }
+ if asyncId != 1 {
+ t.Fatalf("asyncId was expected to be 1 but got %v", asyncId)
+ return
+ }
+}
+
+func TestUpdateAsyncStatus(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)
+ return
+ }
+ defer mockDB.Close()
+
+ db := sqlx.NewDb(mockDB, "sqlmock")
+ defer db.Close()
+
+ expectedMessage := "test updated async message"
+ expectedStatus := AsyncPending
+ mock.ExpectBegin()
+ mock.ExpectExec("UPDATE").WithArgs(expectedStatus, expectedMessage, 1).WillReturnResult(sqlmock.NewResult(1, 1))
+
+ updateErr := UpdateAsyncStatus(db, expectedStatus, expectedMessage, 1, false)
+
+ if updateErr != nil {
+ t.Fatalf("updateErr was expected to be nil but got %v", updateErr)
+ return
+ }
+}
+
+func TestUpdateAsyncStatusFinished(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)
+ return
+ }
+ defer mockDB.Close()
+
+ db := sqlx.NewDb(mockDB, "sqlmock")
+ defer db.Close()
+
+ expectedMessage := "test job complete"
+ expectedStatus := AsyncSucceeded
+ mock.ExpectBegin()
+ mock.ExpectExec("UPDATE").WithArgs(expectedStatus, expectedMessage, 1).WillReturnResult(sqlmock.NewResult(1, 1))
+
+ updateErr := UpdateAsyncStatus(db, expectedStatus, expectedMessage, 1, true)
+
+ if updateErr != nil {
+ t.Fatalf("updateErr was expected to be nil but got %v", updateErr)
+ return
+ }
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go b/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
index cea7e55..d04683a 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
@@ -104,7 +104,12 @@ func renewCertificates(w http.ResponseWriter, r *http.Request, deprecated bool)
ctx, _ := context.WithTimeout(r.Context(), LetsEncryptTimeout*time.Duration(len(existingCerts)))
- go RunAutorenewal(existingCerts, inf.Config, ctx, inf.User)
+ asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(inf.Tx.Tx, "ACME async job has started.")
+ if userErr != nil || sysErr != nil {
+ api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+ }
+
+ go RunAutorenewal(existingCerts, inf.Config, ctx, inf.User, asyncStatusId)
var alerts tc.Alerts
if deprecated {
@@ -112,33 +117,47 @@ func renewCertificates(w http.ResponseWriter, r *http.Request, deprecated bool)
}
alerts.AddAlert(tc.Alert{
- Text: "Beginning async call to renew certificates. This may take a few minutes.",
+ Text: "Beginning async call to renew certificates. This may take a few minutes. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
Level: tc.SuccessLevel.String(),
})
+
+ w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
api.WriteAlerts(w, r, http.StatusAccepted, alerts)
}
-func RunAutorenewal(existingCerts []ExistingCerts, cfg *config.Config, ctx context.Context, currentUser *auth.CurrentUser) {
+func RunAutorenewal(existingCerts []ExistingCerts, cfg *config.Config, ctx context.Context, currentUser *auth.CurrentUser, asyncStatusId int) {
db, err := api.GetDB(ctx)
if err != nil {
log.Errorf("Error getting db: %s", err.Error())
+ if err = api.UpdateAsyncStatus(db, api.AsyncFailed, "ACME renewal failed.", asyncStatusId, true); err != nil {
+ log.Errorf("updating async status for id %v: %v", asyncStatusId, err)
+ }
return
}
tx, err := db.Begin()
if err != nil {
log.Errorf("Error getting tx: %s", err.Error())
+ if err = api.UpdateAsyncStatus(db, api.AsyncFailed, "ACME renewal failed.", asyncStatusId, true); err != nil {
+ log.Errorf("updating async status for id %v: %v", asyncStatusId, err)
+ }
return
}
logTx, err := db.Begin()
if err != nil {
log.Errorf("Error getting logTx: %s", err.Error())
+ if err = api.UpdateAsyncStatus(db, api.AsyncFailed, "ACME renewal failed.", asyncStatusId, true); err != nil {
+ log.Errorf("updating async status for id %v: %v", asyncStatusId, err)
+ }
return
}
defer logTx.Commit()
keysFound := ExpirationSummary{}
+ renewedCount := 0
+ errorCount := 0
+
for _, ds := range existingCerts {
if !ds.Version.Valid || ds.Version.Int64 == 0 {
continue
@@ -166,13 +185,21 @@ func RunAutorenewal(existingCerts []ExistingCerts, cfg *config.Config, ctx conte
err = base64DecodeCertificate(&keyObj.Certificate)
if err != nil {
log.Errorf("cert autorenewal: error getting SSL keys for XMLID '%s': %s", ds.XmlId, err.Error())
- return
+ dsExpInfo.XmlId = ds.XmlId
+ dsExpInfo.Version = util.JSONIntStr(int(ds.Version.Int64))
+ dsExpInfo.Error = errors.New("decoding the certificate for xmlId: " + ds.XmlId + " and version: " + strconv.Itoa(int(ds.Version.Int64)))
+ keysFound.OtherExpirations = append(keysFound.OtherExpirations, dsExpInfo)
+ continue
}
expiration, err := parseExpirationFromCert([]byte(keyObj.Certificate.Crt))
if err != nil {
log.Errorf("cert autorenewal: %s: %s", ds.XmlId, err.Error())
- return
+ dsExpInfo.XmlId = ds.XmlId
+ dsExpInfo.Version = util.JSONIntStr(int(ds.Version.Int64))
+ dsExpInfo.Error = errors.New("parsing the expiration for xmlId: " + ds.XmlId + " and version: " + strconv.Itoa(int(ds.Version.Int64)))
+ keysFound.OtherExpirations = append(keysFound.OtherExpirations, dsExpInfo)
+ continue
}
// Renew only certificates within configured limit. Default is 30 days.
@@ -204,6 +231,9 @@ func RunAutorenewal(existingCerts []ExistingCerts, cfg *config.Config, ctx conte
if error := GetLetsEncryptCertificates(cfg, req, ctx, currentUser); error != nil {
dsExpInfo.Error = error
+ errorCount++
+ } else {
+ renewedCount++
}
keysFound.LetsEncryptExpirations = append(keysFound.LetsEncryptExpirations, dsExpInfo)
@@ -216,17 +246,35 @@ func RunAutorenewal(existingCerts []ExistingCerts, cfg *config.Config, ctx conte
} else {
userErr, sysErr, statusCode := renewAcmeCerts(cfg, keyObj.DeliveryService, ctx, currentUser)
if userErr != nil {
+ errorCount++
dsExpInfo.Error = userErr
} else if sysErr != nil {
+ errorCount++
dsExpInfo.Error = sysErr
} else if statusCode != http.StatusOK {
+ errorCount++
dsExpInfo.Error = errors.New("Status code not 200: " + strconv.Itoa(statusCode))
+ } else {
+ renewedCount++
}
keysFound.AcmeExpirations = append(keysFound.AcmeExpirations, dsExpInfo)
}
}
+ if err = api.UpdateAsyncStatus(db, api.AsyncPending, "ACME renewal in progress. "+strconv.Itoa(renewedCount)+" certs renewed, "+strconv.Itoa(errorCount)+" errors.", asyncStatusId, false); err != nil {
+ log.Errorf("updating async status for id %v: %v", asyncStatusId, err)
+ }
+
+ }
+
+ // put status as succeeded if any certs were successfully renewed
+ asyncStatus := api.AsyncSucceeded
+ if errorCount > 0 && renewedCount == 0 {
+ asyncStatus = api.AsyncFailed
+ }
+ if err = api.UpdateAsyncStatus(db, asyncStatus, "ACME renewal complete. "+strconv.Itoa(renewedCount)+" certs renewed, "+strconv.Itoa(errorCount)+" errors.", asyncStatusId, true); err != nil {
+ log.Errorf("updating async status for id %v: %v", asyncStatusId, err)
}
if cfg.SMTP.Enabled && cfg.ConfigAcmeRenewal.SummaryEmail != "" {
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go b/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go
index a062ef8..37bb781 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go
@@ -121,8 +121,8 @@ func (d *DNSProviderTrafficRouter) CleanUp(domain, token, keyAuth string) error
return errors.New("Determining rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
}
if rows == 0 {
- log.Errorf("Zero rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
- return errors.New("Zero rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+ log.Errorf("Zero rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value)
+ return errors.New("Zero rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value)
}
}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 1adaab7..f7794b7 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -141,6 +141,7 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
//Delivery service ACME
{api.Version{4, 0}, http.MethodPost, `deliveryservices/xmlId/{xmlid}/sslkeys/renew$`, deliveryservice.RenewAcmeCertificate, auth.PrivLevelOperations, Authenticated, nil, 2534390573},
{api.Version{4, 0}, http.MethodPost, `acme_autorenew/?$`, deliveryservice.RenewCertificates, auth.PrivLevelOperations, Authenticated, nil, 2534390574},
+ {api.Version{4, 0}, http.MethodGet, `async_status/{id}$`, api.GetAsyncStatus, auth.PrivLevelOperations, Authenticated, nil, 2534390575},
// API Capability
{api.Version{4, 0}, http.MethodGet, `api_capabilities/?$`, apicapability.GetAPICapabilitiesHandler, auth.PrivLevelReadOnly, Authenticated, nil, 48132065893},