Skip to content

Commit

Permalink
Add ScriptCommands() for collecting commands
Browse files Browse the repository at this point in the history
Add the ScriptCmd* types for providing script commands
within hive and the ScriptCommands() method for pulling
them out from *Hive.

Signed-off-by: Jussi Maki <[email protected]>
  • Loading branch information
joamaki committed Oct 9, 2024
1 parent ec464f9 commit fc01caf
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 3 deletions.
32 changes: 31 additions & 1 deletion example/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import (
"math/rand"
"time"

"github.com/cilium/hive"
"github.com/cilium/hive/cell"
"github.com/cilium/hive/job"
"github.com/cilium/hive/script"
"github.com/cilium/stream"
)

Expand All @@ -20,7 +22,10 @@ var eventsCell = cell.Module(
"example-events",
"Provides a stream of example events",

cell.Provide(newExampleEvents),
cell.Provide(
newExampleEvents,
showEventsCommand,
),
)

type ExampleEvent struct {
Expand Down Expand Up @@ -86,3 +91,28 @@ func newExampleEvents(lc cell.Lifecycle, jobs job.Registry, health cell.Health)
lc.Append(g)
return es
}

// showEventsCommand defines the hive script command "events" that subscribes
// and shows 5 events.
func showEventsCommand(ee ExampleEvents) hive.ScriptCmdOut {
return hive.NewScriptCmd(
"events",
script.Command(
script.CmdUsage{Summary: "Show 5 events"},
func(s *script.State, args ...string) (script.WaitFunc, error) {
n := 5
ctx, cancel := context.WithCancel(s.Context())
defer cancel()
for e := range stream.ToChannel(ctx, ee) {
if n >= 0 {
s.Logf("%s\n", e)
} else {
cancel()
}
n--
}
return nil, nil
},
),
)
}
27 changes: 25 additions & 2 deletions example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package main

import (
"log/slog"
"os"

"github.com/spf13/cobra"

Expand Down Expand Up @@ -65,14 +66,36 @@ var (
return nil
},
}

// Define the "repl" command to run the application in an interactive
// read-eval-print-loop:
//
// $ go run . repl
// example> hive start
// time=2024-10-08T09:39:00.881+02:00 level=INFO msg=Starting
// ...
// example> events
// ...
// example> hive stop
replCmd = &cobra.Command{
Use: "repl",
Run: func(_ *cobra.Command, args []string) {
hive.RunRepl(Hive, os.Stdin, os.Stdout, "example> ")
},
}
)

func main() {
// Register all configuration flags in the hive to the command
Hive.RegisterFlags(cmd.Flags())

// Add the "hive" sub-command for inspecting the hive
cmd.AddCommand(Hive.Command())
cmd.AddCommand(
// Add the "hive" sub-command for inspecting the hive
Hive.Command(),

// Add the "repl" command to interactively run the application.
replCmd,
)

// And finally execute the command to parse the command-line flags and
// run the hive
Expand Down
17 changes: 17 additions & 0 deletions hive.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"go.uber.org/dig"

"github.com/cilium/hive/cell"
"github.com/cilium/hive/script"
)

type Options struct {
Expand Down Expand Up @@ -421,3 +422,19 @@ func (h *Hive) getEnvName(option string) string {
upper := strings.ToUpper(under)
return h.opts.EnvPrefix + upper
}

func (h *Hive) ScriptCommands(log *slog.Logger) (map[string]script.Cmd, error) {
if err := h.Populate(log); err != nil {
return nil, fmt.Errorf("failed to populate object graph: %s", err)
}
m := map[string]script.Cmd{}
m["hive"] = hiveScriptCmd(h, log)

// Gather the commands from the hive.
h.container.Invoke(func(sc ScriptCmds) {
for name, cmd := range sc.Map() {
m[name] = cmd
}
})
return m, nil
}
134 changes: 134 additions & 0 deletions script.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package hive

import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"time"

"github.com/cilium/hive/cell"
"github.com/cilium/hive/script"
"golang.org/x/term"
)

func NewScriptCmd(name string, cmd script.Cmd) ScriptCmdOut {
return ScriptCmdOut{ScriptCmd: ScriptCmd{name, cmd}}
}

func NewScriptCmds(cmds map[string]script.Cmd) (out ScriptCmdsOut) {
out.ScriptCmds = make([]ScriptCmd, 0, len(cmds))
for name, cmd := range cmds {
out.ScriptCmds = append(out.ScriptCmds, ScriptCmd{name, cmd})
}
return out
}

type ScriptCmd struct {
Name string
Cmd script.Cmd
}

type ScriptCmds struct {
cell.In

ScriptCmds []ScriptCmd `group:"script-commands"`
}

func (sc ScriptCmds) Map() map[string]script.Cmd {
m := make(map[string]script.Cmd, len(sc.ScriptCmds))
for _, c := range sc.ScriptCmds {
m[c.Name] = c.Cmd
}
return m
}

type ScriptCmdOut struct {
cell.Out

ScriptCmd ScriptCmd `group:"script-commands"`
}

type ScriptCmdsOut struct {
cell.Out

ScriptCmds []ScriptCmd `group:"script-commands,flatten"`
}

func hiveScriptCmd(h *Hive, log *slog.Logger) script.Cmd {
const defaultTimeout = time.Minute
return script.Command(
script.CmdUsage{
Summary: "inspect and manipulate the hive",
Args: "cmd args...",
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
if len(args) < 1 {
return nil, fmt.Errorf("hive cmd args...\n'cmd' is one of: start, stop, jobs or inspect")
}
switch args[0] {
case "start":
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
return nil, h.Start(log, ctx)
case "stop":
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
return nil, h.Stop(log, ctx)
}
return nil, fmt.Errorf("unknown hive command %q, expected one of: start, stop, jobs or inspect", args[0])
},
)
}

func RunRepl(h *Hive, in *os.File, out *os.File, prompt string) {
// Try to set the input into raw mode.
prev, err := term.MakeRaw(int(in.Fd()))
if err == nil {
defer term.Restore(int(in.Fd()), prev)
}
inout := struct {
io.Reader
io.Writer
}{in, out}
term := term.NewTerminal(inout, prompt)
log := slog.New(slog.NewTextHandler(term, nil))

cmds, err := h.ScriptCommands(log)
if err != nil {
log.Error("ScriptCommands()", "error", err)
return
}
for name, cmd := range script.DefaultCmds() {
cmds[name] = cmd
}

e := script.Engine{
Cmds: cmds,
Conds: nil,
}
s, err := script.NewState(context.TODO(), "/tmp", nil)
if err != nil {
log.Error("script.NewState", "error", err)
return
}
for {
line, err := term.ReadLine()
if err != nil {
if errors.Is(err, io.EOF) {
return
} else {
panic(err)
}
}
err = e.ExecuteLine(s, line, term)
if err != nil {
fmt.Fprintln(term, err.Error())
}
}
}
41 changes: 41 additions & 0 deletions script/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"time"

"github.com/cilium/hive/script/internal/diff"
"golang.org/x/term"
)

// DefaultCmds returns a set of broadly useful script commands.
Expand Down Expand Up @@ -49,6 +50,7 @@ func DefaultCmds() map[string]Cmd {
"stop": Stop(),
"symlink": Symlink(),
"wait": Wait(),
"break": Break(),
}
}

Expand Down Expand Up @@ -1124,3 +1126,42 @@ func (w waitError) Unwrap() error {
}
return nil
}

func Break() Cmd {
return Command(
CmdUsage{
Summary: "break into interactive prompt",
},
func(s *State, args ...string) (WaitFunc, error) {
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, fmt.Errorf("open /dev/tty: %w", err)
}
defer tty.Close()

prev, err := term.MakeRaw(int(tty.Fd()))
if err != nil {
return nil, fmt.Errorf("cannot set /dev/tty to raw mode")
}
defer term.Restore(int(tty.Fd()), prev)

// Flush any pending logs
engine := s.engine

term := term.NewTerminal(tty, "debug> ")
s.flushLog(term)
fmt.Fprintf(term, "\nBreak! Control-d to continue.\n")

for {
line, err := term.ReadLine()
if err != nil {
return nil, nil
}
err = engine.ExecuteLine(s, line, term)
if err != nil {
fmt.Fprintln(term, err.Error())
}
}
},
)
}
58 changes: 58 additions & 0 deletions script_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package hive_test

import (
"bufio"
"bytes"
"context"
"strings"
"testing"

"github.com/cilium/hive"
"github.com/cilium/hive/cell"
"github.com/cilium/hive/hivetest"
"github.com/cilium/hive/script"
"github.com/stretchr/testify/require"
)

func exampleCmd() hive.ScriptCmdOut {
return hive.NewScriptCmd(
"example",
script.Command(
script.CmdUsage{
Summary: "Example command",
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
s.Logf("hello")
return nil, nil
},
),
)
}

func TestScriptCommands(t *testing.T) {
h := hive.New(
cell.Provide(exampleCmd),
)
cmds, err := h.ScriptCommands(hivetest.Logger(t))
require.NoError(t, err, "ScriptCommands")
e := script.Engine{
Cmds: cmds,
}
s, err := script.NewState(context.TODO(), "/tmp", nil)
require.NoError(t, err, "NewState")
script := `
hive start
example
hive stop
`
bio := bufio.NewReader(bytes.NewBufferString(script))
var stdout bytes.Buffer
err = e.Execute(s, "", bio, &stdout)
require.NoError(t, err, "Execute")

expected := `> hive start.*> example.*hello.*> hive stop`
require.Regexp(t, expected, strings.ReplaceAll(stdout.String(), "\n", " "))
}

0 comments on commit fc01caf

Please sign in to comment.