Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
vbehar committed Feb 11, 2019
0 parents commit 50a2c3f
Show file tree
Hide file tree
Showing 45 changed files with 7,765 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/mulder
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# The Jenkins X Files - Mulder

`mulder` is the backend side of [The Jenkins X Files](https://the-jenkins-x-files.github.io/) - the [Jenkins X](https://jenkins-x.io/) workshop. You can also see [Scully](https://github.com/the-jenkins-x-files/scully), the frontend side.

It's a Go application that provides a (very) basic HTTP API, with 1 main endpoint:

- `GET /quote/random` which returns a random quote from FBI's most unwanted, in JSON:

```
{
"quote": "I have a theory. Do you want to hear it?"
}
```
- `GET /healthz` checks the health of the application, and the connection to Redis. It returns either a `200` or `500` status code.
**Dependencies**:
- [Redis](https://redis.io/) - to store the quotes
**Building**:
- `go build`
**Running**:
Either:
- build the binary with `go build`, and run it
- or run it directly with `go run .`
**Flags**:
- `-listen-addr` (string): host:port on which to listen. Default: `:8080`
- `-redis-addr` (string): redis host:port to connect to. Default: `:6379`
- `-redis-connect-timeout` (duration): timeout for connecting to redis. Default: `1m0s`
**Unit Tests**:
- `go test -v .`
**Integration Tests**:
- `go test -v ./tests -addr HOST:PORT`
- don't forget to replace the `HOST:PORT` argument with the hostname and port of a running mulder instance you want to test - the integration tests won't start it for you.
## What about Makefile, Dockerfile, Jenkinsfile, Helm Chart, ... ?
It ain't there, and it's on purpose - because the goal of the workshop is to write them, or at least have them automatically generated by [Jenkins X](https://jenkins-x.io/).
But you can still find:
- a Docker image on the [dockerhub](https://hub.docker.com/), at [thejenkinsxfiles/mulder](https://hub.docker.com/r/thejenkinsxfiles/mulder)
- Pull it with `docker pull thejenkinsxfiles/mulder:1.0.0`
- Check the `Dockerfile` in the [dockerfile branch](https://github.com/the-jenkins-x-files/mulder/blob/dockerfile/Dockerfile)
- an [Helm](https://helm.sh/) chart at <https://the-jenkins-x-files.github.io/charts/>
- Check the chart sources in the [helm-chart branch](https://github.com/the-jenkins-x-files/mulder/blob/helm-chart/charts/mulder)
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module mulder

require (
github.com/chromedp/cdproto v0.0.0-20190207000234-34a5588f6c0e // indirect
github.com/gomodule/redigo v2.0.0+incompatible
github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1
github.com/tidwall/gjson v1.1.5
github.com/tidwall/match v1.0.1 // indirect
)
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
github.com/chromedp/cdproto v0.0.0-20190207000234-34a5588f6c0e h1:W+NmCDxT3g5yIiE55frPTbOk/mrjbpZ8uL+gx+vYJ34=
github.com/chromedp/cdproto v0.0.0-20190207000234-34a5588f6c0e/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307 h1:vl4eIlySbjertFaNwiMjXsGrFVK25aOWLq7n+3gh2ls=
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1 h1:+kGqA4dNN5hn7WwvKdzHl0rdN5AEkbNZd0VjRltAiZg=
github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY=
github.com/tidwall/gjson v1.1.5 h1:QysILxBeUEY3GTLA0fQVgkQG1zme8NxGvhh2SSqWNwI=
github.com/tidwall/gjson v1.1.5/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
167 changes: 167 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package main

import (
"encoding/json"
"flag"
"io"
"log"
"math/rand"
"net/http"
"time"

"github.com/gomodule/redigo/redis"
)

const (
quotesKey = "quotes"
)

var (
quotes = []interface{}{ // https://www.imdb.com/title/tt0106179/quotes
"I would never lie. I willfully participated in a campaign of misinformation.",
"Scully, I was like you once. I didn't know who to trust. Then I... I chose another path... another life, another fate, where I found my sister. The end of my world was unrecognizable and upside down. There was one thing that remained the same. You were my friend, and you told me the truth. Even when the world was falling apart, you were my constant. My touchstone.",
"Trust no one.",
"You know, they say when you talk to God it's prayer, but when God talks to you, it's schizophrenia.",
"Sorry, nobody down here but the FBI's most unwanted.",
"I have a theory. Do you want to hear it?",
"You have to be willing to see.",
"Scully, you have to believe me. Nobody else on this whole damn planet does or ever will. You're my one in five billion.",
"Scully, you are the only one I trust.",
"Sometimes the only sane answer to an insane world is insanity.",
"I've often felt that dreams are answers to questions we haven't yet figured out how to ask.",
"We've both lost so much... but I believe that what we're looking for is in the X-Files. I'm more certain than ever that the truth is in there.",
"If coincidences are coincidences, why do they feel so contrived?",
"And all the choices would then lead to this very moment. One wrong turn, and we wouldn't be sitting here together. Well, that says a lot. That says a lot, a lot, a lot.",
"The truth will save you, Scully. I think it'll save both of us.",
"THE TRUTH IS OUT THERE",
"I want to believe.",
"TRUST NO-ONE",
"What can I do about a Lie with an Official Seal on it?",
}

listenAddr string
redisAddr string
redisConnectTimeout time.Duration

redisConn redis.Conn
)

func init() {
rand.Seed(time.Now().UnixNano())

flag.StringVar(&listenAddr, "listen-addr", ":8080", "host:port on which to listen")
flag.StringVar(&redisAddr, "redis-addr", ":6379", "redis host:port to connect to")
flag.DurationVar(&redisConnectTimeout, "redis-connect-timeout", 1*time.Minute, "timeout for connecting to redis")

http.HandleFunc("/quote/random", randomQuoteHandler)
http.HandleFunc("/healthz", healthzHandler)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
}

func main() {
log.Println("Mulder is waking up...")
flag.Parse()

if err := connectToRedis(); err != nil {
log.Fatalf("Failed to connect to The (redis) X-Files at %s after timeout %s: %v", redisAddr, redisConnectTimeout, err)
}
defer redisConn.Close()

if err := insertQuotesInRedis(); err != nil {
log.Fatalf("Failed to insert files in The X-Files: %v", err)
}

log.Printf("Starting HTTP server on %s", listenAddr)
if err := http.ListenAndServe(listenAddr, nil); err != nil {
log.Fatalf("Failed to listen on %s: %v", listenAddr, err)
}
}

func connectToRedis() (err error) {
log.Printf("Connecting to The (redis) X-Files at %s...", redisAddr)

redisConn, err = redis.Dial("tcp", redisAddr, redis.DialConnectTimeout(redisConnectTimeout))
if err != nil {
return err
}

infos, err := redis.String(redisConn.Do("INFO", "SERVER"))
if err != nil {
return err
}

log.Printf("Connected to The (redis) X-Files:\n%s", infos)
return nil
}

func insertQuotesInRedis() error {
log.Println("Checking The X-Files...")
existingQuotes, err := redis.Int(redisConn.Do("LLEN", quotesKey))
if err != nil {
return err
}

if existingQuotes == len(quotes) {
log.Printf("All The %d X-Files are already there!", existingQuotes)
return nil
}

if existingQuotes > 0 {
log.Printf("There is a mess in The X-Files, we don't have the right number of quotes - %d instead of %d. Let's clean everything first...", existingQuotes, len(quotes))
if _, err = redis.Int(redisConn.Do("DEL", quotesKey)); err != nil {
return err
}
}

log.Printf("Inserting %d files in The X-Files...", len(quotes))
args := append([]interface{}{}, quotesKey)
args = append(args, quotes...)
insertedQuotes, err := redis.Int(redisConn.Do("RPUSH", args...))
if err != nil {
return err
}

log.Printf("Inserted %d/%d files in The X-Files", insertedQuotes, len(quotes))
return nil
}

func getRandomQuote() (string, error) {
quotesCount, err := redis.Int(redisConn.Do("LLEN", quotesKey))
if err != nil {
return "", err
}

randomIndex := rand.Intn(quotesCount)
return redis.String(redisConn.Do("LINDEX", quotesKey, randomIndex))
}

func randomQuoteHandler(w http.ResponseWriter, r *http.Request) {
quote, err := getRandomQuote()
if err != nil {
log.Printf("Failed to retrieve an X-File: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

log.Printf("Handled an X-File request, returned: '%s'", quote)

w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(&response{Quote: quote}); err != nil {
log.Printf("Failed to write HTTP response: %v", err)
}
}

type response struct {
Quote string `json:"quote"`
}

func healthzHandler(w http.ResponseWriter, r *http.Request) {
pong, err := redis.String(redisConn.Do("PING"))
if err != nil {
log.Printf("Healthz handler failing: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

io.WriteString(w, pong)
}
28 changes: 28 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"testing"

"github.com/rafaeljusto/redigomock"
)

func TestGetRandomQuote(t *testing.T) {
mock := redigomock.NewConn()
redisConn = mock

mock.Command("LLEN", quotesKey).Expect(int64(len(quotes)))
for i, quote := range quotes {
mock.Command("LINDEX", quotesKey, i).Expect(quote)
}

quote, err := getRandomQuote()
if err != nil {
t.Fatalf("Failed to get a random quote: %v", err)
}
for _, q := range quotes {
if q.(string) == quote {
return
}
}
t.Errorf("Invalid random quote %s", quote)
}
47 changes: 47 additions & 0 deletions tests/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package tests

import (
"flag"
"fmt"
"io/ioutil"
"net/http"
"testing"

"github.com/tidwall/gjson"
)

var (
mulderAddr string
)

func init() {
flag.StringVar(&mulderAddr, "addr", "localhost:8080", "mulder host:port on which to connect to run the integration tests")
}

func TestRandomQuote(t *testing.T) {
randomQuoteURL := fmt.Sprintf("http://%s/quote/random", mulderAddr)
t.Logf("Testing %s", randomQuoteURL)

resp, err := http.Get(randomQuoteURL)
if err != nil {
t.Fatalf("Got unexpected error on %s: %v", randomQuoteURL, err)
}

if resp.StatusCode != http.StatusOK {
t.Errorf("Got wrong HTTP Status Code %d - expected %d", resp.StatusCode, http.StatusOK)
}
if contentType := resp.Header.Get("Content-Type"); contentType != "application/json" {
t.Errorf("Got wrong Content-Type '%s' - expected '%s'", contentType, "application/json")
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Got unexpected error while reading the HTTP response body from %s: %v", randomQuoteURL, err)
}

quote := gjson.ParseBytes(body).Get("quote").String()
t.Logf("Got quote: %s", quote)
if len(quote) == 0 {
t.Error("Got invalid empty quote")
}
}
Loading

0 comments on commit 50a2c3f

Please sign in to comment.