Last active
October 18, 2017 03:17
-
-
Save bpostlethwaite/cc5d8927849b834a8b151ac113ca024e to your computer and use it in GitHub Desktop.
kube potato
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
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 |
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
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) | |
})) | |
} |
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
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 |
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
<!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> |
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
@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