Skip to content

Instantly share code, notes, and snippets.

@bpostlethwaite
Last active October 18, 2017 03:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bpostlethwaite/cc5d8927849b834a8b151ac113ca024e to your computer and use it in GitHub Desktop.
Save bpostlethwaite/cc5d8927849b834a8b151ac113ca024e to your computer and use it in GitHub Desktop.
kube potato
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: sentimentextor
spec:
replicas: 1
template:
metadata:
labels:
app: sentimentextor
spec:
containers:
- name: api
image: bpostlethwaite/potato:latest
ports:
- name: api
containerPort: 3000
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"log"
"math"
"net/http"
"os"
"strings"
language "cloud.google.com/go/language/apiv1"
"github.com/go-chi/chi"
languagepb "google.golang.org/genproto/googleapis/cloud/language/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
const Desc = "Getting sentimental"
type Potato struct {
Text string `json:"text"`
History []Entry `json:"history"`
}
type Entry struct {
Node string `json:"node"`
Text string `json:"text"`
Desc string `json:"desc"`
}
func main() {
r := chi.NewRouter()
r.Post("/process", passThePotato)
r.Post("/services", servicePost)
r.Get("/services", serviceGet)
FileServer(r, "/static", http.Dir("./static"))
fmt.Println("listening on localhost:3000")
err := http.ListenAndServe(":3000", r)
if err != nil {
log.Fatal(err)
}
}
func passThePotato(w http.ResponseWriter, r *http.Request) {
var p Potato
if r.Body == nil {
http.Error(w, "Please send a request body", 400)
return
}
err := json.NewDecoder(r.Body).Decode(&p)
if err != nil {
http.Error(w, err.Error(), 400)
return
}
// process text
newText := genNewText(p.Text)
// generate a new Entry
hostname, err := os.Hostname()
if err != nil {
hostname = "sentimentextor"
}
entry := Entry{
Node: hostname,
Text: newText,
Desc: Desc,
}
p.History = append(p.History, entry)
p.Text = newText
err = json.NewEncoder(w).Encode(p)
if err != nil {
log.Fatal(err)
}
}
type Subject struct {
Name string
Score float32
}
func (s Subject) Adjective() string {
score := s.Score
switch {
case 1.0 <= score && score > 0.8:
return "ace"
case 0.8 <= score && score > 0.2:
return "well decent"
case 0.2 <= score && score >= -0.2:
return "pretty nuff"
case -0.2 < score && score >= -0.8:
return "duff"
case -0.8 < score && score >= -1.0:
return "bollocks"
}
return "potty"
}
const Response = "Golang MTL thinks %s is %s!"
func (s Subject) String() string {
return fmt.Sprintf(Response, s.Name, s.Adjective())
}
func genNewText(text string) string {
ctx := context.Background()
// Creates a client.
client, err := language.NewClient(ctx)
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
// Detects the sentiment of the text.
resp, err := client.AnalyzeEntitySentiment(
ctx, &languagepb.AnalyzeEntitySentimentRequest{
Document: &languagepb.Document{
Source: &languagepb.Document_Content{
Content: text,
},
Type: languagepb.Document_PLAIN_TEXT,
},
EncodingType: languagepb.EncodingType_UTF8,
})
if err != nil {
log.Fatalf("Failed to analyze text: %v", err)
}
// proto.MarshalText(os.Stdout, resp)
if len(resp.Entities) == 0 {
return ""
}
// pick the most salient entity
subject := Subject{}
var subjectSalience float32 = math.SmallestNonzeroFloat32
for _, entity := range resp.Entities {
if entity.Salience > subjectSalience {
subject.Name = entity.Name
subject.Score = entity.Sentiment.Score
subjectSalience = entity.Salience
}
}
return subject.String()
}
type Page struct {
Title string
Services []string
JSON template.HTML
}
func servicePost(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "bad potato", 400)
return
}
service := r.FormValue("service")
text := r.FormValue("text")
var buf bytes.Buffer
err = json.NewEncoder(&buf).Encode(Potato{text, nil})
if err != nil {
log.Fatal(err)
}
req, err := http.Post(fmt.Sprintf("http://%s/process", service), "application/json", &buf)
if err != nil {
log.Fatal(err)
}
if req.Body == nil {
http.Error(w, "Please send a request body", 400)
return
}
b, err := ioutil.ReadAll(req.Body)
if err != nil {
log.Fatal(err)
}
// Do the Kube requests
page := Page{"Potato Kubed", getServices(), template.HTML(string(b))}
t, err := template.ParseFiles("views/services.html")
if err != nil {
log.Fatal(err)
}
t.Execute(w, &page)
}
func serviceGet(w http.ResponseWriter, r *http.Request) {
// Do the Kube requests
page := Page{"Potato Kubed", getServices(), template.HTML(`{"text": "sample text", "history": []}`)}
t, err := template.ParseFiles("views/services.html")
if err != nil {
log.Fatal(err)
}
t.Execute(w, &page)
}
func getServices() []string {
serviceNames := []string{}
// creates the in-cluster config
config, err := rest.InClusterConfig()
if err != nil {
panic(err.Error())
}
// creates the clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err.Error())
}
services, err := clientset.CoreV1().Services("").List(metav1.ListOptions{})
if err != nil {
panic(err.Error())
}
for _, s := range services.Items {
serviceNames = append(serviceNames, s.ObjectMeta.Name)
}
return serviceNames
}
// fileserver conveniently sets up a http.FileServer handler to serve
// static files from a http.FileSystem.
func FileServer(r chi.Router, path string, root http.FileSystem) {
if strings.ContainsAny(path, "{}*") {
panic("FileServer does not permit URL parameters.")
}
fs := http.StripPrefix(path, http.FileServer(root))
if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
path += "/"
}
path += "*"
r.Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fs.ServeHTTP(w, r)
}))
}
apiVersion: v1
kind: Service
metadata:
name: sentimentextor
spec:
selector:
app: sentimentextor
ports:
- port: 80 # Keep this port, so people can reach you through http://[your name]/process
targetPort: 3000
type: LoadBalancer
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="en">
<meta name="viewport" content="width=device-width">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Personal financial dashboard">
<meta name="author" content="">
<link rel="icon" href="/static/gopher2.png">
<link rel="stylesheet" href="static/style.css">
<title>{{.Title}}</title>
</head>
<body>
<div class="safe-form">
<input type="text"
value="Service"
class="no-borders"
readonly
>
<input type="text"
value="Potato Text"
class="no-borders"
readonly
>
</div>
<form action="services" method="post" class="safe-form">
<select name="service"> <!--Supplement an id here instead of using 'name'-->
{{range .Services}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
<textarea name="text">{{.JSON}}</textarea>
<input type="submit" class="short pushsides" value="update">
</form>
</body>
</html>
@import url(https://fonts.googleapis.com/css?family=Exo:100,200,400);
@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:700,400,300);
html {
height: 100%;
width: 100%;
}
body{
background-color:black;
font-family: 'Exo', "OpenSans", "Verdana", Georgia, Serif;
margin: 0;
padding: 0;
background: black;
color: #fff;
height: 100%;
width: 100%;
}
.header{
margin-left: 20px;
margin-right: 20px;
}
.header div{
color: #fff;
font-size: 35px;
font-weight: 200;
}
.header div span{
color: #5379fa !important;
}
.header__item {
margin-left: 5px;
margin-right: 5px;
}
.content {
margin-left: 20px;
margin-right: 20px;
}
.grid {
display: grid;
}
.flex-grid {
display: flex;
}
.col {
flex: 1;
}
.center {
display: flex;
justify-content: center;
align-items: center;
}
.vertical-center {
display: flex;
align-items: center;
flex-flow: row wrap;
}
.safe-form input[type=text]{
width: 250px;
height: 30px;
background: transparent;
border: 1px solid rgba(255,255,255,0.6);
border-radius: 2px;
color: #fff;
font-size: 16px;
font-weight: 400;
padding: 4px;
}
.safe-form input[type=password]{
width: 250px;
height: 30px;
background: transparent;
border: 1px solid rgba(255,255,255,0.6);
border-radius: 2px;
color: #fff;
font-size: 16px;
font-weight: 400;
padding: 4px;
margin-top: 10px;
}
.safe-form input[type=submit]{
width: 260px;
height: 35px;
background: #fff;
border: 1px solid #fff;
cursor: pointer;
border-radius: 2px;
color: #a18d6c;
font-size: 16px;
font-weight: 400;
padding: 6px;
margin-top: 10px;
}
.safe-form input[type=submit]:hover{
opacity: 0.8;
}
.safe-form input[type=submit]:active{
opacity: 0.6;
}
.safe-form input[type=text]:focus{
outline: none;
border: 1px solid rgba(255,255,255,0.9);
}
.safe-form input[type=password]:focus{
outline: none;
border: 1px solid rgba(255,255,255,0.9);
}
.safe-form input[type=submit]:focus{
outline: none;
}
.short {
width: 100px !important;
}
.pushsides {
margin-left: 16px;
margin-right: 16px;
}
.pushleft {
margin-left: 16px;
}
.secondary {
color: #fff !important;
background: #f0ad4e !important;
border-color: #f0ad4e !important;
}
hr {
height: 10px;
border: 0;
box-shadow: 0 10px 10px -10px rgba(255,255,255,0.6) inset;
}
.no-borders {
border: 0 !important;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment