forked from the-jenkins-x-files/mulder
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 50a2c3f
Showing
45 changed files
with
7,765 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/mulder |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |
Oops, something went wrong.