cluster/identd/ident: add Query function

This is a high-level wrapper for querying identd, and uses IdentError to
carry errors received from the server.

Change-Id: I6444a67117193b97146ffd1548151cdb234d47b5
diff --git a/cluster/identd/ident/client.go b/cluster/identd/ident/client.go
index c76e867..4d65b28 100644
--- a/cluster/identd/ident/client.go
+++ b/cluster/identd/ident/client.go
@@ -185,3 +185,41 @@
 	}
 	return c.conn.Close()
 }
+
+// Query performs a single ident protocol request to a server running at target
+// and returns the received ident response. If the ident server cannot be
+// queries, or the ident server returns an ident error, an error is returned.
+//
+// This a convenience wrapper around Dial/Do. The given target must be either a
+// host or host:port pair. If not given, the port defaults to 113.
+//
+// Returned ident server error resposes are *IdentError, and can be tested for
+// using errors.Is/errors.As. See the IdentError type documentation for more
+// information.
+//
+// The given context will be used to time out the request, either at the
+// connection or request stage. If the context is canceled/times out, the
+// context error will be returned and the query aborted.
+//
+// Since Query opens a connection to the ident server just for a single query,
+// it should not be used if a single server is going to be queries about
+// multiple addresses, and instead Dial/Do should be used to keep a
+// long-standing connection if possible.
+func Query(ctx context.Context, target string, client, server uint16) (*IdentResponse, error) {
+	cl, err := Dial(target)
+	if err != nil {
+		return nil, fmt.Errorf("could not dial: %w", err)
+	}
+	defer cl.Close()
+	resp, err := cl.Do(ctx, &Request{
+		ClientPort: client,
+		ServerPort: server,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("could not query: %w", err)
+	}
+	if resp.Ident != nil {
+		return resp.Ident, nil
+	}
+	return nil, &IdentError{Inner: resp.Error}
+}