traffic_ops/traffic_ops_golang/api/info.go (240 lines of code) (raw):

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 ( "context" "database/sql" "encoding/json" "errors" "fmt" "net/http" "strings" "time" "github.com/apache/trafficcontrol/v8/lib/go-log" "github.com/apache/trafficcontrol/v8/lib/go-rfc" "github.com/apache/trafficcontrol/v8/lib/go-tc" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/auth" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/config" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/tenant" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/trafficvault" influx "github.com/influxdata/influxdb/client/v2" "github.com/jmoiron/sqlx" ) const createChangeLogQuery = ` INSERT INTO log ( level, message, tm_user ) VALUES ( $1, $2, $3 ) ` const influxServersQuery = ` SELECT (host_name||'.'||domain_name) as fqdn, tcp_port, https_port FROM server WHERE type in ( SELECT id FROM type WHERE name='INFLUXDB' ) AND status=(SELECT id FROM status WHERE name='ONLINE') ` // Info structures contain all of the information an API route handler needs to // be able to service a request, including some things that are pre-parsed (e.g. // query string parameters) for you. It also provides some methods for // accomplishing common tasks. type Info struct { // Params is a mapping of all query string and path parameters to their // respective values. The behavior of this map is not defined when any two // query string parameters and/or path parameters share a name. For example, // if the route is `cdns/{id}/delivery_services/{id}`, the two cannot be // distinguished. Similarly, a request like `GET /api/5.0/cdns?id=1&id=2` // will give either an "id" key that maps to "1", or an "id" key that maps // to "2". Most convolutedly, for the aforementioned route definition, the // request `GET cdns/1/deliveryservices/2?id=3&id=4` gives four possible // values for the "id" key. Take care when constructing routes and deciding // the parameters they will accept. Params map[string]string // IntParams is a mapping of all of the declared parameters that are to be // parsed as ints to the parsed values of those parameters. No key will // appear here that isn't also in Params. IntParams map[string]int // The currently authenticated user - this may be `nil` on routes that do // not require authentication. User *auth.CurrentUser // A unique identifier for the request. ReqID uint64 // The version of the API being requested. This is a pointer for legacy // reasons - all handlers should assume this is not nil (with the possible // exception of plugin handlers). Version *Version // A transaction opened to the Traffic Ops database. Tx *sqlx.Tx // The cancel function for the request and transaction contexts. CancelTx context.CancelFunc // The Traffic Vault implementation. Vault trafficvault.TrafficVault // Config is the Traffic Ops server's current configuration. This is a // pointer presumably to save memory; it should and must never be `nil`. Config *config.Config request *http.Request w http.ResponseWriter } // NewInfo get and returns the context info needed by handlers. It also returns // any user error, any system error, and the status code which should be // returned to the client if an error occurred. // // It is encouraged to call Info.Tx.Tx.Commit() manually when all queries are // finished, to release database resources early, and also to return an error to // the user if the commit failed. In practice, though, the `Close` method // handles this in nearly every case. // // NewInfo guarantees the returned Info.Tx is non-`nil` and Info.Tx.Tx is `nil` // or valid, even if a returned error is not `nil`. Hence, it is safe to pass // the Tx.Tx to HandleErr when this returns errors. // // Close() must be called to free resources, and should be called in a defer // immediately after NewInfo(), to finish the transaction. // // Example: // // func handler(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() // // respObj, err := finalDatabaseOperation(inf.Tx) // if err != nil { // api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("final db op: %w", err)) // return // } // if err := inf.Tx.Tx.Commit(); err != nil { // api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("committing transaction: %w", err)) // return // } // api.WriteResp(w, r, respObj) // } func NewInfo(r *http.Request, requiredParams []string, intParamNames []string) (*Info, error, error, int) { db, err := GetDB(r.Context()) if err != nil { return &Info{Tx: &sqlx.Tx{}}, fmt.Errorf("getting db: %w", err), nil, http.StatusInternalServerError } cfg, err := GetConfig(r.Context()) if err != nil { return &Info{Tx: &sqlx.Tx{}}, fmt.Errorf("getting config: %w", err), nil, http.StatusInternalServerError } tv, err := GetTrafficVault(r.Context()) if err != nil { return &Info{Tx: &sqlx.Tx{}}, fmt.Errorf("getting TrafficVault: %w", err), nil, http.StatusInternalServerError } reqID, err := getReqID(r.Context()) if err != nil { return &Info{Tx: &sqlx.Tx{}}, fmt.Errorf("getting reqID: %w", err), nil, http.StatusInternalServerError } version := GetRequestedAPIVersion(r.URL.Path) user, err := auth.GetCurrentUser(r.Context()) if err != nil { return &Info{Tx: &sqlx.Tx{}}, fmt.Errorf("getting user: %w", err), nil, http.StatusInternalServerError } params, intParams, userErr, sysErr, errCode := AllParams(r, requiredParams, intParamNames) if userErr != nil || sysErr != nil { return &Info{Tx: &sqlx.Tx{}}, userErr, sysErr, errCode } // only place we could call cancel here is in Info.Close(), which already // will rollback the transaction (which is all cancel will do.) // must be last, MUST not return an error if this succeeds, without closing // the tx dbCtx, cancelTx := context.WithTimeout(r.Context(), time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second) tx, err := db.BeginTxx(dbCtx, nil) if err != nil { return &Info{Tx: &sqlx.Tx{}, CancelTx: cancelTx}, userErr, fmt.Errorf("could not begin transaction: %w", err), http.StatusInternalServerError } return &Info{ Config: cfg, ReqID: reqID, Version: version, Params: params, IntParams: intParams, User: user, Tx: tx, CancelTx: cancelTx, Vault: tv, request: r, }, nil, nil, http.StatusOK } // CheckPrecondition checks a request's "preconditions" - its If-Match and // If-Unmodified-Since headers versus the last updated time of the requested // object(s), and returns (in order), an HTTP response code appropriate for the // precondition check results, a user-safe error that should be returned to // clients, and a server-side error that should be logged. // Callers must pass in a query that will return one row containing one column // that is the representative date/time of the last update of the requested // object(s), and optionally any values for placeholder arguments in the query. func (inf Info) CheckPrecondition(query string, args ...interface{}) (int, error, error) { if inf.request == nil { return http.StatusInternalServerError, nil, NilRequestError } ius := inf.request.Header.Get(rfc.IfUnmodifiedSince) etag := inf.request.Header.Get(rfc.IfMatch) if ius == "" && etag == "" { return http.StatusOK, nil, nil } if inf.Tx == nil || inf.Tx.Tx == nil { return http.StatusInternalServerError, nil, NilTransactionError } var lastUpdated time.Time if err := inf.Tx.Tx.QueryRow(query, args...).Scan(&lastUpdated); err != nil { return http.StatusInternalServerError, nil, fmt.Errorf("scanning for lastUpdated: %w", err) } if etag != "" { if et, ok := rfc.ParseETags(strings.Split(etag, ",")); ok { if lastUpdated.After(et) { return http.StatusPreconditionFailed, ResourceModifiedError, nil } } } if ius == "" { return http.StatusOK, nil, nil } if tm, ok := rfc.ParseHTTPDate(ius); ok { if lastUpdated.After(tm) { return http.StatusPreconditionFailed, ResourceModifiedError, nil } } return http.StatusOK, nil, nil } // Close implements the io.Closer interface. It should be called in a defer immediately after NewInfo(). // // Close will commit the transaction, if it hasn't been rolled back. func (inf *Info) Close() { defer inf.CancelTx() if err := inf.Tx.Tx.Commit(); err != nil && !errors.Is(err, sql.ErrTxDone) { log.Errorln("committing transaction: " + err.Error()) } } // WriteOKResponse writes a 200 OK response with the given object as the // 'response' property of the response body. // // This CANNOT be used by any Info that wasn't constructed for the caller by // Wrap - ing a Handler (yet). func (inf Info) WriteOKResponse(resp any) (int, error, error) { WriteResp(inf.w, inf.request, resp) return http.StatusOK, nil, nil } // WriteOKResponseWithSummary writes a 200 OK response with the given object as // the 'response' property of the response body, and the given count as the // `count` property of the response's summary. // // This CANNOT be used by any Info that wasn't constructed for the caller by // Wrap - ing a Handler (yet). // // Deprecated: Summary sections on responses were intended to cover up for a // deficiency in jQuery-based tables on the front-end, so now that we aren't // using those anymore it serves no purpose. func (inf Info) WriteOKResponseWithSummary(resp any, count uint64) (int, error, error) { WriteRespWithSummary(inf.w, inf.request, resp, count) return http.StatusOK, nil, nil } // WriteNotModifiedResponse writes a 304 Not Modified response with the given // time as the last modified time in the headers. // // This CANNOT be used by any Info that wasn't constructed for the caller by // Wrap - ing a Handler (yet). func (inf Info) WriteNotModifiedResponse(lastModified time.Time) (int, error, error) { inf.w.Header().Set(rfc.LastModified, FormatLastModified(lastModified)) inf.w.WriteHeader(http.StatusNotModified) setRespWritten(inf.request) return http.StatusNotModified, nil, nil } // WriteSuccessResponse writes the given response object as the `response` // property of the response body, with the accompanying message as a // success-level Alert. func (inf Info) WriteSuccessResponse(resp any, message string) (int, error, error) { WriteAlertsObj(inf.w, inf.request, http.StatusOK, tc.CreateAlerts(tc.SuccessLevel, message), resp) return http.StatusOK, nil, nil } // WriteCreatedResponse writes the given response object as the `response` // property of the response body of a 201 created response, with the // accompanying message as a success-level Alert. It also sets the Location // header to the given path. This will be automatically prefaced with the // correct path to the API version the client requested. func (inf Info) WriteCreatedResponse(resp any, message, path string) (int, error, error) { inf.w.Header().Set(rfc.Location, strings.Join([]string{"/api", inf.Version.String(), strings.TrimPrefix(path, "/")}, "/")) inf.w.WriteHeader(http.StatusCreated) WriteAlertsObj(inf.w, inf.request, http.StatusCreated, tc.CreateAlerts(tc.SuccessLevel, message), resp) return http.StatusCreated, nil, nil } // RequestHeaders returns the headers sent by the client in the API request. func (inf Info) RequestHeaders() http.Header { return inf.request.Header } // SetLastModified sets the "last modified" header on the response writer. // // This CANNOT be used by any Info that wasn't constructed for the caller by // Wrap - ing a Handler (yet). func (inf Info) SetLastModified(t time.Time) { inf.w.Header().Set(rfc.LastModified, FormatLastModified(t)) } // DecodeBody reads the client request's body and attempts to decode it into the // provided reference. func (inf Info) DecodeBody(ref any) error { return json.NewDecoder(inf.request.Body).Decode(ref) } // SendMail is a convenience method used to call SendMail using an Info // structure's configuration. func (inf *Info) SendMail(to rfc.EmailAddress, msg []byte) (int, error, error) { return SendMail(to, msg, inf.Config) } // IsResourceAuthorizedToCurrentUser is a convenience method used to call // github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/tenant.IsResourceAuthorizedToUserTx // using an Info structure to provide the current user and database transaction. func (inf *Info) IsResourceAuthorizedToCurrentUser(resourceTenantID int) (bool, error) { return tenant.IsResourceAuthorizedToUserTx(resourceTenantID, inf.User, inf.Tx.Tx) } // CreateChangeLog creates a new changelog message at the APICHANGE level for // the current user. func (inf Info) CreateChangeLog(msg string) { _, err := inf.Tx.Tx.Exec(createChangeLogQuery, ApiChange, msg, inf.User.ID) if err != nil { log.Errorf("Inserting chage log level '%s' message '%s' for user '%s': %v", ApiChange, msg, inf.User.UserName, err) } } // UseIMS returns whether or not If-Modified-Since constraints should be used to // service the given request. func (inf Info) UseIMS() bool { if inf.request == nil || inf.Config == nil { return false } return inf.Config.UseIMS && inf.request.Header.Get(rfc.IfModifiedSince) != "" } // CreateInfluxClient constructs and returns an InfluxDB HTTP client, if enabled // and when possible. The error this returns should not be exposed to the user; // it's for logging purposes only. // // If Influx connections are not enabled, this will return `nil` - but also no // error. It is expected that the caller will handle this situation // appropriately. func (inf *Info) CreateInfluxClient() (*influx.Client, error) { if !inf.Config.InfluxEnabled || inf.Config.ConfigInflux == nil { return nil, nil } var fqdn string var tcpPort uint var httpsPort sql.NullInt64 // this is the only one that's optional row := inf.Tx.Tx.QueryRow(influxServersQuery) if e := row.Scan(&fqdn, &tcpPort, &httpsPort); e != nil { return nil, fmt.Errorf("failed to create influx client: %w", e) } host := "http%s://%s:%d" if inf.Config.ConfigInflux.Secure != nil && *inf.Config.ConfigInflux.Secure { if !httpsPort.Valid { log.Warnf("INFLUXDB Server %s has no secure ports, assuming default of 8086!", fqdn) httpsPort = sql.NullInt64{Int64: 8086, Valid: true} } p := httpsPort.Int64 if p <= 0 || p > 65535 { log.Warnf("INFLUXDB Server %s has invalid port, assuming default of 8086!", fqdn) p = 8086 } host = fmt.Sprintf(host, "s", fqdn, p) } else if tcpPort > 0 && tcpPort <= 65535 { host = fmt.Sprintf(host, "", fqdn, tcpPort) } else { log.Warnf("INFLUXDB Server %s has invalid port, assuming default of 8086!", fqdn) host = fmt.Sprintf(host, "", fqdn, 8086) } config := influx.HTTPConfig{ Addr: host, Username: inf.Config.ConfigInflux.User, Password: inf.Config.ConfigInflux.Password, UserAgent: fmt.Sprintf("TrafficOps/%s (Go)", inf.Config.Version), Timeout: time.Duration(float64(inf.Config.ReadTimeout)/2.1) * time.Second, } var client influx.Client client, e := influx.NewHTTPClient(config) if e != nil { return nil, fmt.Errorf("failed to create influx client: %w", e) } if client == nil { return nil, errors.New("failed to create influx client: client was nil") } return &client, e } // DefaultSort sets the `orderby` query string parameter to the given value, as // though the client had set it, should it be missing. func (inf Info) DefaultSort(param string) { if _, ok := inf.Params["orderby"]; !ok { inf.Params["orderby"] = param } }