Adds ch11 exoplanets application (based on ch11 books app)
10 files added
1 files modified
| | |
| | | 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 |
| | | |
New file |
| | |
| | | # -- 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"] |
New file |
| | |
| | | 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 |
New file |
| | |
| | | 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). |
New file |
| | |
| | | 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 |
| | | } |
New file |
| | |
| | | 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) |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | #!/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("}") |
New file |
| | |
| | | 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)) |
| | | } |
New file |
| | |
| | | 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, |
| | | }, |
| | | } |
New file |
| | |
| | | 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() |
| | | } |
New file |
| | |
| | | <!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> |