Skip to content

Commit

Permalink
feat: http handlers for greetings example
Browse files Browse the repository at this point in the history
  • Loading branch information
jlarfors committed Mar 4, 2024
1 parent 31d0f77 commit 6834eab
Show file tree
Hide file tree
Showing 42 changed files with 1,362 additions and 347 deletions.
3 changes: 2 additions & 1 deletion cmd/hzctl/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ func printObject(object hz.GenericObject) error {
buf.WriteString("meta:\n")
buf.WriteString(fmt.Sprintf("\taccount: %s\n", object.Account))
buf.WriteString(fmt.Sprintf("\tname: %s\n", object.Name))
oMF, _ := json.MarshalIndent(object.ManagedFields, "\t", " ")
buf.WriteString(
fmt.Sprintf("\tmanagedFields: %s\n", object.ManagedFields),
fmt.Sprintf("\tmanagedFields: %s\n", oMF),
)
buf.WriteString(fmt.Sprintf("\tlabels: %s\n", object.Labels))
buf.WriteString("spec:\n")
Expand Down
21 changes: 18 additions & 3 deletions examples/greetings/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
# Greetings

This is an example of implementing:
The greetings extension is a pointless extension that gives greetings to people.

The extension was developed to not speak with strangers and only recognises a few common Finnish names.

You can run an action (stateless, synchronous) or create an object (stateful, asynchronous) to get a greeting.

The example includes implementing:

1. A [controller](./controller.go) (with a `Reconciler` and `Validator`)
2. An [actor](./actor.go)
3. A [portal](./portal.go).

> [!WARNING]
> The greetings extension currently does not have a portal endpoint (i.e. no http/nats service to return HTML). So there is nothing to browse in the UI.
## Running the example

1. Start a Horizon server (e.g. `go run ./cmd/horizon/main.go`)
2. Generate NATS user credentials (TODO: document how)
3. Run the greetings extention: `go run ./examples/cmd/main.go`

Once you start the server and create an account you should see the "Greetings" portal on the left:

![greetings-screenshot](./greetings-example-screenshot.png)

From there you can click around and greet some people.
32 changes: 27 additions & 5 deletions examples/greetings/actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,39 @@ var _ (hz.Action[Greeting]) = (*GreetingsHelloAction)(nil)
type GreetingsHelloAction struct{}

// Action implements hz.Action.
func (*GreetingsHelloAction) Action() string {
func (GreetingsHelloAction) Action() string {
return "hello"
}

// Do implements hz.Action.
func (*GreetingsHelloAction) Do(
func (a GreetingsHelloAction) Do(
ctx context.Context,
greeting Greeting,
) (Greeting, error) {
fmt.Println("Greetings, " + *greeting.Spec.Name + "!")
greeting.Status.Ready = true
greeting.Status.Phase = StatusPhaseCompleted
if err := a.validate(greeting); err != nil {
return greeting, fmt.Errorf("validating greeting: %w", err)
}
greeting.Status = &GreetingStatus{
Ready: true,
Phase: StatusPhaseCompleted,
Response: "Greetings, " + *greeting.Spec.Name + "!",
}
return greeting, nil
}

func (a GreetingsHelloAction) validate(greeting Greeting) error {
if greeting.Spec == nil {
return fmt.Errorf("spec is required")
}
if greeting.Spec.Name == nil {
return fmt.Errorf("name is required")
}

if !isFriend(*greeting.Spec.Name) {
return fmt.Errorf(
"we don't greet strangers in Finland, we only know: %v",
friends,
)
}
return nil
}
90 changes: 90 additions & 0 deletions examples/greetings/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

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

"github.com/nats-io/nats.go"
"github.com/verifa/horizon/examples/greetings"
"github.com/verifa/horizon/pkg/hz"
)

func main() {
// TODO: get NATS credentials and server information.
if err := run(); err != nil {
slog.Error("running", "error", err)
os.Exit(1)

}
}

func run() error {
conn, err := nats.Connect(
nats.DefaultURL,
nats.UserCredentials("nats.creds"),
)
if err != nil {
return fmt.Errorf("connect: %w", err)
}
ctx := context.Background()

ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
defer stop()

actor, err := hz.StartActor(
ctx,
conn,
hz.WithActorActioner(
greetings.GreetingsHelloAction{},
),
)
if err != nil {
return fmt.Errorf("start actor: %w", err)
}
defer func() {
_ = actor.Stop()
}()

validator := greetings.GreetingValidator{}

reconciler := greetings.GreetingReconciler{
GreetingClient: hz.ObjectClient[greetings.Greeting]{
Client: hz.NewClient(
conn,
hz.WithClientInternal(true),
hz.WithClientManager("ctlr-greetings"),
),
},
}
ctlr, err := hz.StartController(
ctx,
conn,
hz.WithControllerFor(greetings.Greeting{}),
hz.WithControllerReconciler(&reconciler),
hz.WithControllerValidator(&validator),
)
if err != nil {
return fmt.Errorf("start controller: %w", err)
}
defer func() {
_ = ctlr.Stop()
}()

portalHandler := greetings.PortalHandler{
Conn: conn,
}
router := portalHandler.Router()
portal, err := hz.StartPortal(ctx, conn, greetings.Portal, router)
if err != nil {
return fmt.Errorf("start portal: %w", err)
}
defer func() {
_ = portal.Stop()
}()

<-ctx.Done()
return nil
}
57 changes: 53 additions & 4 deletions examples/greetings/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"

"github.com/verifa/horizon/pkg/hz"
"golang.org/x/exp/slices"
)

type GreetingReconciler struct {
Expand All @@ -21,6 +22,18 @@ func (r *GreetingReconciler) Reconcile(
if err != nil {
return hz.Result{}, hz.IgnoreNotFound(err)
}
if greeting.DeletionTimestamp.IsPast() {
// Handle any cleanup logic here.
return hz.Result{}, nil
}
applyGreet, err := hz.ExtractManagedFields(
greeting,
r.GreetingClient.Client.Manager,
)
if err != nil {
return hz.Result{}, fmt.Errorf("extracting managed fields: %w", err)
}

// Obviously we don't need to run an action here, but this is just an
// example.
reply, err := r.GreetingClient.Run(
Expand All @@ -32,8 +45,34 @@ func (r *GreetingReconciler) Reconcile(
return hz.Result{}, fmt.Errorf("running hello action: %w", err)
}

if err := r.GreetingClient.Apply(ctx, reply); err != nil {
return hz.Result{}, fmt.Errorf("updating greeting: %w", err)
// If status is nil then set it.
if greeting.Status == nil {
applyGreet.Status = reply.Status

if err := r.GreetingClient.Apply(ctx, applyGreet); err != nil {
return hz.Result{}, fmt.Errorf("updating greeting: %w", err)
}
// Return here, because the above apply will trigger a reconcile.
return hz.Result{}, nil
}

// If the current response does not match, or the greeting is not ready,
// update the status.
if greeting.Status.Response != reply.Status.Response ||
!greeting.Status.Ready {
if !greeting.Status.Ready {
applyGreet.Status = &GreetingStatus{
Ready: true,
FailureReason: "",
Phase: StatusPhaseCompleted,
Response: reply.Status.Response,
}
if err := r.GreetingClient.Apply(ctx, applyGreet); err != nil {
return hz.Result{}, fmt.Errorf("updating greeting: %w", err)
}
return hz.Result{}, nil
}
return hz.Result{}, nil
}

return hz.Result{}, nil
Expand All @@ -49,16 +88,26 @@ func (*GreetingValidator) Validate(ctx context.Context, data []byte) error {
if err := json.Unmarshal(data, &greeting); err != nil {
return fmt.Errorf("unmarshalling greeting: %w", err)
}
if greeting.Spec == nil {
return fmt.Errorf("spec is required")
}
if greeting.Spec.Name == nil {
return fmt.Errorf("name is required")
}

if !isFriend(*greeting.Spec.Name) {
return fmt.Errorf("we don't greet strangers in Finland, terribly sorry")
return fmt.Errorf(
"we don't greet strangers in Finland, we only know: %v",
friends,
)
}
return nil
}

var friends = []string{
"Pekka", "Matti", "Jukka", "Kari", "Jari", "Mikko", "Ilkka",
}

func isFriend(name string) bool {
return name == "Alice" || name == "Bob"
return slices.Contains(friends, name)
}
Loading

0 comments on commit 6834eab

Please sign in to comment.