Nebula: separate controller from api
diff --git a/core/nebula/api/Dockerfile b/core/nebula/api/Dockerfile
new file mode 100644
index 0000000..6c5a573
--- /dev/null
+++ b/core/nebula/api/Dockerfile
@@ -0,0 +1,6 @@
+FROM alpine:latest
+
+ARG TARGETARCH
+
+COPY web_${TARGETARCH} /usr/bin/nebula-web
+RUN chmod +x /usr/bin/nebula-web
diff --git a/core/nebula/api/Makefile b/core/nebula/api/Makefile
new file mode 100644
index 0000000..3898183
--- /dev/null
+++ b/core/nebula/api/Makefile
@@ -0,0 +1,21 @@
+clean:
+ rm -f api_*
+
+api_arm64: export CGO_ENABLED=0
+api_arm64: export GO111MODULE=on
+api_arm64: export GOOS=linux
+api_arm64: export GOARCH=arm64
+api_arm64:
+ go1.17.1 build -o api_arm64 main.go
+
+api_amd64: export CGO_ENABLED=0
+api_amd64: export GO111MODULE=on
+api_amd64: export GOOS=linux
+api_amd64: export GOARCH=amd64
+api_amd64:
+ go1.17.1 build -o api_amd64 main.go
+
+api: clean api_arm64 api_amd64
+
+push: api
+ docker buildx build --tag=giolekva/nebula-api:latest . --platform=linux/arm64,linux/amd64 --push
diff --git a/core/nebula/api/go.mod b/core/nebula/api/go.mod
new file mode 100644
index 0000000..67cf6bf
--- /dev/null
+++ b/core/nebula/api/go.mod
@@ -0,0 +1,3 @@
+module github.com/giolekva/pcloud/core/nebula/api
+
+go 1.17
diff --git a/core/nebula/api/main.go b/core/nebula/api/main.go
new file mode 100644
index 0000000..ed09135
--- /dev/null
+++ b/core/nebula/api/main.go
@@ -0,0 +1,225 @@
+package main
+
+import (
+ "context"
+ "embed"
+ "flag"
+ "fmt"
+ "html/template"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/tools/clientcmd"
+
+ nebulav1 "github.com/giolekva/pcloud/core/nebula/controller/apis/nebula/v1"
+ clientset "github.com/giolekva/pcloud/core/nebula/controller/generated/clientset/versioned"
+)
+
+var port = flag.Int("port", 8080, "Port to listen on.")
+var kubeConfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
+var masterURL = flag.String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
+
+//go:embed templates/*
+var tmpls embed.FS
+
+type Templates struct {
+ Index *template.Template
+}
+
+func ParseTemplates(fs embed.FS) (*Templates, error) {
+ index, err := template.ParseFS(fs, "templates/index.html")
+ if err != nil {
+ return nil, err
+ }
+ return &Templates{index}, nil
+}
+
+type nebulaCA struct {
+ Name string
+ Namespace string
+ Nodes []nebulaNode
+}
+
+type nebulaNode struct {
+ Name string
+ Namespace string
+ IP string
+}
+
+type Manager struct {
+ kubeClient kubernetes.Interface
+ nebulaClient clientset.Interface
+}
+
+func (m *Manager) ListAll() ([]*nebulaCA, error) {
+ ret := make([]*nebulaCA, 0)
+ cas, err := m.nebulaClient.LekvaV1().NebulaCAs("").List(context.TODO(), metav1.ListOptions{})
+ if err != nil {
+ return nil, err
+ }
+ for _, ca := range cas.Items {
+ ret = append(ret, &nebulaCA{
+ Name: ca.Name,
+ Namespace: ca.Namespace,
+ Nodes: make([]nebulaNode, 0),
+ })
+ }
+ nodes, err := m.nebulaClient.LekvaV1().NebulaNodes("").List(context.TODO(), metav1.ListOptions{})
+ if err != nil {
+ return nil, err
+ }
+ for _, node := range nodes.Items {
+ for _, ca := range ret {
+ if ca.Name == node.Spec.CAName {
+ ca.Nodes = append(ca.Nodes, nebulaNode{
+ Name: node.Name,
+ Namespace: node.Namespace,
+ IP: node.Spec.IPCidr,
+ })
+ }
+ }
+ }
+ return ret, nil
+}
+
+func (m *Manager) createNode(namespace, name, caNamespace, caName, ipCidr, pubKey string) (string, string, error) {
+ node := &nebulav1.NebulaNode{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ Namespace: namespace,
+ },
+ Spec: nebulav1.NebulaNodeSpec{
+ CAName: caName,
+ CANamespace: caNamespace,
+ IPCidr: ipCidr,
+ PubKey: pubKey,
+ SecretName: fmt.Sprintf("%s-cert", name),
+ },
+ }
+ node, err := m.nebulaClient.LekvaV1().NebulaNodes(namespace).Create(context.TODO(), node, metav1.CreateOptions{})
+ if err != nil {
+ return "", "", err
+ }
+ return node.Namespace, node.Name, nil
+}
+
+func (m *Manager) getNodeCertQR(namespace, name string) ([]byte, error) {
+ node, err := m.nebulaClient.LekvaV1().NebulaNodes(namespace).Get(context.TODO(), name, metav1.GetOptions{})
+ if err != nil {
+ return nil, err
+ }
+ secret, err := m.kubeClient.CoreV1().Secrets(namespace).Get(context.TODO(), node.Spec.SecretName, metav1.GetOptions{})
+ if err != nil {
+ return nil, err
+ }
+ return secret.Data["host.png"], nil
+}
+
+func (m *Manager) getCACertQR(namespace, name string) ([]byte, error) {
+ ca, err := m.nebulaClient.LekvaV1().NebulaCAs(namespace).Get(context.TODO(), name, metav1.GetOptions{})
+ if err != nil {
+ return nil, err
+ }
+ secret, err := m.kubeClient.CoreV1().Secrets(namespace).Get(context.TODO(), ca.Spec.SecretName, metav1.GetOptions{})
+ if err != nil {
+ return nil, err
+ }
+ return secret.Data["ca.png"], nil
+}
+
+type Handler struct {
+ mgr Manager
+ tmpls *Templates
+}
+
+func (h *Handler) handleIndex(w http.ResponseWriter, r *http.Request) {
+ cas, err := h.mgr.ListAll()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := h.tmpls.Index.Execute(w, cas); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+
+func (h *Handler) handleNode(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ namespace := vars["namespace"]
+ name := vars["name"]
+ qr, err := h.mgr.getNodeCertQR(namespace, name)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ w.Header().Set("Content-Type", "img/png")
+ w.Write(qr)
+}
+
+func (h *Handler) handleCA(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ namespace := vars["namespace"]
+ name := vars["name"]
+ qr, err := h.mgr.getCACertQR(namespace, name)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ w.Header().Set("Content-Type", "img/png")
+ w.Write(qr)
+}
+
+func (h *Handler) handleSignNode(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ _, _, err := h.mgr.createNode(
+ r.FormValue("node-namespace"),
+ r.FormValue("node-name"),
+ r.FormValue("ca-namespace"),
+ r.FormValue("ca-name"),
+ r.FormValue("ip-cidr"),
+ r.FormValue("pub-key"),
+ )
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+}
+
+func main() {
+ flag.Parse()
+ cfg, err := clientcmd.BuildConfigFromFlags(*masterURL, *kubeConfig)
+ if err != nil {
+ panic(err)
+ }
+ kubeClient, err := kubernetes.NewForConfig(cfg)
+ if err != nil {
+ panic(err)
+ }
+ nebulaClient := clientset.NewForConfigOrDie(cfg)
+ t, err := ParseTemplates(tmpls)
+ if err != nil {
+ log.Fatal(err)
+ }
+ mgr := Manager{
+ kubeClient: kubeClient,
+ nebulaClient: nebulaClient,
+ }
+ handler := Handler{
+ mgr: mgr,
+ tmpls: t,
+ }
+ r := mux.NewRouter()
+ r.HandleFunc("/node/{namespace:[a-zA-z0-9-]+}/{name:[a-zA-z0-9-]+}", handler.handleNode)
+ r.HandleFunc("/ca/{namespace:[a-zA-z0-9-]+}/{name:[a-zA-z0-9-]+}", handler.handleCA)
+ r.HandleFunc("/sign-node", handler.handleSignNode)
+ r.HandleFunc("/", handler.handleIndex)
+ http.Handle("/", r)
+ fmt.Printf("Starting HTTP server on port: %d\n", *port)
+ log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
+}
diff --git a/core/nebula/api/templates/index.html b/core/nebula/api/templates/index.html
new file mode 100644
index 0000000..ac5ae32
--- /dev/null
+++ b/core/nebula/api/templates/index.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title>Nebula Mesh VPN Manager</title>
+</head>
+<body>
+ <form action="/sign-node" method="POST">
+ <label for="ca-name">CA Name:</label><br />
+ <input type="text" name="ca-name" /><br />
+ <label for="ca-namespace">CA Namespace:</label><br />
+ <input type="text" name="ca-namespace" /><br />
+ <label for="node-name">Node Name:</label><br />
+ <input type="text" name="node-name" /><br />
+ <label for="node-namespace">Node Namespace:</label><br />
+ <input type="text" name="node-namespace" /><br />
+ <label for="ip-cidr">IP/CIDR:</label><br />
+ <input type="text" name="ip-cidr" /><br />
+ <label for="pub-key">Public Key:</label><br />
+ <textarea name="pub-key">Put node public key here</textarea><br />
+ <input type="submit" value="Sign node key" />
+ </form>
+ {{range .}}
+ <a href="/ca/{{.Namespace}}/{{.Name}}"><h1>{{.Name}}</h1></a>
+ <table>
+ <tr>
+ <th>Node</th>
+ <th>IP</th>
+ </tr>
+ {{range .Nodes}}
+ <tr>
+ <td>
+ <a href="/node/{{.Namespace}}/{{.Name}}">{{.Name}}</a>
+ </td>
+ <td>
+ {{.IP}}
+ </td>
+ </tr>
+ {{end}}
+ </table>
+ {{end}}
+</body>
+</html>
+