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 <[email protected]>
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},

Reply via email to