diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..9a6270e Binary files /dev/null and b/.DS_Store differ diff --git a/k8s/k8s.go b/k8s/k8s.go new file mode 100644 index 0000000..e828500 --- /dev/null +++ b/k8s/k8s.go @@ -0,0 +1,108 @@ +package k8s + +import ( + "bytes" + "embed" + "fmt" + "text/template" + + "github.com/flashbots/builder-playground/playground" + "github.com/mitchellh/mapstructure" + "github.com/spf13/cobra" +) + +//go:embed templates/* +var templates embed.FS + +var outputFlag string + +func init() { + K8sCommand.Flags().StringVar(&outputFlag, "output", "", "Output folder for the artifacts") +} + +var K8sCommand = &cobra.Command{ + Use: "k8s", + Short: "Kubernetes commands", + RunE: func(cmd *cobra.Command, args []string) error { + var err error + if outputFlag == "" { + if outputFlag, err = playground.GetHomeDir(); err != nil { + return err + } + } + + manifest, err := playground.ReadManifest(outputFlag) + if err != nil { + return err + } + + out, err := applyTemplate("namespace", manifest) + if err != nil { + return err + } + fmt.Println(out) + + for _, svc := range manifest.Services { + if err := createService(manifest, svc); err != nil { + return err + } + } + + return nil + }, +} + +func createService(manifest *playground.Manifest, svc *playground.Service) error { + funcs := template.FuncMap{ + "Service": func(name string, portLabel, protocol, user string) string { + target := manifest.MustGetService(name) + return playground.PrintAddr(protocol, name, target.MustGetPort(portLabel).Port, user) + }, + "Port": func(name string, defaultPort int) int { + return defaultPort + }, + "PortUDP": func(name string, defaultPort int) int { + return defaultPort + }, + } + + newArgs, err := svc.ReplaceArgs(funcs) + if err != nil { + return fmt.Errorf("failed to replace args: %w", err) + } + svc.Args = newArgs + + var input map[string]interface{} + if err := mapstructure.Decode(svc, &input); err != nil { + return fmt.Errorf("failed to decode service: %w", err) + } + + // add more context data + input["Namespace"] = manifest.Name + + res, err := applyTemplate("deployment", input) + if err != nil { + return fmt.Errorf("failed to apply service template: %w", err) + } + fmt.Println(res) + + return nil +} + +func applyTemplate(templateName string, input interface{}) (string, error) { + content, err := templates.ReadFile(fmt.Sprintf("templates/%s.yaml.tmpl", templateName)) + if err != nil { + return "", fmt.Errorf("failed to open template: %w", err) + } + + tmpl, err := template.New(templateName).Parse(string(content)) + if err != nil { + return "", fmt.Errorf("failed to parse template: %w", err) + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, input); err != nil { + return "", fmt.Errorf("failed to execute template: %w", err) + } + return buf.String(), nil +} diff --git a/k8s/templates/configmap.yaml.tmpl b/k8s/templates/configmap.yaml.tmpl new file mode 100644 index 0000000..6104ff5 --- /dev/null +++ b/k8s/templates/configmap.yaml.tmpl @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Name }}-config + namespace: {{ .Namespace }} + labels: + app: {{ .Name }} +data: + {{- if .Env }} + # Environment variables as config + {{- range $key, $value := .Env }} + {{ $key }}: {{ $value }} + {{- end }} + {{- end }} + {{- if .FilesMapped }} + # Mapped files as config + {{- range $path, $name := .FilesMapped }} + {{ $name }}: | + {{- $content := readFile $path }} + {{ $content | indent 4 }} + {{- end }} + {{- end }} diff --git a/k8s/templates/deployment.yaml.tmpl b/k8s/templates/deployment.yaml.tmpl new file mode 100644 index 0000000..26322b7 --- /dev/null +++ b/k8s/templates/deployment.yaml.tmpl @@ -0,0 +1,58 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Name }} + namespace: {{ .Namespace }} + labels: + app: {{ .Name }} + playground: "true" +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Name }} + template: + metadata: + labels: + app: {{ .Name }} + playground: "true" + spec: + containers: + - name: {{ .Name }} + image: {{ .Image }}:{{ .Tag }} + {{- if .Entrypoint }} + command: ["{{ .Entrypoint }}"] + {{- end }} + {{- if .Args }} + args: [{{ range $i, $arg := .Args }}{{if $i}}, {{end}}"{{ $arg }}"{{ end }}] + {{- end }} + {{- if .Env }} + env: + {{- range $key, $value := .Env }} + - name: {{ $key }} + value: "{{ $value }}" + {{- end }} + {{- end }} + {{- if .Ports }} + ports: + {{- range .Ports }} + - containerPort: {{ .Port }} + protocol: {{ if eq .Protocol "udp" }}UDP{{ else }}TCP{{ end }} + name: {{ if eq .Protocol "udp" }}{{ .Name }}-udp{{ else }}{{ .Name }}{{ end }} + {{- end }} + {{- end }} + {{- if .VolumesMapped }} + volumeMounts: + {{- range $path, $name := .VolumesMapped }} + - name: {{ $name }} + mountPath: {{ $path }} + {{- end }} + {{- end }} + {{- if .VolumesMapped }} + volumes: + {{- range $path, $name := .VolumesMapped }} + - name: {{ $name }} + emptyDir: {} + {{- end }} + {{- end }} +--- \ No newline at end of file diff --git a/k8s/templates/namespace.yaml.tmpl b/k8s/templates/namespace.yaml.tmpl new file mode 100644 index 0000000..b5c35d5 --- /dev/null +++ b/k8s/templates/namespace.yaml.tmpl @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Name }} + labels: + playground: "true" + name: {{ .Name }} +--- \ No newline at end of file diff --git a/k8s/templates/service.yaml.tmpl b/k8s/templates/service.yaml.tmpl new file mode 100644 index 0000000..a1a3997 --- /dev/null +++ b/k8s/templates/service.yaml.tmpl @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Name }} + namespace: {{ .Namespace }} + labels: + app: {{ .Name }} +spec: + ports: + {{- range .Ports }} + - port: {{ .Port }} + name: {{ .Name }} + {{- end }} + selector: + app: {{ .Name }} +--- \ No newline at end of file diff --git a/k8s/templates/statefulset.yaml.tmpl b/k8s/templates/statefulset.yaml.tmpl new file mode 100644 index 0000000..9eb97bc --- /dev/null +++ b/k8s/templates/statefulset.yaml.tmpl @@ -0,0 +1,78 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Name }} + namespace: {{ .Namespace }} + labels: + app: {{ .Name }} +spec: + ports: + {{- range .Ports }} + - port: {{ .Port }} + name: {{ .Name }} + {{- end }} + clusterIP: None + selector: + app: {{ .Name }} +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ .Name }} + namespace: {{ .Namespace }} + labels: + app: {{ .Name }} +spec: + serviceName: "{{ .Name }}" + replicas: 1 + selector: + matchLabels: + app: {{ .Name }} + template: + metadata: + labels: + app: {{ .Name }} + spec: + containers: + - name: {{ .Name }} + image: {{ .Image }}:{{ .Tag }} + {{- if .Entrypoint }} + command: ["{{ .Entrypoint }}"] + {{- end }} + {{- if .Args }} + args: + {{- range .Args }} + - {{ . }} + {{- end }} + {{- end }} + {{- if .Env }} + env: + {{- range $key, $value := .Env }} + - name: {{ $key }} + value: {{ $value }} + {{- end }} + {{- end }} + ports: + {{- range .Ports }} + - containerPort: {{ .Port }} + name: {{ .Name }} + {{- end }} + {{- if .VolumesMapped }} + volumeMounts: + {{- range $path, $name := .VolumesMapped }} + - name: {{ $name }} + mountPath: {{ $path }} + {{- end }} + {{- end }} + {{- if .VolumesMapped }} + volumeClaimTemplates: + {{- range $path, $name := .VolumesMapped }} + - metadata: + name: {{ $name }} + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi + {{- end }} + {{- end }} diff --git a/main.go b/main.go index 8e2784e..0b38f7a 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/flashbots/builder-playground/k8s" "github.com/flashbots/builder-playground/playground" "github.com/spf13/cobra" ) @@ -29,6 +30,7 @@ var withPrometheus bool var networkName string var labels playground.MapStringFlag var disableLogs bool +var nameFlag string var rootCmd = &cobra.Command{ Use: "playground", @@ -177,6 +179,7 @@ func main() { recipeCmd.Flags().StringVar(&networkName, "network", "", "network name") recipeCmd.Flags().Var(&labels, "labels", "list of labels to apply to the resources") recipeCmd.Flags().BoolVar(&disableLogs, "disable-logs", false, "disable logs") + recipeCmd.Flags().StringVar(&nameFlag, "name", "", "name of the recipe") cookCmd.AddCommand(recipeCmd) } @@ -189,6 +192,7 @@ func main() { rootCmd.AddCommand(artifactsCmd) rootCmd.AddCommand(artifactsAllCmd) rootCmd.AddCommand(inspectCmd) + rootCmd.AddCommand(k8s.K8sCommand) if err := rootCmd.Execute(); err != nil { fmt.Println(err) @@ -223,6 +227,7 @@ func runIt(recipe playground.Recipe) error { } svcManager := recipe.Apply(&playground.ExContext{LogLevel: logLevel}, artifacts) + svcManager.Name = nameFlag if err := svcManager.Validate(); err != nil { return fmt.Errorf("failed to validate manifest: %w", err) } diff --git a/playground/local_runner.go b/playground/local_runner.go index 9d76e31..f6b54c3 100644 --- a/playground/local_runner.go +++ b/playground/local_runner.go @@ -492,14 +492,14 @@ func (d *LocalRunner) applyTemplate(s *Service) ([]string, map[string]string, er if d.isHostService(s.Name) { // A and B - return printAddr(protocol, "localhost", port.HostPort, user) + return PrintAddr(protocol, "localhost", port.HostPort, user) } else { if d.isHostService(svc.Name) { // D - return printAddr(protocol, "host.docker.internal", port.HostPort, user) + return PrintAddr(protocol, "host.docker.internal", port.HostPort, user) } // C - return printAddr(protocol, svc.Name, port.Port, user) + return PrintAddr(protocol, svc.Name, port.Port, user) } }, "Port": func(name string, defaultPort int) int { @@ -547,7 +547,7 @@ func (d *LocalRunner) applyTemplate(s *Service) ([]string, map[string]string, er return argsResult, envs, nil } -func printAddr(protocol, serviceName string, port int, user string) string { +func PrintAddr(protocol, serviceName string, port int, user string) string { var protocolPrefix string if protocol != "" { protocolPrefix = protocol + "://" diff --git a/playground/manifest.go b/playground/manifest.go index 8429327..7721229 100644 --- a/playground/manifest.go +++ b/playground/manifest.go @@ -25,6 +25,8 @@ type Recipe interface { // Manifest describes a list of services and their dependencies type Manifest struct { + Name string `json:"name"` + ctx *ExContext // list of Services @@ -330,6 +332,32 @@ func (s *Service) WithLabel(key, value string) *Service { return s } +func (s *Service) ReplaceArgs(funcs template.FuncMap) ([]string, error) { + runTemplate := func(arg string) (string, error) { + tpl, err := template.New("").Funcs(funcs).Parse(arg) + if err != nil { + return "", err + } + + var out strings.Builder + if err := tpl.Execute(&out, nil); err != nil { + return "", err + } + return out.String(), nil + } + + var argsResult []string + for _, arg := range s.Args { + newArg, err := runTemplate(arg) + if err != nil { + return nil, err + } + argsResult = append(argsResult, newArg) + } + + return argsResult, nil +} + func (s *Manifest) NewService(name string) *Service { return &Service{Name: name, Args: []string{}, Ports: []*Port{}, NodeRefs: []*NodeRef{}} } diff --git a/playground/manifest_test.go b/playground/manifest_test.go index 8431679..df822da 100644 --- a/playground/manifest_test.go +++ b/playground/manifest_test.go @@ -52,7 +52,7 @@ func TestNodeRefString(t *testing.T) { } for _, testCase := range testCases { - result := printAddr(testCase.protocol, testCase.service, testCase.port, testCase.user) + result := PrintAddr(testCase.protocol, testCase.service, testCase.port, testCase.user) if result != testCase.expected { t.Errorf("expected %s, got %s", testCase.expected, result) }