joelbirchler
2019-12-18 13f7b7c55186c0daeb26c4850f876e5b5f9cf0ec
Adds ch11 exoplanets application (based on ch11 books app)
10 files added
1 files modified
775 ■■■■■ changed files
books/README.md 18 ●●●● patch | view | raw | blame | history
exoplanets/Dockerfile 32 ●●●●● patch | view | raw | blame | history
exoplanets/Makefile 46 ●●●●● patch | view | raw | blame | history
exoplanets/README.md 68 ●●●●● patch | view | raw | blame | history
exoplanets/database.go 37 ●●●●● patch | view | raw | blame | history
exoplanets/exoplanets.go 89 ●●●●● patch | view | raw | blame | history
exoplanets/fetch_planets.py 30 ●●●●● patch | view | raw | blame | history
exoplanets/main.go 29 ●●●●● patch | view | raw | blame | history
exoplanets/seed.go 310 ●●●●● patch | view | raw | blame | history
exoplanets/server.go 41 ●●●●● patch | view | raw | blame | history
exoplanets/template.html 75 ●●●●● patch | view | raw | blame | history
books/README.md
@@ -1,12 +1,24 @@
A simple example application for use with PostgreSQL.
Currently, the app drops and creates a new "book" table on start up. This is not
Books also features a /leak endpoint that grabs a few MB of memory and holds onto
it. Hit it with curl in a loop to simulate a memory leak.
## Similarities to the Exoplanets Application
This application is similar to the "exoplanets" application in the same repository, and
there is currently some code duplication between the two. We considered combining
the applications into a single codebase, but the forking logic to juggle different
datasets and templates proved more complex than the value.
## Table Drop Warning
The app drops and creates a new "exoplanets" table on start up. This is not
ideal in a deployment or any real scenario, but makes everything easy from an
instructional point of view. A better approach would be to create a job that populates
the initial data.
We also have a /leak endpoint that grabs a few MB of memory and holds onto it. Hit
it with curl in a loop to simulate a memory leak.
## Environment Variables
exoplanets/Dockerfile
New file
@@ -0,0 +1,32 @@
# -- Build stage --
FROM registry.redhat.io/ubi8/go-toolset:1.12.8 AS builder
WORKDIR $HOME/go/src/exoplanets
COPY *.go .
RUN go get -u \
      github.com/gorilla/mux \
      github.com/lib/pq && \
    go build
# -- App stage --
FROM registry.redhat.io/ubi8-minimal:8.0-213
LABEL maintainer="jbirchler@redhat.com" \
      version="v1.0.0"
WORKDIR /home
COPY --from=builder /opt/app-root/src/go/src/exoplanets/exoplanets .
COPY template.html .
RUN chmod 440 template.html && \
    chgrp -R 0 . && \
    chmod -R g=u .
USER 1001
EXPOSE 8080
CMD ["./exoplanets"]
exoplanets/Makefile
New file
@@ -0,0 +1,46 @@
version = v1.0
repo = quay.io/redhattraining
all: fmt build run
fmt:
    gofmt -w *.go
build:
    sudo buildah bud -t exoplanets:latest .
run:
    sudo podman run \
        -p 8080:8080 \
        -e DB_HOST=$$(sudo podman inspect postgres --format '{{ .NetworkSettings.IPAddress }}') \
        -e DB_PORT=5432 \
        -e DB_USER=user \
        -e DB_PASSWORD=password \
        -e DB_NAME=postgres \
        exoplanets:latest
run-without-db:
    sudo podman run \
        -p 8080:8080 \
        exoplanets:latest
tag:
    sudo buildah tag exoplanets:latest $(repo)/exoplanets:latest
    sudo buildah tag exoplanets:latest $(repo)/exoplanets:$(version)
push:
    sudo podman push $(repo)/exoplanets:latest
    sudo podman push $(repo)/exoplanets:$(version)
pg-up:
    sudo podman run -d \
        --name postgres \
        -p 5432:5432 \
        -e POSTGRES_PASSWORD=password \
        -e POSTGRES_USER=user \
        postgres:12.1-alpine
pg-down:
    sudo podman rm -f postgres
.PHONY: fmt build run run-without-db tag push pg-up pg-down
exoplanets/README.md
New file
@@ -0,0 +1,68 @@
A simple example application for use with PostgreSQL.
## Similarities to the Books Application
This application is similar to the "books" application in the same repository, and
there is currently some code duplication between the two. We considered combining
the applications into a single codebase, but the forking logic to juggle different
datasets and templates proved more complex than the value.
The exoplanets application does not include the "memory leak" feature that is
available in the books application.
## Table Drop Warning
The app drops and creates a new "exoplanets" table on start up. This is not
ideal in a deployment or any real scenario, but makes everything easy from an
instructional point of view. A better approach would be to create a job that populates
the initial data.
## Environment Variables
We use the following ENVs to connect to the database:
  * `DB_HOST`
  * `DB_PORT`
  * `DB_USER`
  * `DB_PASSWORD`
  * `DB_NAME`
If the variables are not present, the application will run but not attempt to connect
to the database.
## Fetching Exoplanets
The exoplanet data comes from the [Open Exoplanet Catalogue](https://github.com/openexoplanetcatalogue/open_exoplanet_catalogue/).
We've included a script in this repository that pulls data from the catalog and
then outputs a small subset of the planets as a Go struct. This approach makes it
easy to refresh the data: `python3 fetch_planets.py > seed.go`
## Building
A Makefile exists to avoid the burden of remembering things.
  * `make build`: Builds a container
  * `make`: Gofmt, build, and run (locally).
## Pushing
First log podman in to quay.io/redhattraining and verify the `version` and `repo` variables in the Makefile.
Once that's all good: `make tag push`.
## Local Development
There are a few helper tasks in the Makefile that might be of use:
  * `make pg-up`: Starts a PostgreSQL container.
  * `make pg-down`: Completely stops (rm -f) PostgreSQL.
  * `make run`: Runs the app (you'll need to build it first) with DB_HOST to the ip
  of the postgres container.
  * `make`: Gofmt, build, and run (locally).
exoplanets/database.go
New file
@@ -0,0 +1,37 @@
package main
import (
    "database/sql"
    "fmt"
    "log"
    _ "github.com/lib/pq"
)
type dbInfo struct {
    host, port, user, password, dbname string
}
func (d dbInfo) String() string {
    return fmt.Sprintf(
        "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
        d.host, d.port, d.user, d.password, d.dbname)
}
func dbConnect(info dbInfo) *sql.DB {
    log.Printf("Connecting to database with: %s", info)
    db, err := sql.Open("postgres", info.String())
    if err != nil {
        log.Fatal(err)
    }
    err = db.Ping()
    if err != nil {
        log.Fatal(err)
    }
    log.Println("Connected to database")
    return db
}
exoplanets/exoplanets.go
New file
@@ -0,0 +1,89 @@
package main
import (
    "database/sql"
    "log"
    _ "github.com/lib/pq"
)
// Exoplanet is a simple record
type Exoplanet struct {
    Name                 string
    Mass, Period, Radius float64
}
// Exoplanets handles database interactions for lists of exoplanets
type Exoplanets struct {
    DB   *sql.DB
    List []Exoplanet
}
// fetch retrieves a fresh list from the database
func (b *Exoplanets) fetch() {
    if b.DB == nil {
        log.Println("Not connected to database")
        return
    }
    log.Printf("Fetching exoplanets")
    rows, err := b.DB.Query(`SELECT name, mass, period, radius FROM exoplanet ORDER BY name ASC`)
    if err != nil {
        log.Printf("Unable to select exoplanet table:", err)
        return
    }
    defer rows.Close()
    // clear the List and rebuild it from the returned rows
    b.List = []Exoplanet{}
    var (
        name                 string
        mass, period, radius float64
    )
    for rows.Next() {
        err = rows.Scan(&name, &mass, &period, &radius)
        if err != nil {
            log.Printf("Error: %v", err.Error())
        }
        b.List = append(b.List, Exoplanet{Name: name, Mass: mass, Period: period, Radius: radius})
    }
}
// populate creates and populates an exoplanet table from the seed
func (b *Exoplanets) populate() {
    if b.DB == nil {
        log.Println("Not connected to database")
        return
    }
    log.Printf("Recreating exoplanets table")
    // drop the table (in case it already exists)
    _, err := b.DB.Query(`DROP TABLE exoplanet`)
    if err != nil {
        log.Println("Unable to drop exoplanet table (may not exist)")
    }
    // create the exoplanet table
    _, err = b.DB.Query(`CREATE TABLE exoplanet
    (id serial primary key,
    name varchar(255),
    mass double precision,
        period double precision,
        radius double precision)`)
    if err != nil {
        log.Fatalf("Unable to create exoplanet table:", err)
    }
    // populate the table from the seed exoplanet list
    log.Printf("Populating exoplanet table")
    for _, p := range seed {
        _, err = b.DB.Query(`INSERT INTO exoplanet (name, mass, period, radius) VALUES ($1,$2,$3, $4)`, p.Name, p.Mass, p.Period, p.Radius)
        if err != nil {
            log.Fatalf("Unable to populate exoplanet table:", err)
        }
    }
}
exoplanets/fetch_planets.py
New file
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
import xml.etree.ElementTree as ET, urllib.request, gzip, io
# Grab and parse the data
url  = "https://github.com/OpenExoplanetCatalogue/oec_gzip/raw/master/systems.xml.gz"
resp = urllib.request.urlopen(url).read()
body = gzip.GzipFile(fileobj=io.BytesIO(resp))
oec  = ET.parse(body)
fields = ["name", "mass", "radius", "period"]
count = 0
limit = 50
# Print out a GoLang formatted struct seed, if we have all of the values
print("package main\n")
print("var seed = []Exoplanet{")
for planet in oec.findall(".//planet"):
    p = { k:planet.findtext(k) for k in fields }
    if all(p.values()):
        print("\t{")
        print('\t\tName:   "{}",'.format(p.get("name")))
        print('\t\tMass:   {},'.format(p.get("mass")))
        print('\t\tRadius: {},'.format(p.get("radius")))
        print('\t\tPeriod: {},'.format(p.get("period")))
        print("\t},")
        count += 1
        if count > limit:
            break
print("}")
exoplanets/main.go
New file
@@ -0,0 +1,29 @@
package main
import (
    "database/sql"
    "log"
    "os"
)
func main() {
    var (
        db         *sql.DB
        exoplanets *Exoplanets
    )
    if os.Getenv("DB_HOST") != "" {
        db = dbConnect(dbInfo{
            host:     os.Getenv("DB_HOST"),
            port:     os.Getenv("DB_PORT"),
            user:     os.Getenv("DB_USER"),
            password: os.Getenv("DB_PASSWORD"),
            dbname:   os.Getenv("DB_NAME")})
        defer db.Close()
    }
    exoplanets = &Exoplanets{DB: db}
    exoplanets.populate()
    log.Fatal(listenAndServe("8080", exoplanets))
}
exoplanets/seed.go
New file
@@ -0,0 +1,310 @@
package main
var seed = []Exoplanet{
    {
        Name:   "2M 0746+20 b",
        Mass:   30,
        Radius: 0.97,
        Period: 4640,
    },
    {
        Name:   "2M 2140+16 b",
        Mass:   20,
        Radius: 0.92,
        Period: 7340,
    },
    {
        Name:   "2M 2206-20 b",
        Mass:   30,
        Radius: 1.3,
        Period: 8686,
    },
    {
        Name:   "51 Eri b",
        Mass:   2,
        Radius: 1,
        Period: 15000,
    },
    {
        Name:   "55 Cancri e",
        Mass:   0.0251,
        Radius: 0.16728,
        Period: 0.7365474,
    },
    {
        Name:   "BD+20 594 b",
        Mass:   0.05129,
        Radius: 0.1989,
        Period: 41.6855,
    },
    {
        Name:   "beta Pic b",
        Mass:   11,
        Radius: 1.65,
        Period: 8167,
    },
    {
        Name:   "CoRoT-10 b",
        Mass:   2.75,
        Radius: 0.97,
        Period: 13.2406,
    },
    {
        Name:   "CoRoT-11 b",
        Mass:   2.49,
        Radius: 1.39,
        Period: 2.994325,
    },
    {
        Name:   "CoRoT-12 b",
        Mass:   0.917,
        Radius: 1.44,
        Period: 2.828042,
    },
    {
        Name:   "CoRoT-13 b",
        Mass:   1.308,
        Radius: 0.885,
        Period: 4.03519,
    },
    {
        Name:   "CoRoT-14 b",
        Mass:   7.6,
        Radius: 1.09,
        Period: 1.51214,
    },
    {
        Name:   "CoRoT-16 b",
        Mass:   0.53,
        Radius: 1.17,
        Period: 5.35227,
    },
    {
        Name:   "CoRoT-17 b",
        Mass:   2.45,
        Radius: 1.02,
        Period: 3.768125,
    },
    {
        Name:   "CoRoT-18 b",
        Mass:   3.47,
        Radius: 1.31,
        Period: 1.9000693,
    },
    {
        Name:   "CoRoT-19 b",
        Mass:   1.11,
        Radius: 1.45,
        Period: 3.89713,
    },
    {
        Name:   "CoRoT-1 b",
        Mass:   1.03,
        Radius: 1.49,
        Period: 1.5089557,
    },
    {
        Name:   "CoRoT-20 b",
        Mass:   4.3,
        Radius: 0.84,
        Period: 9.24285,
    },
    {
        Name:   "CoRoT-21 b",
        Mass:   2.26,
        Radius: 1.3,
        Period: 2.72474,
    },
    {
        Name:   "CoRoT-22 b",
        Mass:   0.0383853003178,
        Radius: 0.435365216104,
        Period: 9.75598,
    },
    {
        Name:   "CoRoT-23 b",
        Mass:   2.8,
        Radius: 1.05,
        Period: 3.6314,
    },
    {
        Name:   "CoRoT-24 c",
        Mass:   0.088,
        Radius: 0.44,
        Period: 11.759,
    },
    {
        Name:   "CoRoT-25 b",
        Mass:   0.27,
        Radius: 1.08,
        Period: 4.86069,
    },
    {
        Name:   "CoRoT-26 b",
        Mass:   0.52,
        Radius: 1.26,
        Period: 4.20474,
    },
    {
        Name:   "CoRoT-27 b",
        Mass:   10.39,
        Radius: 1.007,
        Period: 3.57532,
    },
    {
        Name:   "CoRoT-28 b",
        Mass:   0.484,
        Radius: 0.955,
        Period: 5.20851,
    },
    {
        Name:   "CoRoT-29 b",
        Mass:   0.85,
        Radius: 0.97,
        Period: 2.8505615,
    },
    {
        Name:   "CoRoT-2 b",
        Mass:   3.31,
        Radius: 1.465,
        Period: 1.7429964,
    },
    {
        Name:   "CoRoT-30 b",
        Mass:   2.84,
        Radius: 1.02,
        Period: 9.06005,
    },
    {
        Name:   "CoRoT-31 b",
        Mass:   0.85,
        Radius: 1.5,
        Period: 4.62941,
    },
    {
        Name:   "CoRoT-3 b",
        Mass:   21.66,
        Radius: 1.01,
        Period: 4.25680,
    },
    {
        Name:   "CoRoT-4 b",
        Mass:   0.72,
        Radius: 1.19,
        Period: 9.20205,
    },
    {
        Name:   "CoRoT-5 b",
        Mass:   0.467,
        Radius: 1.388,
        Period: 4.0378962,
    },
    {
        Name:   "CoRoT-6 b",
        Mass:   2.96,
        Radius: 1.166,
        Period: 8.886593,
    },
    {
        Name:   "CoRoT-7 b",
        Mass:   0.0151,
        Radius: 0.15,
        Period: 0.853585,
    },
    {
        Name:   "CoRoT-8 b",
        Mass:   0.22,
        Radius: 0.57,
        Period: 6.21229,
    },
    {
        Name:   "CoRoT-9 b",
        Mass:   0.84,
        Radius: 1.05,
        Period: 95.2738,
    },
    {
        Name:   "EPIC 201367065 b",
        Mass:   0.0264,
        Radius: 0.194,
        Period: 10.05449,
    },
    {
        Name:   "EPIC 201505350 c",
        Mass:   0.0500,
        Radius: 0.434,
        Period: 11.90715,
    },
    {
        Name:   "EPIC 201505350 b",
        Mass:   0.138,
        Radius: 0.691,
        Period: 7.91940,
    },
    {
        Name:   "EPIC 201546283 b",
        Mass:   0.09156,
        Radius: 0.3970,
        Period: 6.77145,
    },
    {
        Name:   "EPIC 201577035 b",
        Mass:   0.0850,
        Radius: 0.3426,
        Period: 19.3044,
    },
    {
        Name:   "EPIC 201912552 b",
        Mass:   0.02504,
        Radius: 0.2123,
        Period: 32.93963,
    },
    {
        Name:   "EPIC 203771098 b",
        Mass:   0.066,
        Radius: 0.520,
        Period: 20.88508,
    },
    {
        Name:   "EPIC 203771098 c",
        Mass:   0.0850,
        Radius: 0.723,
        Period: 42.36342,
    },
    {
        Name:   "EPIC 204221263 b",
        Mass:   0.0378,
        Radius: 0.138,
        Period: 4.01593,
    },
    {
        Name:   "EPIC 204221263 c",
        Mass:   0.031,
        Radius: 0.216,
        Period: 10.56103,
    },
    {
        Name:   "EPIC 205071984 b",
        Mass:   0.06639,
        Radius: 0.480,
        Period: 8.99218,
    },
    {
        Name:   "K2-33 b",
        Mass:   3.6,
        Radius: 0.451,
        Period: 5.42513,
    },
    {
        Name:   "EPIC 210894022 b",
        Mass:   0.027,
        Radius: 0.17,
        Period: 5.35117,
    },
    {
        Name:   "K2-29 b",
        Mass:   0.73,
        Radius: 1.19,
        Period: 3.2588321,
    },
}
exoplanets/server.go
New file
@@ -0,0 +1,41 @@
package main
import (
    "fmt"
    "github.com/gorilla/mux"
    "html/template"
    "log"
    "net/http"
    "time"
)
var homeTemplate *template.Template
func init() {
    homeTemplate = template.Must(template.ParseFiles("template.html"))
}
func listenAndServe(port string, exoplanets *Exoplanets) error {
    r := mux.NewRouter()
    r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        exoplanets.fetch()
        w.WriteHeader(http.StatusOK)
        homeTemplate.Execute(w, exoplanets.List)
    })
    r.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        fmt.Fprint(w, "ok")
    })
    srv := &http.Server{
        Handler:      r,
        Addr:         "0.0.0.0:" + port,
        WriteTimeout: 10 * time.Second,
        ReadTimeout:  10 * time.Second,
    }
    log.Printf("Listening on :%s", port)
    return srv.ListenAndServe()
}
exoplanets/template.html
New file
@@ -0,0 +1,75 @@
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Exoplanets</title>
  <style>
    body {
      margin: 2em;
      font-size: 100%;
      line-height: 1.5;
      background-color: black;
      color: white;
    }
    h1 {
      color: #fc0;
    }
    section h1 {
      color: #09c;
    }
    table {
      border: solid 1px #039;
      border-collapse: collapse;
      border-spacing: 0;
    }
    table thead {
      background-color: #039;
    }
    td, th {
      padding: 0.2em 0.5em;
    }
  </style>
</head>
<body>
  <h1>Exoplanets</h1>
  <p>
    The planets listed here are a small subset of the known planets found outside of our solar system.
    Mass and radius are listed in "Jupiter mass" and "Jupiter radius" units. The orbital period is measured in Earth days.
    The full dataset is available from the <a href="https://github.com/openexoplanetcatalogue/open_exoplanet_catalogue/">Open Exoplanet Catalogue</a>.
  </p>
  {{range .}}
  <section>
    <h1>{{.Name}}</h1>
    <table>
      <thead>
        <tr>
          <th>Mass</th>
          <th>Radius</th>
          <th>Period</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>{{.Mass}}</td>
          <td>{{.Radius}}</td>
          <td>{{.Period}}</td>
        </tr>
      </tbody>
    </table>
  </section>
  {{end}}
</body>
</html>