From 39cd9d1f32cfea5d5ff4c0f1c01c9d17eb658432 Mon Sep 17 00:00:00 2001
From: Malte Bauch <malte.bauch@stud.h-da.de>
Date: Thu, 27 Jan 2022 15:31:29 +0100
Subject: [PATCH] build plugins within orchestrator

---
 build.go                   |  58 ++++++++++
 go.mod                     |   4 +-
 go.sum                     |   6 +
 grpc.go                    |  87 +++++++-------
 resources/plugin_deps.json | 230 +++++++++++++++++++++++++++++++++++++
 templates.go               |  34 +++++-
 write.go                   |  35 +++++-
 7 files changed, 406 insertions(+), 48 deletions(-)
 create mode 100644 resources/plugin_deps.json

diff --git a/build.go b/build.go
index 1c32d1b9..a71da01a 100644
--- a/build.go
+++ b/build.go
@@ -2,11 +2,13 @@ package csbi
 
 import (
 	"bufio"
+	"bytes"
 	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
+	"os/exec"
 	"path/filepath"
 	"time"
 
@@ -15,7 +17,9 @@ import (
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/pkg/archive"
+	"github.com/google/uuid"
 	"github.com/prometheus/client_golang/prometheus"
+	log "github.com/sirupsen/logrus"
 )
 
 // nolint
@@ -78,3 +82,57 @@ func print(rd io.Reader) error {
 
 	return scanner.Err()
 }
+
+// buildPlugin builds a go plugin from ygot generated go code provided within
+// a plugin folder.
+func buildPlugin(id uuid.UUID) error {
+	labels := prometheus.Labels{"type": spb.Type_PLUGIN.String()}
+	start := promStartHook(labels, buildsTotal)
+
+	var stderr bytes.Buffer
+	buildDir := id.String()
+
+	if err := executeGoCommand(buildDir, &stderr, []string{"go", "mod", "tidy"}); err != nil {
+		log.Error(stderr.String())
+		return err
+	}
+
+	stderr.Reset()
+
+	buildCommand := []string{
+		"go",
+		"build",
+		"-buildmode=plugin",
+		"-o",
+		"./plugin.so",
+		"./gostructs.go",
+	}
+	if err := executeGoCommand(buildDir, &stderr, buildCommand); err != nil {
+		log.Error(stderr.String())
+		return err
+	}
+	promEndHook(labels, start, buildDurationSecondsTotal, buildDurationSeconds)
+	return nil
+}
+
+/*
+executeGoCommand runs a go command. Therefore it creates a new *exec.Cmd and
+adds the provided build directory as string, a byte buffer and the build
+commands as string slice.
+
+Example for a build command slice:
+
+buildCommand := []string{
+    "build",
+    "-o",
+    outputPath,
+    sourcePath,
+}
+*/
+func executeGoCommand(dir string, stderr *bytes.Buffer, buildCommand []string) error {
+	cmd := exec.Command(buildCommand[0], buildCommand[1:]...)
+	cmd.Dir = dir
+	cmd.Stderr = stderr
+
+	return cmd.Run()
+}
diff --git a/go.mod b/go.mod
index ef96bc0a..9725933d 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,8 @@ module code.fbi.h-da.de/danet/csbi
 go 1.17
 
 require (
-	code.fbi.h-da.de/danet/api v0.2.5-0.20220120151437-a3719e95faf2
-	code.fbi.h-da.de/danet/gosdn v0.0.3-0.20220120152434-de2a634e03ba
+	code.fbi.h-da.de/danet/api v0.2.5-0.20220125160614-789e7e1c26f0
+	code.fbi.h-da.de/danet/gosdn v0.0.3-0.20220127123058-90495a469857
 	github.com/docker/docker v20.10.11+incompatible // as per https://github.com/moby/moby/issues/41191#issuecomment-656342401
 	github.com/google/uuid v1.2.0
 	github.com/mitchellh/go-homedir v1.1.0
diff --git a/go.sum b/go.sum
index 40c2da75..bb44a5bc 100644
--- a/go.sum
+++ b/go.sum
@@ -45,12 +45,18 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 code.fbi.h-da.de/danet/api v0.2.5-0.20220120151437-a3719e95faf2 h1:YABbazS13g70cftlTqZ1zzmAJr0kHmHIzMD32NXlaFY=
 code.fbi.h-da.de/danet/api v0.2.5-0.20220120151437-a3719e95faf2/go.mod h1:kjazkgCFLje+z4BBNBLlyozhQUnkJd0sqlZz1Axe0wM=
+code.fbi.h-da.de/danet/api v0.2.5-0.20220125160614-789e7e1c26f0 h1:QdTE7B6ScMWtPpwmqKvvwGTWsKyWXTR8AWTYu6AWLRA=
+code.fbi.h-da.de/danet/api v0.2.5-0.20220125160614-789e7e1c26f0/go.mod h1:kjazkgCFLje+z4BBNBLlyozhQUnkJd0sqlZz1Axe0wM=
 code.fbi.h-da.de/danet/forks/goarista v0.0.0-20210709163519-47ee8958ef40 h1:x7rVYGqfJSMWuYBp+JE6JVMcFP03Gx0mnR2ftsgqjVI=
 code.fbi.h-da.de/danet/forks/goarista v0.0.0-20210709163519-47ee8958ef40/go.mod h1:uVe3gCeF2DcIho8K9CIO46uAkHW/lUF+fAaUX1vHrF0=
 code.fbi.h-da.de/danet/forks/google v0.0.0-20210709163519-47ee8958ef40 h1:B45k5tGEdjjdsKK4f+0dQoyReFmsWdwYEzHofA7DPM8=
 code.fbi.h-da.de/danet/forks/google v0.0.0-20210709163519-47ee8958ef40/go.mod h1:Uutdj5aA3jpzfNm3C8gt2wctYE6cRrdyZsILUgJ+tMY=
 code.fbi.h-da.de/danet/gosdn v0.0.3-0.20220120152434-de2a634e03ba h1:e7SXJ+cf04cHaOC8+HAU9xO47vn9KfdCyAUMp5G1X3E=
 code.fbi.h-da.de/danet/gosdn v0.0.3-0.20220120152434-de2a634e03ba/go.mod h1:jlFu92Dx/AIuhERvZDKHX3ipmOVqON6g7I1gBt9RwF4=
+code.fbi.h-da.de/danet/gosdn v0.0.3-0.20220125173344-b64ac9efc3b9 h1:xCZQvil6G6PJEMV/tg2y5KTSVafVU19n2N1loQvvf40=
+code.fbi.h-da.de/danet/gosdn v0.0.3-0.20220125173344-b64ac9efc3b9/go.mod h1:/JfwV+FUs/bZZD3P1gvQ3EuwzvFSWxjtmf0UoVU/JmM=
+code.fbi.h-da.de/danet/gosdn v0.0.3-0.20220127123058-90495a469857 h1:iRFhTyTajxhn8QtPnlMYzRGqHwdsEAnKr7jNpZ36pUk=
+code.fbi.h-da.de/danet/gosdn v0.0.3-0.20220127123058-90495a469857/go.mod h1:/JfwV+FUs/bZZD3P1gvQ3EuwzvFSWxjtmf0UoVU/JmM=
 code.fbi.h-da.de/danet/yang-models v0.1.0 h1:C658HkGYZSV5Eq5nY2NnC/PQPKp3BaTXwGZICCr0sqk=
 code.fbi.h-da.de/danet/yang-models v0.1.0/go.mod h1:0TNkzPA1OW9lF9ey18GQWcMd4ORvOfhhFOA/t0SjenM=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
diff --git a/grpc.go b/grpc.go
index d504dbb5..1793f1da 100644
--- a/grpc.go
+++ b/grpc.go
@@ -12,6 +12,7 @@ import (
 	"github.com/google/uuid"
 	"github.com/prometheus/client_golang/prometheus"
 	log "github.com/sirupsen/logrus"
+	"google.golang.org/grpc"
 	codes "google.golang.org/grpc/codes"
 	status "google.golang.org/grpc/status"
 )
@@ -31,6 +32,11 @@ const (
 	YB
 )
 
+type pluginStream interface {
+	Send(*pb.Payload) error
+	grpc.ServerStream
+}
+
 type server struct {
 	pb.UnimplementedCsbiServer
 	orchestrator Orchestrator
@@ -89,8 +95,9 @@ func (s server) Create(ctx context.Context, req *pb.CreateRequest) (*pb.CreateRe
 	}, nil
 }
 
-func (s server) GetGoStruct(req *pb.GetRequest, stream pb.Csbi_GetGoStructServer) error {
-	log.Info("started GetGoStruct")
+// TODO(maba): add description and consider to allow requesting
+func (s server) GetPlugin(req *pb.GetRequest, stream pb.Csbi_GetPluginServer) error {
+	log.Info("started GetPlugin")
 	labels := prometheus.Labels{"rpc": "get_go_struct"}
 	start := promStartHook(labels, grpcRequestsTotal)
 	defer promEndHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)
@@ -101,33 +108,11 @@ func (s server) GetGoStruct(req *pb.GetRequest, stream pb.Csbi_GetGoStructServer
 		return handleRPCError(labels, err)
 	}
 
-	file, err := os.Open(filepath.Join(dep.ID.String(), "gostructs.go"))
-	if err != nil {
-		return handleRPCError(labels, err)
-	}
-	defer file.Close()
-
-	buffer := make([]byte, int(MB))
-
-	for {
-		n, err := file.Read(buffer)
-		if err != nil {
-			if err != io.EOF {
-				fmt.Println(err)
-			}
-			break
-		}
-		log.WithField("n", n).Trace("read bytes")
-		payload := &pb.Payload{Chunk: buffer[:n]}
-		err = stream.Send(payload)
-		if err != nil {
-			return handleRPCError(labels, err)
-		}
-	}
-	return nil
+	return sendPlugin(dep.ID, labels, stream)
 }
-func (s server) CreateGoStruct(req *pb.CreateRequest, stream pb.Csbi_CreateGoStructServer) error {
-	log.Info("started CreateGoStruct")
+
+func (s server) CreatePlugin(req *pb.CreateRequest, stream pb.Csbi_CreatePluginServer) error {
+	log.Info("started CreatePlugin")
 	labels := prometheus.Labels{"rpc": "create_plugin"}
 	start := promStartHook(labels, grpcRequestsTotal)
 	defer promEndHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)
@@ -142,30 +127,44 @@ func (s server) CreateGoStruct(req *pb.CreateRequest, stream pb.Csbi_CreateGoStr
 		if err != nil {
 			return handleRPCError(labels, err)
 		}
-		file, err := os.Open(filepath.Join(d.ID.String(), "gostructs.go"))
+		buildPlugin(d.ID)
 		if err != nil {
 			return handleRPCError(labels, err)
 		}
-		defer file.Close()
+		err = sendPlugin(d.ID, labels, stream)
+		if err != nil {
+			return handleRPCError(labels, err)
+		}
+	}
+	return nil
+}
 
-		buffer := make([]byte, int(MB))
+// sendPlugin takes a
+func sendPlugin(id uuid.UUID, labels prometheus.Labels, stream pluginStream) error {
+	file, err := os.Open(filepath.Join(id.String(), "plugin.so"))
+	if err != nil {
+		return handleRPCError(labels, err)
+	}
+	defer file.Close()
 
-		for {
-			n, err := file.Read(buffer)
-			if err != nil {
-				if err != io.EOF {
-					fmt.Println(err)
-				}
-				break
-			}
-			log.WithField("n", n).Trace("read bytes")
-			payload := &pb.Payload{Chunk: buffer[:n]}
-			err = stream.Send(payload)
-			if err != nil {
-				return handleRPCError(labels, err)
+	buffer := make([]byte, int(MB))
+
+	for {
+		n, err := file.Read(buffer)
+		if err != nil {
+			if err != io.EOF {
+				fmt.Println(err)
 			}
+			break
+		}
+		log.WithField("n", n).Trace("read bytes")
+		payload := &pb.Payload{Chunk: buffer[:n]}
+		err = stream.Send(payload)
+		if err != nil {
+			return handleRPCError(labels, err)
 		}
 	}
+
 	return nil
 }
 
diff --git a/resources/plugin_deps.json b/resources/plugin_deps.json
new file mode 100644
index 00000000..e8e4e413
--- /dev/null
+++ b/resources/plugin_deps.json
@@ -0,0 +1,230 @@
+{
+	"Module": {
+		"Path": "code.fbi.h-da.de/danet/plugin-sbi"
+	},
+	"Go": "1.17",
+	"Require": [
+	 		{
+			"Path": "code.fbi.h-da.de/danet/gosdn",
+			"Version": "v0.0.3-0.20220127123058-90495a469857"
+		},
+		{
+			"Path": "code.fbi.h-da.de/danet/api",
+			"Version": "v0.2.5-0.20220125160614-789e7e1c26f0"
+		},
+		{
+			"Path": "code.fbi.h-da.de/danet/forks/goarista",
+			"Version": "v0.0.0-20210709163519-47ee8958ef40"
+		},
+		{
+			"Path": "code.fbi.h-da.de/danet/forks/google",
+			"Version": "v0.0.0-20210709163519-47ee8958ef40"
+		},
+		{
+			"Path": "code.fbi.h-da.de/danet/yang-models",
+			"Version": "v0.1.0"
+		},
+		{
+			"Path": "github.com/google/uuid",
+			"Version": "v1.2.0"
+		},
+		{
+			"Path": "github.com/openconfig/gnmi",
+			"Version": "v0.0.0-20210914185457-51254b657b7d"
+		},
+		{
+			"Path": "github.com/openconfig/goyang",
+			"Version": "v0.3.1"
+		},
+		{
+			"Path": "github.com/openconfig/ygot",
+			"Version": "v0.12.5"
+		},
+		{
+			"Path": "github.com/prometheus/client_golang",
+			"Version": "v1.9.0"
+		},
+		{
+			"Path": "github.com/sirupsen/logrus",
+			"Version": "v1.8.1"
+		},
+		{
+			"Path": "github.com/spf13/cobra",
+			"Version": "v1.1.3"
+		},
+		{
+			"Path": "github.com/spf13/viper",
+			"Version": "v1.9.0"
+		},
+		{
+			"Path": "github.com/stretchr/objx",
+			"Version": "v0.2.0",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/stretchr/testify",
+			"Version": "v1.7.0"
+		},
+		{
+			"Path": "google.golang.org/grpc",
+			"Version": "v1.43.0"
+		},
+		{
+			"Path": "google.golang.org/protobuf",
+			"Version": "v1.27.1"
+		},
+		{
+			"Path": "github.com/beorn7/perks",
+			"Version": "v1.0.1",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/cespare/xxhash/v2",
+			"Version": "v2.1.1",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/davecgh/go-spew",
+			"Version": "v1.1.1",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/fsnotify/fsnotify",
+			"Version": "v1.5.1",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/golang/glog",
+			"Version": "v1.0.0",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/golang/protobuf",
+			"Version": "v1.5.2",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/google/go-cmp",
+			"Version": "v0.5.6",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/hashicorp/hcl",
+			"Version": "v1.0.0",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/inconshreveable/mousetrap",
+			"Version": "v1.0.0",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/kylelemons/godebug",
+			"Version": "v1.1.0",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/magiconair/properties",
+			"Version": "v1.8.5",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/matttproud/golang_protobuf_extensions",
+			"Version": "v1.0.1",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/mitchellh/mapstructure",
+			"Version": "v1.4.2",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/pelletier/go-toml",
+			"Version": "v1.9.4",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/pmezard/go-difflib",
+			"Version": "v1.0.0",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/prometheus/client_model",
+			"Version": "v0.2.0",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/prometheus/common",
+			"Version": "v0.18.0",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/prometheus/procfs",
+			"Version": "v0.6.0",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/spf13/afero",
+			"Version": "v1.6.0",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/spf13/cast",
+			"Version": "v1.4.1",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/spf13/jwalterweatherman",
+			"Version": "v1.1.0",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/spf13/pflag",
+			"Version": "v1.0.5",
+			"Indirect": true
+		},
+		{
+			"Path": "github.com/subosito/gotenv",
+			"Version": "v1.2.0",
+			"Indirect": true
+		},
+		{
+			"Path": "golang.org/x/net",
+			"Version": "v0.0.0-20211123203042-d83791d6bcd9",
+			"Indirect": true
+		},
+		{
+			"Path": "golang.org/x/sys",
+			"Version": "v0.0.0-20211123173158-ef496fb156ab",
+			"Indirect": true
+		},
+		{
+			"Path": "golang.org/x/text",
+			"Version": "v0.3.7",
+			"Indirect": true
+		},
+		{
+			"Path": "google.golang.org/genproto",
+			"Version": "v0.0.0-20211208223120-3a66f561d7aa",
+			"Indirect": true
+		},
+		{
+			"Path": "gopkg.in/ini.v1",
+			"Version": "v1.64.0",
+			"Indirect": true
+		},
+		{
+			"Path": "gopkg.in/yaml.v2",
+			"Version": "v2.4.0",
+			"Indirect": true
+		},
+		{
+			"Path": "gopkg.in/yaml.v3",
+			"Version": "v3.0.0-20210107192922-496545a6307b",
+			"Indirect": true
+		}
+	],
+	"Exclude": null,
+	"Replace": null,
+	"Retract": null
+}
diff --git a/templates.go b/templates.go
index 04933bce..be0f7345 100644
--- a/templates.go
+++ b/templates.go
@@ -1,6 +1,10 @@
 package csbi
 
-import "github.com/openconfig/ygot/ygen"
+import (
+	"html/template"
+
+	"github.com/openconfig/ygot/ygen"
+)
 
 var pluginStruct = ygen.GoStructCodeSnippet{
 	StructName: "Csbi",
@@ -100,3 +104,31 @@ const pluginImportAmendmend = `
 
 var PluginSymbol Csbi
 `
+
+var templater *template.Template
+
+const pluginGoModTemplate = `module << .Module.Path >>
+
+go << .GoVersion >>
+
+require (<<range $element := .Dependencies>>
+	<<$element.Path>> <<$element.Version>>
+<<end>>)
+`
+
+type Module struct {
+	Path string `json:"Path"`
+}
+
+type Dependency struct {
+	Path    string `json:"Path"`
+	Version string `json:"Version"`
+	//Indirect bool   `json:"Indirect,omitempty"`
+}
+
+// GoMod represents a go.mod file used for templates.
+type GoMod struct {
+	Module       Module       `json:"Module"`
+	GoVersion    string       `json:"Go"`
+	Dependencies []Dependency `json:"Require"`
+}
diff --git a/write.go b/write.go
index d3233ae7..b5cb947b 100644
--- a/write.go
+++ b/write.go
@@ -3,7 +3,9 @@ package csbi
 import (
 	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
+	"html/template"
 	"io/fs"
 	"net"
 	"os"
@@ -91,7 +93,11 @@ func writeCsbi(ctx context.Context, code *ygen.GeneratedGoCode, path string) err
 }
 
 func writePlugin(code *ygen.GeneratedGoCode, path string) error {
-	return writeCode(path, code)
+	err := writeCode(path, code)
+	if err != nil {
+		return err
+	}
+	return writeGoMod(path)
 }
 
 func copyFile(path, filename string) error {
@@ -147,3 +153,30 @@ func writeCode(path string, code *ygen.GeneratedGoCode) error {
 	}
 	return nil
 }
+
+func writeGoMod(path string) error {
+	// Read dependencies from JSON file
+	deps, err := os.ReadFile(filepath.Join("resources", "plugin_deps.json"))
+	if err != nil {
+		return err
+	}
+	module := GoMod{}
+	if err := json.Unmarshal(deps, &module); err != nil {
+		return err
+	}
+
+	// Create go.mod in destination directory and write template
+	file := filepath.Join(path, "go.mod")
+	goMod, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY, 0755)
+	if err != nil {
+		return err
+	}
+	defer goMod.Sync()
+
+	templater := template.New("goMod").Delims("<<", ">>")
+	_, err = templater.Parse(pluginGoModTemplate)
+	if err != nil {
+		return err
+	}
+	return templater.Execute(goMod, module)
+}
-- 
GitLab