DEVELOPMENT ENVIRONMENT

~liljamo/emerwen-web

6363d38d035b8b64a35fd40586ab563f07d46601 — Jonni Liljamo 3 days ago a206355
feat: gRPC client and fetch basic worker details
M cmd/emerwen-web/main.go => cmd/emerwen-web/main.go +4 -1
@@ 14,6 14,7 @@ import (
	"time"

	"git.src.quest/~liljamo/emerwen-web/internal/auth"
	"git.src.quest/~liljamo/emerwen-web/internal/client"
	"git.src.quest/~liljamo/emerwen-web/internal/config"
	"git.src.quest/~liljamo/emerwen-web/internal/log"
	"git.src.quest/~liljamo/emerwen-web/internal/router"


@@ 37,10 38,12 @@ func main() {
	sm = scs.New()
	sm.Lifetime = 1 * time.Hour

	client := client.NewClient(&c)

	slog.Info("serving", slog.String("bindaddr", c.BindAddr))
	s := &http.Server{
		Addr:    c.BindAddr,
		Handler: sm.LoadAndSave(router.SetupRouter(l, a, sm)),
		Handler: sm.LoadAndSave(router.SetupRouter(l, a, sm, client)),
	}

	g.Go(func() error {

M go.mod => go.mod +10 -7
@@ 1,15 1,17 @@
module git.src.quest/~liljamo/emerwen-web

go 1.23.2
go 1.23.3

require (
	git.src.quest/~liljamo/emerwen-proto v0.0.0-20241118080237-06cafd5cb729
	github.com/a-h/templ v0.2.793
	github.com/alexedwards/scs/v2 v2.8.0
	github.com/coreos/go-oidc/v3 v3.11.0
	github.com/gin-gonic/gin v1.10.0
	github.com/samber/slog-gin v1.13.6
	golang.org/x/oauth2 v0.21.0
	golang.org/x/oauth2 v0.24.0
	golang.org/x/sync v0.8.0
	google.golang.org/grpc v1.68.0
)

require (


@@ 37,10 39,11 @@ require (
	go.opentelemetry.io/otel v1.29.0 // indirect
	go.opentelemetry.io/otel/trace v1.29.0 // indirect
	golang.org/x/arch v0.8.0 // indirect
	golang.org/x/crypto v0.26.0 // indirect
	golang.org/x/net v0.28.0 // indirect
	golang.org/x/sys v0.23.0 // indirect
	golang.org/x/text v0.17.0 // indirect
	google.golang.org/protobuf v1.34.2 // indirect
	golang.org/x/crypto v0.27.0 // indirect
	golang.org/x/net v0.29.0 // indirect
	golang.org/x/sys v0.25.0 // indirect
	golang.org/x/text v0.18.0 // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
	google.golang.org/protobuf v1.35.2 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

M go.sum => go.sum +20 -12
@@ 1,3 1,5 @@
git.src.quest/~liljamo/emerwen-proto v0.0.0-20241118080237-06cafd5cb729 h1:Zka776qZfycsq3f6VbDrbou3egXl2mCRFp0JkfH1VyY=
git.src.quest/~liljamo/emerwen-proto v0.0.0-20241118080237-06cafd5cb729/go.mod h1:TZkTqP3/rDTcJDTDU61QvV+4+TsZIvW0lt4hOiYCrr0=
github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY=
github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=


@@ 33,6 35,8 @@ github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=


@@ 86,22 90,26 @@ go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+M
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

A internal/client/client.go => internal/client/client.go +42 -0
@@ 0,0 1,42 @@
/*
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// Package client provides a type wrapper for the emerwen gRPC WebToMasterClient.
package client

import (
	"context"
	"fmt"
	"log/slog"

	"git.src.quest/~liljamo/emerwen-proto/go/proto"
	"git.src.quest/~liljamo/emerwen-web/internal/config"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
)

// Client wraps the emerwen gRPC WebToMasterClient, along with a Context.
type Client struct {
	Client proto.WebToMasterClient
	Ctx    context.Context
}

// NewClient constructs a new Client.
func NewClient(c *config.Config) *Client {
	conn, err := grpc.NewClient(fmt.Sprintf("unix://%s", c.SocketPath), grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		slog.Error("yay")
	}

	client := proto.NewWebToMasterClient(conn)

	// TODO: config read auth_token from file
	ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer avain_perkele")

	return &Client{Client: client, Ctx: ctx}
}

A internal/components/components.templ => internal/components/components.templ +34 -0
@@ 0,0 1,34 @@
package components

import "fmt"

script toggleKeyVisibility(id string) {
	var input = document.getElementById(id);
	var eye_vis = document.getElementById("eye_visible_" + id);
	var eye_hid = document.getElementById("eye_hidden_" + id);
	if (input.type === "password") {
		input.type = "text";
	} else {
		input.type = "password";
	};
	eye_vis.classList.toggle("hidden");
	eye_hid.classList.toggle("hidden");
}

templ eyeToggle(t string) {
	<div class="border p-1 flex items-center justify-center">
		<input class="absolute w-6 h-6 appearance-none cursor-pointer" type="checkbox"
			onClick={ toggleKeyVisibility(t) }
		/>
		<div>
			<svg id={ fmt.Sprintf("eye_visible_%s", t) } class="hidden w-5 h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
				<path d="M15.0007 12C15.0007 13.6569 13.6576 15 12.0007 15C10.3439 15 9.00073 13.6569 9.00073 12C9.00073 10.3431 10.3439 9 12.0007 9C13.6576 9 15.0007 10.3431 15.0007 12Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
				<path d="M12.0012 5C7.52354 5 3.73326 7.94288 2.45898 12C3.73324 16.0571 7.52354 19 12.0012 19C16.4788 19 20.2691 16.0571 21.5434 12C20.2691 7.94291 16.4788 5 12.0012 5Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
			</svg>

			<svg id={ fmt.Sprintf("eye_hidden_%s", t) } class="w-5 h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
				<path d="M2.99902 3L20.999 21M9.8433 9.91364C9.32066 10.4536 8.99902 11.1892 8.99902 12C8.99902 13.6569 10.3422 15 11.999 15C12.8215 15 13.5667 14.669 14.1086 14.133M6.49902 6.64715C4.59972 7.90034 3.15305 9.78394 2.45703 12C3.73128 16.0571 7.52159 19 11.9992 19C13.9881 19 15.8414 18.4194 17.3988 17.4184M10.999 5.04939C11.328 5.01673 11.6617 5 11.9992 5C16.4769 5 20.2672 7.94291 21.5414 12C21.2607 12.894 20.8577 13.7338 20.3522 14.5" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
			</svg>
		</div>
	</div>
}

M internal/components/index.templ => internal/components/index.templ +3 -1
@@ 3,7 3,9 @@ package components
templ Index() {
	@Base("emerwen, counting sheep") {
		<div class="flex flex-col gap-2">
			<button class="border p-1 w-max">sheep: 0</button>
			if loggedIn(ctx) {
				<div hx-get="/partials/workers" hx-trigger="load" hx-target="this"></div>
			}
		</div>
	}
}

A internal/components/partials.templ => internal/components/partials.templ +66 -0
@@ 0,0 1,66 @@
package components

import "git.src.quest/~liljamo/emerwen-proto/go/proto/shared"
import "fmt"

func getMethodString(target *shared.Target) string {
	switch target.Method.(type) {
	case *shared.Target_Ping:
		return "Ping"
	case *shared.Target_Get:
		return "Get"
	default:
		return "fuck"
	}
}

templ PartialWorkers(workers []*shared.Worker) {
	<div class="flex flex-col gap-2">
		for _, worker := range workers {
			<div class="flex flex-col bg-slate-100 p-2">
        <div class="flex gap-1">
          ID: { fmt.Sprintf("%d", worker.Id) }
        </div>
        <div class="flex items-center gap-1 border p-1">
          <div>Auth Token: </div>
          <input class="border" disabled type="password" value={ worker.AuthToken }
						id={ fmt.Sprintf("auth_key_%d", worker.Id) }
					/>
					@eyeToggle(fmt.Sprintf("auth_key_%d", worker.Id))
        </div>
				<details>
					<summary>Targets:</summary>
					<div class="flex flex-col gap-1 p-2">
						for _, target := range worker.Targets {
							<div class="flex flex-col bg-slate-200 p-2">
								<div class="flex gap-1">
									ID: { fmt.Sprintf("%d", target.Id) }
								</div>
								<div class="flex items-center gap-1 border p-1">
									<div>Addr: </div>
									<input class="border" value={ target.Addr }/>
								</div>
								<div class="flex items-center gap-1 border p-1">
									<div>Interval (ms): </div>
									<input class="border" value={ fmt.Sprint(target.Interval) }/>
								</div>
								<div class="flex flex-col gap-1">
									<div>Method:</div>
									switch m := target.Method.(type) {
									case *shared.Target_Ping:
										<div>Ping</div>
									case *shared.Target_Get:
										<div>Get</div>
										<input class="border" value={ fmt.Sprint(m.Get.OkCodes) }/>
									default:
										<div>fuck</div>
									}
								</div>
							</div>
						}
					</div>
				</details>
      </div>
		}
	</div>
}

M internal/config/config.go => internal/config/config.go +5 -2
@@ 17,7 17,8 @@ import (

// Config holds application configuration values.
type Config struct {
	BindAddr string
	BindAddr   string
	SocketPath string

	OIDCClientID     string
	OIDCClientSecret string


@@ 31,6 32,7 @@ type Config struct {
// a file.
func ParseFromArgs() Config {
	bindAddrPtr := flag.String("bind_address", "127.0.0.1:3000", "bind address")
	socketPathPtr := flag.String("socket_path", "/tmp/emerwen/master.sock", "master socket path")

	oidcClientIDPtr := flag.String("oidc_client_id", "emerwen", "OIDC client ID")
	oidcClientIDFilePtr := flag.String("oidc_client_id_file", "", "OIDC client ID file")


@@ 70,7 72,8 @@ func ParseFromArgs() Config {
	}

	return Config{
		BindAddr: *bindAddrPtr,
		BindAddr:   *bindAddrPtr,
		SocketPath: *socketPathPtr,

		OIDCClientID:     oidcClientID,
		OIDCClientSecret: oidcClientSecret,

A internal/handlers/partials.go => internal/handlers/partials.go +54 -0
@@ 0,0 1,54 @@
/*
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

package handlers

import (
	"fmt"
	"log/slog"
	"net/http"

	"git.src.quest/~liljamo/emerwen-web/internal/client"
	"git.src.quest/~liljamo/emerwen-web/internal/components"
	"git.src.quest/~liljamo/emerwen-web/internal/renderer"
	"github.com/gin-gonic/gin"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"
)

func handleClientError(c *gin.Context, err error) {
	if status, ok := status.FromError(err); ok {
		slog.Error("RPC error", slog.String("err", err.Error()))

		if status.Code() == codes.Unavailable {
			c.String(http.StatusInternalServerError, "RPC unavailable.")
			return
		}

		c.String(http.StatusInternalServerError, fmt.Sprintf("RPC error: %s", status.String()))
		return
	}

	slog.Error("Non-RPC error", slog.String("err", err.Error()))

	c.String(http.StatusInternalServerError, "Non-RPC error, see server logs.")
}

// PartialWorkers returns a gin handler for the partial workers route.
func PartialWorkers(client *client.Client) gin.HandlerFunc {
	return func(c *gin.Context) {
		response, err := client.Client.GetWorkers(client.Ctx, &emptypb.Empty{})
		if err != nil {
			handleClientError(c, err)
			return
		}

		r := renderer.New(BaseContext(c), http.StatusOK, components.PartialWorkers(response.Workers))
		c.Render(http.StatusOK, r)
	}
}

M internal/router/router.go => internal/router/router.go +7 -1
@@ 13,6 13,7 @@ import (
	"log/slog"

	"git.src.quest/~liljamo/emerwen-web/internal/auth"
	"git.src.quest/~liljamo/emerwen-web/internal/client"
	"git.src.quest/~liljamo/emerwen-web/internal/handlers"
	"git.src.quest/~liljamo/emerwen-web/internal/middlewares"
	"git.src.quest/~liljamo/emerwen-web/internal/renderer"


@@ 22,7 23,7 @@ import (
)

// SetupRouter returns a gin engine.
func SetupRouter(l *slog.Logger, a *auth.Auth, sm *scs.SessionManager) *gin.Engine {
func SetupRouter(l *slog.Logger, a *auth.Auth, sm *scs.SessionManager, client *client.Client) *gin.Engine {
	r := gin.New()
	r.Use(gin.Recovery())
	r.Use(sloggin.New(l))


@@ 41,5 42,10 @@ func SetupRouter(l *slog.Logger, a *auth.Auth, sm *scs.SessionManager) *gin.Engi
	r.GET("/logout", handlers.Logout(sm))
	r.GET("/oauth2/oidc/callback", handlers.OAuth2OIDCCallback(a, sm))

	s := r.Group("/", middlewares.RequireSession(sm))
	{
		s.GET("/partials/workers", handlers.PartialWorkers(client))
	}

	return r
}