feat: add stock ticker app

This commit is contained in:
Dan Anglin 2023-02-19 16:28:25 +00:00
commit 30ca1c95df
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
9 changed files with 357 additions and 0 deletions

4
.dockerignore Normal file
View file

@ -0,0 +1,4 @@
*
!*.go
!go.sum
!go.mod

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
stock-ticker

22
Dockerfile Normal file
View file

@ -0,0 +1,22 @@
FROM golang:1.19.6-alpine AS builder
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
ADD *.go /workspace/
ADD go.mod /workspace/
WORKDIR /workspace
RUN go build -a -v -ldflags="-s -w" -o /workspace/stock-ticker .
FROM gcr.io/distroless/static-debian11
COPY --from=builder /workspace/stock-ticker /stock-ticker
USER 1000
ENTRYPOINT ["/stock-ticker"]
HEALTHCHECK NONE

1
README.md Normal file
View file

@ -0,0 +1 @@
# Stock Ticker

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module gitlab.com/dananglin/stock-ticker
go 1.19

78
handlers.go Normal file
View file

@ -0,0 +1,78 @@
package main
import (
"encoding/json"
"fmt"
"sort"
"strings"
"net/http"
)
func healthCheckHandleFunc(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "I'm all good!")
}
func stockPriceHandleFunc(apiKey, symbol string, nWeeks int) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintf(w, "ERROR: The method '%s' is not allow at this endpoint.\n", r.Method)
return
}
resp, err := http.Get(fmt.Sprintf(requestURLFormat, symbol, apiKey))
if err != nil {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusBadGateway)
fmt.Fprintf(w, "ERROR: Unable to get a response from Alpha Vantage; %v\n", err)
return
}
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)
var s Stock
if err := decoder.Decode(&s); err != nil {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusBadGateway)
fmt.Fprintf(w, "ERROR: Unable to decode the response from Alpha Vantage; %v\n", err)
return
}
if nWeeks > len(s.TimeSeries) {
nWeeks = len(s.TimeSeries)
}
var keys []string
for k := range s.TimeSeries {
keys = append(keys, k)
}
sort.Sort(sort.Reverse(sort.StringSlice(keys)))
var b strings.Builder
b.WriteString(fmt.Sprintf(tableFormat, s.MetaData.Symbol))
for i := 0; i < nWeeks; i++ {
b.WriteString(fmt.Sprintf("%s | %s\n", keys[i], s.TimeSeries[keys[i]].Close))
}
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, b.String())
}
}

169
kubernetes/manifest.yaml Normal file
View file

@ -0,0 +1,169 @@
---
# Namespace
apiVersion: v1
kind: Namespace
metadata:
name: stock-ticker
labels:
app: stock-ticker
---
# ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: stock-ticker
namespace: stock-ticker
labels:
app: stock-ticker
---
# Ingress
apiVersion: networking.k8s.io
kind: Ingress
metadata:
name: stock-ticker
namespace: stock-ticker
labels:
app: stock-ticker
spec:
rules:
- http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: stock-ticker
port:
number: 80
---
# Service
apiVersion: v1
kind: Service
metadata:
name: stock-ticker
namespace: stock-ticker
labels:
app: stock-ticker
spec:
selector:
app: stock-ticker
port:
- protocol: TCP
port: 80
targetPort: web
---
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: stock-ticker
namespace: stock-ticker
labels:
app: stock-ticker
data:
SYMBOL: "MSFT"
NWEEKS: "7"
---
# Secret
apiVersion: v1
kind: Secret
metadata:
name: stock-ticker-api-key
namespace: stock-ticker
labels:
app: stock-ticker
type: Opaque
data:
APIKEY: QzIyN1dEOVczTFVWS1ZWOQ==
---
# PDB
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: stock-ticker-api-key
namespace: stock-ticker
labels:
app: stock-ticker
spec:
minAvailable: 1
selector:
matchLabels:
app: stock-ticker
---
# Deployment
apiVersion: v1
kind: Deployment
metadata:
name: stock-ticker
namespace: stock-ticker
labels:
app: stock-ticker
spec:
replicas: 3
revisionHistoryLimit: 3
selector:
matchLabels:
app: stock-ticker
template:
metadata:
labels:
app: stock-ticker
spec:
serviceAccountName: stock-ticker
securityContext:
runAsUser: 1000
runAsNonRoot: true
runAsGroup: 1000
containers:
- name: stock-ticker
args:
- "--address=$(WEBSERVER_HOST):$(WEBSERVER_PORT)"
env:
- name: SYMBOL
valueFrom:
configMapKeyRef:
name: stock-ticker
key: SYMBOL
- name: APIKEY
valueFrom:
secretKeyRef:
name: stock-ticker-api-key
key: APIKEY
- name: NWEEKS
valueFrom:
configMapKeyRef:
name: stock-ticker
key: NWEEKS
- name: WEBSERVER_HOST
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: WEBSERVER_PORT
value: "8080"
image: ""
imagePullPolicy: Always
livenessProbe:
httpGet:
port: 8080
path: /healthcheck
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
readinessProbe:
httpGet:
port: 8080
path: /healthcheck
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
ports:
- containerPort: 8080
name: web
protocol: TCP
resources:
limits:
cpu: 50m
memory: 75Mi
requests:
cpu: 25m
memory: 50Mi

65
main.go Normal file
View file

@ -0,0 +1,65 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"strconv"
)
const (
requestURLFormat string = "https://www.alphavantage.co/query?function=TIME_SERIES_WEEKLY&symbol=%s&apikey=%s"
)
var tableFormat = `
Symbol: %s
Week | Closing Price
==========================
`
func main() {
var address string
flag.StringVar(&address, "address", "0.0.0.0:8080", "the web server's listening address")
flag.Parse()
symbol, err := getEnvironment("SYMBOL")
if err != nil {
log.Fatalf("ERROR: %v.\n", err)
}
apiKey, err := getEnvironment("APIKEY")
if err != nil {
log.Fatalf("ERROR: %v.\n", err)
}
nWeeksString, err := getEnvironment("NWEEKS")
if err != nil {
log.Fatalf("ERROR: %v.\n", err)
}
nWeeks, err := strconv.Atoi(nWeeksString)
if err != nil {
log.Fatalf("ERROR: Unable to parse the number of days from %s, %v\n", nWeeksString, err)
}
http.HandleFunc("/healthcheck", healthCheckHandleFunc)
http.HandleFunc("/stockprice", stockPriceHandleFunc(apiKey, symbol, nWeeks))
log.Printf("The web server is listening on %s.\n", address)
log.Fatal(http.ListenAndServe(address, nil))
}
func getEnvironment(key string) (string, error) {
value := os.Getenv(key)
if value == "" {
return "", fmt.Errorf("the environment variable %s is not set", key)
}
return value, nil
}

14
types.go Normal file
View file

@ -0,0 +1,14 @@
package main
type Stock struct {
MetaData MetaData `json:"Meta Data"`
TimeSeries map[string]StockInfo `json:"Weekly Time Series"`
}
type MetaData struct {
Symbol string `json:"2. Symbol"`
}
type StockInfo struct {
Close string `json:"4. close"`
}