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},