Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add schema with crd #22

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

## 1. Is Horizon an alternative to Kubernetes?

No. You might run Horizon on Kubernetes, or use Horizon to provision Kubernetes resources.
So why build Horizon? To combine the controller model, an API and a web UI that is easy to extend and test.
If you are using Kubernetes to run containers and workloads then no.

For simple environments (e.g. home labs) you could write a container scheduler using controllers and actors and have Horizon run containers for you as an alternative to Kubernetes.
If you are extending Kubernetes with CustomResourceDefinitions and Controllers to provide self-service capabitilies, like a Platform Team would do, then potentially yes.
Building operators (custom resource definitions + controllers) is the main use case for Horizon.

For fun (e.g. home labs) you could write a container scheduler using controllers and actors and have Horizon run containers for you as an alternative to Kubernetes.

## 2. If we need to automate everything anyway, isn't this just more work?

Absolutely. Using Horizon is more work than just shipping some Terraform or Ansible scripts to end users.
Do some reading on Developer Experience and Platform Engineering.
Our recommendation would be to do some reading on Developer Experience and Platform Engineering.
Shipping Terraform modules with documentation and a PR-based workflow is **not** (in our eyes) a real platform and provides a terrible Developer Experience.
Architecting abstractions to enable developer flow is not easy and requires effort and time.
The more developers using your platform, the more value there is in doing so.

Expand All @@ -30,3 +33,5 @@ Reconcile logic could be ported to CI pipelines or some other automated event.
Portals are just Go servers that you could run somewhere else.

Like any tool, you will have to invest in Horizon to make it meaningful, but the idea that you get "locked in" because you write Go code (as opposed to bash) is an unfounded one.

You can always port your Go code to a Kubernetes Controller.
38 changes: 38 additions & 0 deletions docs/rbac.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Role Based Access Control (RBAC)

This page talks about the RBAC model that Horizon uses.

At this time, only one very rudimentary model is supported, which is inspired by [Kubernetes' RBAC model](https://kubernetes.io/docs/reference/access-authn-authz/rbac/).

## Request

Whenever the [store](./architecture.md#core---store) receives a command (e.g. apply, get, list) it needs to check if the subject trying to make the request has the necessary permissions to do so.
This is defined in the `auth.Request` struct, which includes a `Subject`, `Verb` and `Object`.
The request is asking: can the Subject perform Verb on the Object.

It is the responsibility of the `auth` package to check a request and say whether the action is permitted or not.

### Subjects

At this time, the only supported Subject is a list of groups that a user belongs to (fetched via the OIDC provider).

#### Special groups

There are some special groups to consider:

- `system:authenticated` is added to the UserInfo that comes from the session. You can use this group to target anyone who has logged in.

### Verbs

The supported verbs are:

- `read`
- `create`
- `update`
- `delete`
- `run`
- `*`

### Objects

## Roles and RoleBindings
9 changes: 5 additions & 4 deletions examples/greetings/cmd/greetings.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

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

Expand Down Expand Up @@ -57,12 +58,12 @@ func run() error {
),
},
}
ctlr, err := hz.StartController(
ctlr, err := controller.Start(
ctx,
conn,
hz.WithControllerFor(greetings.Greeting{}),
hz.WithControllerReconciler(&reconciler),
hz.WithControllerValidator(&validator),
controller.WithFor(greetings.Greeting{}),
controller.WithReconciler(&reconciler),
controller.WithValidator(&validator),
)
if err != nil {
return fmt.Errorf("start controller: %w", err)
Expand Down
9 changes: 5 additions & 4 deletions examples/greetings/greetings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/verifa/horizon/examples/greetings"
"github.com/verifa/horizon/pkg/controller"
"github.com/verifa/horizon/pkg/hz"
"github.com/verifa/horizon/pkg/hztest"
"github.com/verifa/horizon/pkg/server"
Expand All @@ -29,12 +30,12 @@ func TestGreeting(t *testing.T) {
recon := greetings.GreetingReconciler{
GreetingClient: greetClient,
}
ctlr, err := hz.StartController(
ctlr, err := controller.Start(
ctx,
ts.Conn,
hz.WithControllerFor(greetings.Greeting{}),
hz.WithControllerValidator(&validr),
hz.WithControllerReconciler(&recon),
controller.WithFor(greetings.Greeting{}),
controller.WithValidator(&validr),
controller.WithReconciler(&recon),
)
if err != nil {
t.Fatal("starting greeting controller: ", err)
Expand Down
2 changes: 1 addition & 1 deletion examples/greetings/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
var _ (hz.Validator) = (*GreetingValidator)(nil)

type GreetingValidator struct {
hz.ZeroValidator
hz.ValidateNothing
}

func (*GreetingValidator) ValidateCreate(
Expand Down
1 change: 1 addition & 0 deletions examples/services/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*_templ.go
15 changes: 15 additions & 0 deletions examples/services/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Extension Template

This is a very basic example to get you started.

It embeds the Horizon and NATS server, so you can make a single executable that also includes your extension.

You can use [gonew](https://pkg.go.dev/golang.org/x/tools/cmd/gonew) to bootstrap your project:

```console
go install golang.org/x/tools/cmd/gonew@latest

gonew github.com/verifa/horizon/examples/services your.domain/selfservice
```

**TODO: handle new namespace 403 error when logging in as `user-a`**
195 changes: 195 additions & 0 deletions examples/services/cmd/horizon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package main

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

"github.com/nats-io/nats.go"
"github.com/verifa/horizon/examples/services/pkg/ext/services"
"github.com/verifa/horizon/pkg/auth"
"github.com/verifa/horizon/pkg/controller"
"github.com/verifa/horizon/pkg/extensions/core"
"github.com/verifa/horizon/pkg/gateway"
"github.com/verifa/horizon/pkg/hz"
"github.com/verifa/horizon/pkg/server"

cloudrun "google.golang.org/api/run/v1"
)

func main() {
if err := run(); err != nil {
slog.Error("running", "error", err)
os.Exit(1)
}
}

func run() error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
//
// Connect to Google Cloud Run API.
//
_, err := cloudrun.NewService(ctx)
if err != nil {
return fmt.Errorf("connect to Google Cloud Run API: %w", err)
}
//
// Start Horizon embedded server.
//
s, err := server.Start(
ctx,
server.WithDevMode(),
server.WithAuthOptions(auth.WithAdminGroups("admin")),
server.WithGatewayOptions(
gateway.WithOIDCConfig(gateway.OIDCConfig{
Issuer: "https://accounts.google.com",
ClientID: "50561335587-qp0rcctibj88mcpg13fh9ecq2mlb9faq.apps.googleusercontent.com",
ClientSecret: "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",
RedirectURL: "http://localhost:9999/auth/callback",
Scopes: []string{
"openid",
"profile",
"email",
},
}),
),
)
if err != nil {
return err
}
defer s.Close()
slog.Info("horizon server started")

//
// Start Horizon extensions (controllers, portals, etc.).
//
validator := services.Validator{}
reconciler := services.Reconciler{
Client: hz.ObjectClient[services.Service]{
Client: hz.NewClient(
s.Conn,
hz.WithClientInternal(true),
hz.WithClientManager("ctlr-services"),
),
},
}
ctlr, err := controller.Start(
ctx,
s.Conn,
controller.WithFor(services.Service{}),
controller.WithReconciler(&reconciler),
controller.WithValidator(&validator),
)
if err != nil {
return fmt.Errorf("start controller: %w", err)
}
defer func() {
_ = ctlr.Stop()
}()

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

if err := setupDefaultRBAC(ctx, s.Conn); err != nil {
return fmt.Errorf("setup default RBAC: %w", err)
}

if err := createDemoService(ctx, s.Conn); err != nil {
return fmt.Errorf("create demo service: %w", err)
}

// Wait for interrupt signal.
<-ctx.Done()
// Stop listening for interrupts so that a second interrupt will force
// shutdown.
stop()
slog.Info(
"interrupt received, shutting down horizon server",
)
return nil
}

func setupDefaultRBAC(ctx context.Context, conn *nats.Conn) error {
client := hz.NewClient(conn, hz.WithClientInternal(true))
authenticatedRole := auth.Role{
ObjectMeta: hz.ObjectMeta{
Name: "authenticated-users-role",
Namespace: hz.NamespaceRoot,
},
Spec: auth.RoleSpec{
Allow: []auth.Rule{
{
Group: hz.P("*"),
Kind: hz.P("Namespace"),
Verbs: []auth.Verb{auth.VerbRead, auth.VerbCreate},
},
{
Group: hz.P("*"),
Kind: hz.P("Service"),
Verbs: []auth.Verb{auth.VerbRead, auth.VerbAll},
},
},
},
}
authenticatedRoleBinding := auth.RoleBinding{
ObjectMeta: hz.ObjectMeta{
Name: "authenticated-users-role-binding",
Namespace: hz.NamespaceRoot,
},
Spec: auth.RoleBindingSpec{
RoleRef: auth.RoleRefFromRole(authenticatedRole),
Subjects: []auth.Subject{
{
Kind: "Group",
Name: auth.GroupSystemAuthenticated,
},
},
},
}
if _, err := client.Apply(ctx, hz.WithApplyObject(authenticatedRole)); err != nil {
return fmt.Errorf("apply namespace role: %w", err)
}
if _, err := client.Apply(ctx, hz.WithApplyObject(authenticatedRoleBinding)); err != nil {
return fmt.Errorf("apply namespace role binding: %w", err)
}
return nil
}

func createDemoService(ctx context.Context, conn *nats.Conn) error {
client := hz.NewClient(conn, hz.WithClientInternal(true))
ns := core.Namespace{
ObjectMeta: hz.ObjectMeta{
Name: "demo",
Namespace: hz.NamespaceRoot,
},
}
if _, err := client.Apply(ctx, hz.WithApplyObject(ns)); err != nil {
return fmt.Errorf("apply namespace: %w", err)
}
service := services.Service{
ObjectMeta: hz.ObjectMeta{
Namespace: ns.Name,
Name: "demo-prod",
},
Spec: &services.ServiceSpec{
Host: hz.P("demo.horizon.xyz"),
Image: hz.P("horizon-demo:123456"),
},
}
if _, err := client.Apply(ctx, hz.WithApplyObject(service)); err != nil {
return fmt.Errorf("apply service: %w", err)
}
return nil
}
Loading
Loading