Go gRPC server make integration tests fail - testing

I recently started to play again with go, and my task at the moment is to implement a service (that will run on k8s) that should act as a gRPC server.
As per requirements, at the moment, to satisfy the readinessProbe on k8s, I need to implement an healthcheck endpoint for my service, and this should be tested: what I've done is:
func main() {
server := startHTTPServer()
defer server.Close()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
<-c
log.Println("Stopped")
}
func startHTTPServer() *http.Server {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
server := &http.Server{
Addr: net.JoinHostPort("", strconv.Itoa(livelinessPort)),
}
go server.ListenAndServe()
return server
}
and I tested in this way, following this article:
func TestMain(m *testing.M) {
flag.Parse()
wg := setup()
result := m.Run()
shutdown(wg)
os.Exit(result)
}
func shutdown(wg *sync.WaitGroup) {
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
wg.Wait()
}
func setup() *sync.WaitGroup{
os.Setenv("APP_ENV", EnvTest)
wg := &sync.WaitGroup{}
startMainProcess(wg)
return wg
}
func startMainProcess(wg *sync.WaitGroup) {
go func() {
wg.Add(1)
defer wg.Done()
main()
}()
}
func TestK8SHealth(t *testing.T) {
res, err := http.Get("http://:8080/")
if err != nil {
t.Errorf("Unexpected API error: %s", err)
return
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Errorf("Unexpected status code: %d", res.StatusCode)
return
}
}
and so far so good:
$ go test
PASS
2018/08/13 09:23:16 Stopped
ok github.com/... 0.015s
Te problem is when I try to add the gRPC server in the main application, following gPRC go examples. I've added this to my main file
func main() {
[...]
startChecksServiceServer()
[...]
}
func startChecksServiceServer() *grpc.Server {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", serverPort))
if err != nil {
log.Fatalf("Starting gPRC server error: failed to listen port %d: %v", serverPort, err)
}
grpcServer := grpc.NewServer()
pb.RegisterChecksServiceServer(grpcServer, &checksServiceServer{})
grpcServer.Serve(lis)
log.Println("Started grpc server")
return grpcServer
}
but now when I run the test, it fails after the actual test was run, when the interrupt signal is sent.
$ go test
PASS
signal: interrupt
FAIL github.com/... 0.016s
I made some tests and this is not depending by the actual test (as it pass with a test function that just return, and it's working while running the test suite using goLand:
GOROOT=/usr/local/go #gosetup
GOPATH=/Users/marco/Documents/projects/go #gosetup
/usr/local/go/bin/go test -c -i -o /private/var/folders/jh/znhv9f090yb7y390dckb23s00000gn/T/[...] #gosetup
/usr/local/go/bin/go tool test2json -t /private/var/folders/jh/znhv9f090yb7y390dckb23s00000gn/T/[...] -test.v -test.run
^TestK8SHealth$ #gosetup
=== RUN TestK8SHealth
--- PASS: TestK8SHealth (0.00s)
PASS
Process finished with exit code 1
--- EDIT
I found that the problem is around the listener. Having in the main function:
func main() {
net.Listen("tcp", fmt.Sprintf(":%d", serverPort))
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
<-c
log.Println("Stopped")
}
and a test with just a return (with the same setup of the previous one) will cause the test to fail:
$ go test -v
=== RUN Test2
--- PASS: Test2 (0.00s)
PASS
signal: interrupt
FAIL github.com/[...] 0.014s

grpcServer.Serve(lis) is a blocking call. So like what you did with HTTP server go server.ListenAndServe(), you need to run it in a goroutine. Could you please try if go grpcServer.Serve(lis) will make the test pass?
I am not sure about your new EDIT which indicates the problem is abut the listener. You can check whether net.Listen("tcp", fmt.Sprintf(":%d", serverPort)) returns or blocks.

Related

Can't access mux params when testing API endpoint with custom handlers

I have a custom handler for my API endpoints like this:
type HTTPError struct {
Error error
Message string
Code int
}
type endpointREST func(http.ResponseWriter, *http.Request) *HTTPError
func (fn endpointREST) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil {
http.Error(w, e.Message, e.Code)
}
}
my example route look like this:
func GetShare(w http.ResponseWriter, r *http.Request) *HTTPError {
vars := mux.Vars(r)
fmt.Println(r.URL) // http://127.0.0.1:36455/share/5713d228-a042-446d-a5e4-183b19fa832a
fmt.Println(vars) // -->> always empty map when testing
return nil
}
These routes work well (manual, using Postman) after setting them up with
router := mux.NewRouter().StrictSlash(true)
handler := cors.Default().Handler(router)
router.Handle("/share/{id}", endpointREST(GetShare)).Methods("GET")
log.Fatal(http.ListenAndServe(":6969", handler))
The Problem is, that i can't test the API this way, since mux.Vars(r) will always return an empty map in the testing environment.
This is my testing code:
func TestGetShare(t *testing.T) {
Reset()
router := mux.NewRouter()
ts := httptest.NewServer(router)
router.Handle("/share/{id}", endpointREST(GetShare)).Methods("GET")
defer ts.Close()
t.Run("unauthorized", func(t *testing.T) {
req, _ := http.NewRequest("GET", ts.URL + "/share/5713d228-a042-446d-a5e4-183b19fa832a", nil)
res, _ := http.DefaultClient.Do(req)
assert.Equal(t, http.StatusUnauthorized, res.StatusCode)
})
}
I suggest you tu use the SetURLVars test helper func:
// SetURLVars sets the URL variables for the given request, to be accessed via
// mux.Vars for testing route behaviour. Arguments are not modified, a shallow
// copy is returned.
//
// This API should only be used for testing purposes; it provides a way to
// inject variables into the request context. Alternatively, URL variables
// can be set by making a route that captures the required variables,
// starting a server and sending the request to that server.
func SetURLVars(r *http.Request, val map[string]string) *http.Request {
return requestWithVars(r, val)
}

Rest API in Go (Consume and host)

I am trying to request something, like a book by its id, and then host it locally so that if I write my local URL, like http://localhost:8080​/books?books=<book-id> it would show me the specific result.
To try to be concrete, I need to connect the two. Get the information from that URL, so "consume" and also host it locally, specifically by ID. I am not sure how to do both at once.
To create the paths, I've been using gorilla mux
So separately, I've used this, which would give me all the books at once (URL is not real).
func main() {
response, err := http.Get("https://bookibook.herokuapp.com/books/")
if err != nil {
fmt.Printf("there is no book with this ID %s\n", err)
} else {
data, _ := ioutil.ReadAll(response.Body)
fmt.Println(string(data))
}
}
and then this, which would create a local path for http://localhost:8080/books/ID
import (
"fmt"
"github.com/gorilla/mux"
"log"
"net/http"
)
func getID(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
fmt.Fprintf(w, "Get id %s\n", vars["id"])
}
func main() {
// Configure routes.
router := mux.NewRouter()
router.HandleFunc("/books/{id}/", getID).Methods(http.MethodGet)
// Start HTTP server.
if err := http.ListenAndServe(":8080", router); err != nil {
log.Fatal(err)
}
}

Golang imap.DialTLS Config example

I used to be able to connect to port 143 of a mail server like this:
c, err := imap.Dial(mailServer)
The code above connects to port 143 of the mailServer. Now I have a new mail server only accepts port 993. Looking at the Golang imap source code, the function DialTLS will connect to port 993. The signature of DialTLS looks like this:
func DialTLS(addr string, config *tls.Config) (c *Client, err error)
Now I don't know how to construct the *tls.Config. I Googled around, but didn't not find anything really useful. Can someone show me some example how to construct the *tls.Config?
I tried to pass in nil as the second parameter, it compiles, and I didn't get any runtime error. But it seems no new mails were fetched, even I believe there should be.
My fetch mail code looks like this:
// testimap
package main
import (
"bytes"
"code.google.com/p/go-imap/go1/imap"
"fmt"
"net/mail"
"time"
)
type Mail struct {
Subject string
Body string
From string
Uid uint32
}
func FetchMail(lastUid uint32) []*Mail {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
//
// Note: most of error handling code is omitted for brevity
//
var (
c *imap.Client
cmd *imap.Command
rsp *imap.Response
)
// Connect to the server
c, err := imap.DialTLS(mailServer, nil)
if err != nil {
fmt.Println(err)
}
// Remember to log out and close the connection when finished
defer c.Logout(30 * time.Second)
// Print server greeting (first response in the unilateral server data queue)
//fmt.Println("Server says hello:", c.Data[0].Info)
c.Data = nil
// Enable encryption, if supported by the server
if c.Caps["STARTTLS"] {
c.StartTLS(nil)
}
// Authenticate
if c.State() == imap.Login {
c.Login(mailSupportUser, mailSupportPw)
}
//// List all top-level mailboxes, wait for the command to finish
cmd, err = imap.Wait(c.List("", "%"))
if err != nil {
fmt.Println(err)
}
// Print mailbox information
//fmt.Println("\nTop-level mailboxes:")
//for _, rsp = range cmd.Data {
// fmt.Println("|--", rsp.MailboxInfo())
//}
// Check for new unilateral server data responses
//for _, rsp = range c.Data {
// fmt.Println("Server data:", rsp)
//}
c.Data = nil
// Open a mailbox (synchronous command - no need for imap.Wait)
c.Select("INBOX", true)
//fmt.Print("\nMailbox status:\n", c.Mailbox)
// Fetch the headers of the 10 most recent messages
set, err := imap.NewSeqSet(fmt.Sprint(lastUid, ":*"))
if err != nil {
fmt.Println(err)
}
//if c.Mailbox.Messages >= 10 {
// set.AddRange(c.Mailbox.Messages-9, c.Mailbox.Messages)
//} else {
// set.Add("1:*")
//}
cmd, err = c.UIDFetch(set, "RFC822.HEADER", "RFC822.TEXT")
if err != nil {
fmt.Println(err)
}
// Process responses while the command is running
//fmt.Println("\nMost recent messages:")
mails := make([]*Mail, 0, 10)
for cmd.InProgress() {
// Wait for the next response (no timeout)
c.Recv(-1)
// Process command data
for _, rsp = range cmd.Data {
if err != nil {
fmt.Println(err)
}
header := imap.AsBytes(rsp.MessageInfo().Attrs["RFC822.HEADER"])
uid := imap.AsNumber((rsp.MessageInfo().Attrs["UID"]))
body := imap.AsBytes(rsp.MessageInfo().Attrs["RFC822.TEXT"])
if msg, err := mail.ReadMessage(bytes.NewReader(header)); msg != nil {
if err != nil {
fmt.Println(err)
}
mail := &Mail{
Subject: msg.Header.Get("Subject"),
From: msg.Header.Get("FROM"),
Body: string(body),
Uid: uid,
}
if mail.Uid < lastUid {
continue
}
mails = append(mails, mail)
}
}
cmd.Data = nil
c.Data = nil
}
// Check command completion status
if rsp, err := cmd.Result(imap.OK); err != nil {
if err == imap.ErrAborted {
fmt.Println("Fetch command aborted")
} else {
fmt.Println("Fetch error:", rsp.Info)
}
}
fmt.Println(mails)
return mails
}
First off, you should be using the project's GitHub repo as the Google Code project stated development was moving to GitHub due to Google Code shutting down. It's a few commits ahead of the Google Code repo too, so you won't be getting any updates if you don't migrate to the GitHub repo.
Secondly, looking at the package's demo, passing nil to DialTLS as the TLS client seems to be the proper way to connect with a default TLS client.
From the information you've given, it seems like it may be an issue with your server accepting connections over that port. I would look into if the port is open to the client you're trying to connect from or if your IMAP server is even accepting TLS connections.
If you're absolutely sure it's not a server issue after further debugging, I would file an issue on the project's GitHub issue tracker to get help from people who are more familiar with the package, seeing that it's a third party package.

SSH Connection Timeout

I am trying to make SSH connections using golang.org/x/crypto/ssh and I am kinda surprised that I can't seem to find out how to timeout the NewSession function (I actually don't seen any way to timeout anything). When I try to connect to a server that is having issues, this just hangs for a very long time. I have written something to use select with a time.After but it just feels like a hack. Something I haven't tried yet is to keep the underlying net.Conn in my struct and just keep doing Conn.SetDeadline() calls. Haven't tried this yet because I don't know if the crypto/ssh library overrides this or anything like that.
Anyone have a good way to timeout dead servers with this library? Or does anyone know of a better library?
One way to handle this transparently with the ssh package, is to create a connection with an idle timeout via a custom net.Conn which sets deadlines for you. However, this will cause the background Reads on a connection to timeout, so we need to use ssh keepalives to keep the connection open. Depending on your use case, simply using ssh keepalives as an alert for a dead connection may suffice.
// Conn wraps a net.Conn, and sets a deadline for every read
// and write operation.
type Conn struct {
net.Conn
ReadTimeout time.Duration
WriteTimeout time.Duration
}
func (c *Conn) Read(b []byte) (int, error) {
err := c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout))
if err != nil {
return 0, err
}
return c.Conn.Read(b)
}
func (c *Conn) Write(b []byte) (int, error) {
err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout))
if err != nil {
return 0, err
}
return c.Conn.Write(b)
}
You can then use net.DialTimeout or a net.Dialer to get the connection, wrap it in your Conn with timeouts, and pass it into ssh.NewClientConn.
func SSHDialTimeout(network, addr string, config *ssh.ClientConfig, timeout time.Duration) (*ssh.Client, error) {
conn, err := net.DialTimeout(network, addr, timeout)
if err != nil {
return nil, err
}
timeoutConn := &Conn{conn, timeout, timeout}
c, chans, reqs, err := ssh.NewClientConn(timeoutConn, addr, config)
if err != nil {
return nil, err
}
client := ssh.NewClient(c, chans, reqs)
// this sends keepalive packets every 2 seconds
// there's no useful response from these, so we can just abort if there's an error
go func() {
t := time.NewTicker(2 * time.Second)
defer t.Stop()
for range t.C {
_, _, err := client.Conn.SendRequest("keepalive#golang.org", true, nil)
if err != nil {
return
}
}
}()
return client, nil
}
Set the timeout on the ssh.ClientConfig.
cfg := ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.FixedHostKey(hostKey),
Timeout: 15 * time.Second, // max time to establish connection
}
ssh.Dial("tcp", ip+":22", &cfg)
When you call ssh.Dial, the timeout will be passed to net.DialTimeout.

Idiomatic Go for abstracting resource allocation/deallocation

Is there an idiomatic Go way for abstracting resource allocation/deallocation? My initial guess is to abstract the allocation/deallocation in a higher-order function:
func withResource(f func(Resource)error) error {
// allocate resource
// defer free resource
return f(resource)
}
However, this line of thinking is borrowed directly from the functional paradigm and doesn't seem to align well with Go's largely imperative nature.
As a concrete example, running a daemon for the duration of a block of code is a recurring theme in my current project, so I've created a withDaemon function to abstract the commonality:
func withDaemon(
cmd *exec.Cmd,
f func(io.ReadCloser, io.ReadCloser, io.WriteCloser) error,
) error {
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("couldn't get stdout: %v", err)
}
stderr, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("couldn't get stderr: %v", err)
}
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("couldn't get stdin: %v", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start: %v", err)
}
defer func() {
cmd.Process.Kill()
cmd.Wait()
}
return f(stdout, stderr, stdin)
}
I think that the idiomatic way would be to create a Daemon type, and to just use defer in the caller:
d := NewDaemon(...)
defer d.Stop()
doWhatever()