Add LKML Prow Component

Adds component to run lkml controller. Uses service environment variable
to determine cluster ip of git server for creating ProwJobs that may
clone from this component. Starts smtp server and lkml controller, using
the messageQueue to pipe received emails to the controller.

Deployment must expose port 25 for external
traffic using Kubernetes LoadBalancer Service, and exposing port 8080
for internal traffic using Kubernetes ClusterIP Service.

Stock Deployment configuration in another CL.

Change-Id: I30718e65d42df738a71f6548e7b958ae931b0d16
Signed-off-by: Avi Kondareddy <avikr@google.com>
diff --git a/BUILD.bazel b/BUILD.bazel
new file mode 100644
index 0000000..918a226
--- /dev/null
+++ b/BUILD.bazel
@@ -0,0 +1,22 @@
+load(
+    "@io_bazel_rules_docker//container:container.bzl",
+    "container_image",
+    "container_push",
+)
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+container_image(
+    name = "lkml_image",
+    base = "//cmd/lkml:image",
+    directory = "/",
+    tars = ["@git_daemon//:files"],
+)
+
+container_push(
+    name = "push_lkml",
+    format = "Docker",
+    image = ":lkml_image",
+    registry = "gcr.io",
+    repository = "kunit-presubmit/lkml",
+    tag = "latest",
+)
diff --git a/WORKSPACE b/WORKSPACE
index fea7477..993f2a4 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -86,10 +86,30 @@
 )
 
 load("@bazel_gazelle//:deps.bzl", "go_repository")
-load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
+load(
+    "@bazel_tools//tools/build_defs/repo:git.bzl",
+    "git_repository",
+    "new_git_repository",
+)
 
 git_repository(
     name = "test_infra",
     branch = "master",
     remote = "https://github.com/kubernetes/test-infra",
 )
+
+new_git_repository(
+    name = "go_smtpd",
+    branch = "master",
+    build_file_content = """
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["smtpd/smtpd.go"],
+    importpath = "github.com/bradfitz/go-smtpd",
+    visibility = ["//visibility:public"],
+)
+    """,
+    remote = "https://github.com/bradfitz/go-smtpd",
+)
diff --git a/cmd/lkml/BUILD.bazel b/cmd/lkml/BUILD.bazel
new file mode 100644
index 0000000..99f9e92
--- /dev/null
+++ b/cmd/lkml/BUILD.bazel
@@ -0,0 +1,33 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+load("@io_bazel_rules_docker//go:image.bzl", "go_image")
+
+go_binary(
+    name = "binary",
+    embed = [":go_default_library"],
+    pure = "on",
+)
+
+go_image(
+    name = "image",
+    base = "@git_base//image",
+    embed = [":go_default_library"],
+    goarch = "amd64",
+    goos = "linux",
+    pure = "on",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    srcs = ["main.go"],
+    importpath = "github.com/avikr/lkml",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//lkml:go_default_library",
+        "@go_smtpd//:go_default_library",
+        "@test_infra//prow/config:go_default_library",
+        "@test_infra//prow/kube:go_default_library",
+        "@test_infra//prow/pjutil:go_default_library",
+        "@test_infra//vendor/github.com/sirupsen/logrus:go_default_library",
+    ],
+)
diff --git a/cmd/lkml/main.go b/cmd/lkml/main.go
new file mode 100644
index 0000000..0711f0b
--- /dev/null
+++ b/cmd/lkml/main.go
@@ -0,0 +1,151 @@
+package main
+
+import (
+	"flag"
+	"github.com/bradfitz/go-smtpd"
+	"github.com/sirupsen/logrus"
+	"k8s.io/test-infra/prow/config"
+	"k8s.io/test-infra/prow/kube"
+	"log"
+	"os"
+	"prow-lkml/lkml"
+	"strings"
+	"time"
+)
+
+// mail server over smtp port 25
+const smtpListen = ":25"
+
+type mail struct {
+	from         smtpd.MailAddress
+	body         strings.Builder
+	conn         smtpd.Connection
+	hasRcpt      bool
+	messageQueue chan<- string
+}
+
+// possibly filter connection by address
+func onNewConnection(c smtpd.Connection) error {
+	log.Printf("smtpd: new connection from %v", c.Addr())
+	return nil
+}
+
+func (m *mail) AddRecipient(rcpt smtpd.MailAddress) error {
+	m.hasRcpt = true
+	log.Printf("To: email:%s", rcpt.Email())
+	return nil
+}
+
+func (m *mail) BeginData() error {
+	if !m.hasRcpt {
+		return smtpd.SMTPError("554 5.5.1 Error: no valid recipients")
+	}
+	return nil
+}
+
+func (m *mail) Write(line []byte) error {
+	m.body.Write(line)
+	return nil
+}
+
+func (m *mail) Close() error {
+	str := m.body.String()
+	m.messageQueue <- str
+	return nil
+}
+
+type options struct {
+	configPath    string
+	jobConfigPath string
+	sourceRepo    string
+	sourceBranch  string
+	jobURI        string
+}
+
+func (o *options) validate() error {
+	return nil
+}
+
+func gatherOptions() options {
+	o := options{}
+	flag.StringVar(&o.configPath, "config-path", "", "Path to config.yaml.")
+	// TODO: support muliple mailing lists each mapped to multiple repo/branches
+	// one concern is that this might put too much overhead on one container
+	// might be better to split over multiple instances of the lkml pod
+	flag.StringVar(&o.jobConfigPath, "job-config-path", "", "Path to prow job configs")
+	flag.StringVar(&o.sourceRepo, "source-repo", "", "Repo to patch to")
+	flag.StringVar(&o.sourceBranch, "source-branch", "", "Branch to patch to")
+	flag.StringVar(&o.jobURI, "job-uri", "", "Mapping for presubmit job")
+	flag.Parse()
+	return o
+}
+
+func main() {
+	o := gatherOptions()
+	if err := o.validate(); err != nil {
+		log.Fatal("Args to LKML component were not valid: %v", err)
+	}
+
+	// config agent for fetching current configuration
+	ca := &config.Agent{}
+	if err := ca.Start(o.configPath, o.jobConfigPath); err != nil {
+		logrus.WithError(err).Fatal("Error starting config agent.")
+	}
+
+	// start kube client needed to trigger Jobs
+	log.Printf("Namespace is %s", ca.Config().ProwJobNamespace)
+	kc, err := kube.NewClientInCluster(ca.Config().ProwJobNamespace)
+	if err != nil {
+		logrus.WithError(err).Fatal("Error getting kube client.")
+	}
+
+	// get ClusterIP through Service Environment Variable.
+	// need Service deployed before pod for this to work
+	host := os.Getenv("GIT_SERVICE_HOST")
+	if host == "" {
+		log.Fatal("IP address for git server is invalid. Redeploy Pod after" +
+			"Service is live")
+	}
+
+	// queue to pass patches from smtp to lkml controller
+	var messageQueue = make(chan string, lkml.QueueSize)
+
+	c := lkml.New(
+		messageQueue,
+		o.sourceRepo,
+		o.sourceBranch,
+		o.jobURI,
+		host,
+		ca,
+		kc,
+	)
+
+	// closure for accessing message channel from mail
+	onNewMail := func(c smtpd.Connection, from smtpd.MailAddress) (smtpd.Envelope, error) {
+		log.Printf("Email From: email:%s", from.Email())
+		return &mail{
+			from:         from,
+			conn:         c,
+			messageQueue: messageQueue,
+		}, nil
+	}
+
+	// start smtp server
+	go func() {
+		log.Printf("running smtp server on %s", smtpListen)
+		s := &smtpd.Server{
+			Addr:            smtpListen,
+			OnNewMail:       onNewMail,
+			OnNewConnection: onNewConnection,
+			ReadTimeout:     time.Minute,
+		}
+		err := s.ListenAndServe()
+		log.Printf("SMTP server died: %v", err)
+		// process remaining patches and jobs before closing
+		close(messageQueue)
+	}()
+
+	if err := c.Run(); err != nil {
+		log.Fatal("Error from lkml controller: %v", err)
+	}
+}