Skip to content

Commit

Permalink
feat: add openfga module (#2332)
Browse files Browse the repository at this point in the history
* chore: bootstrap openfga module

* chore: add wait strategy

* feat: add container endpoints methods

* chore: add example using client sdk

* feat: enable playground

* chore: add example for writing a model

* docs: include example in docs

* chore: simplify API using built-in blocks

* chore: remove from dependabot
  • Loading branch information
mdelapenya authored Mar 7, 2024
1 parent c6f1f53 commit a935f77
Show file tree
Hide file tree
Showing 12 changed files with 708 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ jobs:
matrix:
go-version: [1.21.x, 1.x]
platform: [ubuntu-latest]
module: [artemis, cassandra, chroma, clickhouse, cockroachdb, compose, consul, couchbase, elasticsearch, gcloud, inbucket, k3s, k6, kafka, localstack, mariadb, milvus, minio, mockserver, mongodb, mssql, mysql, nats, neo4j, ollama, openldap, opensearch, postgres, pulsar, qdrant, rabbitmq, redis, redpanda, surrealdb, vault, weaviate]
module: [artemis, cassandra, chroma, clickhouse, cockroachdb, compose, consul, couchbase, elasticsearch, gcloud, inbucket, k3s, k6, kafka, localstack, mariadb, milvus, minio, mockserver, mongodb, mssql, mysql, nats, neo4j, ollama, openfga, openldap, opensearch, postgres, pulsar, qdrant, rabbitmq, redis, redpanda, surrealdb, vault, weaviate]
uses: ./.github/workflows/ci-test-go.yml
with:
go-version: ${{ matrix.go-version }}
Expand Down
4 changes: 4 additions & 0 deletions .vscode/.testcontainers-go.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@
"name": "module / ollama",
"path": "../modules/ollama"
},
{
"name": "module / openfga",
"path": "../modules/openfga"
},
{
"name": "module / openldap",
"path": "../modules/openldap"
Expand Down
77 changes: 77 additions & 0 deletions docs/modules/openfga.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# OpenFGA

Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

## Introduction

The Testcontainers module for OpenFGA.

## Adding this module to your project dependencies

Please run the following command to add the OpenFGA module to your Go dependencies:

```
go get github.com/testcontainers/testcontainers-go/modules/openfga
```

## Usage example

<!--codeinclude-->
[Creating a OpenFGA container](../../modules/openfga/examples_test.go) inside_block:runOpenFGAContainer
<!--/codeinclude-->

## Module reference

The OpenFGA module exposes one entrypoint function to create the OpenFGA container, and this function receives two parameters:

```golang
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*OpenFGAContainer, error)
```

- `context.Context`, the Go context.
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.

### Container Options

When starting the OpenFGA container, you can pass options in a variadic way to configure it.

#### Image

If you need to set a different OpenFGA Docker image, you can use `testcontainers.WithImage` with a valid Docker image
for OpenFGA. E.g. `testcontainers.WithImage("openfga/openfga:v1.5.0")`.

{% include "../features/common_functional_options.md" %}

### Container Methods

The OpenFGA container exposes the following methods:

#### HttpEndpoint

This method returns the HTTP endpoint to connect to the OpenFGA container, using the `8080` port.

<!--codeinclude-->
[Get HTTP endpoint](../../modules/openfga/examples_test.go) inside_block:httpEndpoint
<!--/codeinclude-->

#### GrpcEndpoint

This method returns the gRPC endpoint to connect to the OpenFGA container, using the `8081` port.

#### Playground URL

In case you want to interact with the openfga playground, please use the `PlaygroundEndpoint` method, using the `3000` port.

<!--codeinclude-->
[Get Playground endpoint](../../modules/openfga/examples_test.go) inside_block:playgroundEndpoint
<!--/codeinclude-->

## Examples

### Writing an OpenFGA model

The following example shows how to write an OpenFGA model using the OpenFGA container.

<!--codeinclude-->
[Get Playground endpoint](../../modules/openfga/examples_test.go) inside_block:openFGAwriteModel
<!--/codeinclude-->
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ nav:
- modules/nats.md
- modules/neo4j.md
- modules/ollama.md
- modules/openfga.md
- modules/openldap.md
- modules/opensearch.md
- modules/postgres.md
Expand Down
5 changes: 5 additions & 0 deletions modules/openfga/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include ../../commons-test.mk

.PHONY: test
test:
$(MAKE) test-openfga
213 changes: 213 additions & 0 deletions modules/openfga/examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package openfga_test

import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"

"github.com/openfga/go-sdk/client"
"github.com/openfga/go-sdk/credentials"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/openfga"
)

func ExampleRunContainer() {
// runOpenFGAContainer {
ctx := context.Background()

openfgaContainer, err := openfga.RunContainer(ctx, testcontainers.WithImage("openfga/openfga:v1.5.0"))
if err != nil {
log.Fatalf("failed to start container: %s", err)
}

// Clean up the container
defer func() {
if err := openfgaContainer.Terminate(ctx); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()
// }

state, err := openfgaContainer.State(ctx)
if err != nil {
log.Fatalf("failed to get container state: %s", err) // nolint:gocritic
}

fmt.Println(state.Running)

// Output:
// true
}

func ExampleRunContainer_connectToPlayground() {
openfgaContainer, err := openfga.RunContainer(context.Background(), testcontainers.WithImage("openfga/openfga:v1.5.0"))
if err != nil {
log.Fatalf("failed to start container: %s", err)
}

// Clean up the container
defer func() {
if err := openfgaContainer.Terminate(context.Background()); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()

// playgroundEndpoint {
playgroundEndpoint, err := openfgaContainer.PlaygroundEndpoint(context.Background())
if err != nil {
log.Fatalf("failed to get playground endpoint: %s", err) // nolint:gocritic
}
// }

httpClient := http.Client{}

resp, err := httpClient.Get(playgroundEndpoint)
if err != nil {
log.Fatalf("failed to get playground endpoint: %s", err) // nolint:gocritic
}

fmt.Println(resp.StatusCode)

// Output:
// 200
}

func ExampleRunContainer_connectWithSDKClient() {
openfgaContainer, err := openfga.RunContainer(context.Background(), testcontainers.WithImage("openfga/openfga:v1.5.0"))
if err != nil {
log.Fatalf("failed to start container: %s", err)
}

// Clean up the container
defer func() {
if err := openfgaContainer.Terminate(context.Background()); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()

// httpEndpoint {
httpEndpoint, err := openfgaContainer.HttpEndpoint(context.Background())
if err != nil {
log.Fatalf("failed to get HTTP endpoint: %s", err) // nolint:gocritic
}
// }

// StoreId is not required for listing and creating stores
fgaClient, err := client.NewSdkClient(&client.ClientConfiguration{
ApiUrl: httpEndpoint, // required
})
if err != nil {
log.Fatalf("failed to create SDK client: %s", err) // nolint:gocritic
}

list, err := fgaClient.ListStores(context.Background()).Execute()
if err != nil {
log.Fatalf("failed to list stores: %s", err) // nolint:gocritic
}

fmt.Println(len(list.Stores))

store, err := fgaClient.CreateStore(context.Background()).Body(client.ClientCreateStoreRequest{Name: "test"}).Execute()
if err != nil {
log.Fatalf("failed to create store: %s", err) // nolint:gocritic
}

fmt.Println(store.Name)

list, err = fgaClient.ListStores(context.Background()).Execute()
if err != nil {
log.Fatalf("failed to list stores: %s", err) // nolint:gocritic
}

fmt.Println(len(list.Stores))

// Output:
// 0
// test
// 1
}

func ExampleRunContainer_writeModel() {
// openFGAwriteModel {
secret := "openfga-secret"
openfgaContainer, err := openfga.RunContainer(
context.Background(),
testcontainers.WithImage("openfga/openfga:v1.5.0"),
testcontainers.WithEnv(map[string]string{
"OPENFGA_LOG_LEVEL": "warn",
"OPENFGA_AUTHN_METHOD": "preshared",
"OPENFGA_AUTHN_PRESHARED_KEYS": secret,
}),
)
if err != nil {
log.Fatalf("failed to start container: %s", err)
}

// Clean up the container
defer func() {
if err := openfgaContainer.Terminate(context.Background()); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()

httpEndpoint, err := openfgaContainer.HttpEndpoint(context.Background())
if err != nil {
log.Fatalf("failed to get HTTP endpoint: %s", err) // nolint:gocritic
}

fgaClient, err := client.NewSdkClient(&client.ClientConfiguration{
ApiUrl: httpEndpoint,
Credentials: &credentials.Credentials{
Method: credentials.CredentialsMethodApiToken,
Config: &credentials.Config{
ApiToken: secret,
},
},
// because we are going to write an authorization model,
// we need to specify an store id. Else, it will fail with
// "Configuration.StoreId is required and must be specified to call this method"
// In this example, it's an arbitrary store id, that will be created
// on the fly.
StoreId: "11111111111111111111111111",
})
if err != nil {
log.Fatalf("failed to create openfga client: %v", err)
}

f, err := os.Open(filepath.Join("testdata", "authorization_model.json"))
if err != nil {
log.Fatalf("failed to open file: %v", err)
}
defer f.Close()

bs, err := io.ReadAll(f)
if err != nil {
log.Fatalf("failed to read file: %v", err)
}

var body client.ClientWriteAuthorizationModelRequest
if err := json.Unmarshal(bs, &body); err != nil {
log.Fatalf("failed to unmarshal json: %v", err)
}

resp, err := fgaClient.WriteAuthorizationModel(context.Background()).Body(body).Execute()
if err != nil {
log.Fatalf("failed to write authorization model: %v", err)
}

// }

value, ok := resp.GetAuthorizationModelIdOk()
fmt.Println(ok)
fmt.Println(*value != "")

// Output:
// true
// true
}
62 changes: 62 additions & 0 deletions modules/openfga/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module github.com/testcontainers/testcontainers-go/modules/openfga

go 1.21

require (
github.com/openfga/go-sdk v0.3.5
github.com/testcontainers/testcontainers-go v0.29.1
)

require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/containerd v1.7.12 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/docker v25.0.3+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)

replace github.com/testcontainers/testcontainers-go => ../..
Loading

0 comments on commit a935f77

Please sign in to comment.