Skip to content
Closed
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
25 changes: 22 additions & 3 deletions cmd/tuple/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ var deleteCmd = &cobra.Command{
}
if fileName != "" {
startTime := time.Now()
successPath, _ := cmd.Flags().GetString("success-log")
failurePath, _ := cmd.Flags().GetString("failure-log")
var successLogger, failureLogger *tuple.TupleLogger
if successPath != "" {
successLogger, err = tuple.NewTupleLogger(successPath, false)
if err != nil {
return err
}
defer successLogger.Close()
}
if failurePath != "" {
failureLogger, err = tuple.NewTupleLogger(failurePath, true)
if err != nil {
return err
}
defer failureLogger.Close()
}

clientTupleKeys, err := tuplefile.ReadTupleFile(fileName)
if err != nil {
Expand All @@ -70,8 +87,10 @@ var deleteCmd = &cobra.Command{
Deletes: clientTupleKeyWithoutCondition,
}

newCtx := tuple.WithSuccessLogger(cmd.Context(), successLogger)
newCtx = tuple.WithFailureLogger(newCtx, failureLogger)
response, err := tuple.ImportTuplesWithoutRampUp(
cmd.Context(), fgaClient,
newCtx, fgaClient,
maxTuplesPerWrite, maxParallelRequests,
writeRequest)
if err != nil {
Expand All @@ -83,11 +102,11 @@ var deleteCmd = &cobra.Command{

outputResponse := make(map[string]interface{})

if !hideImportedTuples && len(response.Successful) > 0 {
if !hideImportedTuples && successPath == "" && len(response.Successful) > 0 {
outputResponse["successful"] = response.Successful
}

if len(response.Failed) > 0 {
if failurePath == "" && len(response.Failed) > 0 {
outputResponse["failed"] = response.Failed
}

Expand Down
2 changes: 2 additions & 0 deletions cmd/tuple/tuple.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ func init() {
TupleCmd.AddCommand(deleteCmd)

TupleCmd.PersistentFlags().String("store-id", "", "Store ID")
TupleCmd.PersistentFlags().String("success-log", "", "Filepath to log successful writes")
TupleCmd.PersistentFlags().String("failure-log", "", "Filepath to log failed writes")

err := TupleCmd.MarkPersistentFlagRequired("store-id")
if err != nil { //nolint:wsl
Expand Down
26 changes: 24 additions & 2 deletions cmd/tuple/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,26 @@ func writeTuplesFromFile(ctx context.Context, flags *flag.FlagSet, fgaClient *cl
return fmt.Errorf("failed to parse debug flag due to %w", err)
}

successPath, _ := flags.GetString("success-log")
failurePath, _ := flags.GetString("failure-log")

var successLogger, failureLogger *tuple.TupleLogger
if successPath != "" {
successLogger, err = tuple.NewTupleLogger(successPath, false)
if err != nil {
return err
}
defer successLogger.Close()
}

if failurePath != "" {
failureLogger, err = tuple.NewTupleLogger(failurePath, true)
if err != nil {
return err
}
defer failureLogger.Close()
}

tuples, err := tuplefile.ReadTupleFile(fileName)
if err != nil {
return err //nolint:wrapcheck
Expand All @@ -243,6 +263,8 @@ func writeTuplesFromFile(ctx context.Context, flags *flag.FlagSet, fgaClient *cl
}

newCtx := utils.WithDebugContext(ctx, debug)
newCtx = tuple.WithSuccessLogger(newCtx, successLogger)
newCtx = tuple.WithFailureLogger(newCtx, failureLogger)

response, err := tuple.ImportTuples(
newCtx, fgaClient,
Expand All @@ -257,11 +279,11 @@ func writeTuplesFromFile(ctx context.Context, flags *flag.FlagSet, fgaClient *cl

outputResponse := make(map[string]interface{})

if !hideImportedTuples && len(response.Successful) > 0 {
if !hideImportedTuples && successPath == "" && len(response.Successful) > 0 {
outputResponse["successful"] = response.Successful
}

if len(response.Failed) > 0 {
if failurePath == "" && len(response.Failed) > 0 {
outputResponse["failed"] = response.Failed
}

Expand Down
41 changes: 32 additions & 9 deletions internal/tuple/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func importTuplesWithoutRampUp(
return nil, fmt.Errorf("failed to import tuples due to %w", err)
}

successful, failed := processWritesAndDeletes(response)
successful, failed := processWritesAndDeletes(ctx, response)
result := ImportResponse{
Successful: successful,
Failed: failed,
Expand Down Expand Up @@ -215,8 +215,8 @@ func importTuplesWithRampUp(ctx context.Context, fgaClient client.SdkClient,
return err //nolint:wrapcheck
}

successfulWrites, failedWrites := processWrites(response.Writes)
successfulDeletes, failedDeletes := processDeletes(response.Deletes)
successfulWrites, failedWrites := processWrites(ctx, response.Writes)
successfulDeletes, failedDeletes := processDeletes(ctx, response.Deletes)

mutex.Lock()

Expand Down Expand Up @@ -298,45 +298,61 @@ func extractErrMsg(err error) string {
}

func processWritesAndDeletes(
ctx context.Context,
response *client.ClientWriteResponse,
) ([]client.ClientTupleKey, []failedWriteResponse) {
successfulWrites, failedWrites := processWrites(response.Writes)
successfulDeletes, failedDeletes := processDeletes(response.Deletes)
successfulWrites, failedWrites := processWrites(ctx, response.Writes)
successfulDeletes, failedDeletes := processDeletes(ctx, response.Deletes)

return append(successfulWrites, successfulDeletes...), append(failedWrites, failedDeletes...)
}

func processWrites(
ctx context.Context,
writes []client.ClientWriteRequestWriteResponse,
) ([]client.ClientTupleKey, []failedWriteResponse) {
var (
successfulWrites []client.ClientTupleKey
failedWrites []failedWriteResponse
)

successLogger := getSuccessLogger(ctx)
failureLogger := getFailureLogger(ctx)

for _, write := range writes {
if write.Status == client.SUCCESS {
successfulWrites = append(successfulWrites, write.TupleKey)
if successLogger != nil {
successLogger.LogSuccess(write.TupleKey)
}
} else {
reason := extractErrMsg(write.Error)
failedWrites = append(failedWrites, failedWriteResponse{
failed := failedWriteResponse{
TupleKey: write.TupleKey,
Reason: reason,
})
}
failedWrites = append(failedWrites, failed)
if failureLogger != nil {
failureLogger.LogFailure(failed)
}
}
}

return successfulWrites, failedWrites
}

func processDeletes(
ctx context.Context,
deletes []client.ClientWriteRequestDeleteResponse,
) ([]client.ClientTupleKey, []failedWriteResponse) {
var (
successfulDeletes []client.ClientTupleKey
failedDeletes []failedWriteResponse
)

successLogger := getSuccessLogger(ctx)
failureLogger := getFailureLogger(ctx)

for _, del := range deletes {
deletedTupleKey := openfga.TupleKey{
Object: del.TupleKey.Object,
Expand All @@ -346,12 +362,19 @@ func processDeletes(

if del.Status == client.SUCCESS {
successfulDeletes = append(successfulDeletes, deletedTupleKey)
if successLogger != nil {
successLogger.LogSuccess(deletedTupleKey)
}
} else {
reason := extractErrMsg(del.Error)
failedDeletes = append(failedDeletes, failedWriteResponse{
failed := failedWriteResponse{
TupleKey: deletedTupleKey,
Reason: reason,
})
}
failedDeletes = append(failedDeletes, failed)
if failureLogger != nil {
failureLogger.LogFailure(failed)
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions internal/tuple/import_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tuple

import (
"context"
"errors"
"testing"

Expand All @@ -23,7 +24,7 @@ func TestProcessWrites(t *testing.T) {
},
}

successful, failed := processWrites(writes)
successful, failed := processWrites(context.Background(), writes)

assert.Len(t, successful, 1)
assert.Len(t, failed, 1)
Expand All @@ -45,7 +46,7 @@ func TestProcessDeletes(t *testing.T) {
},
}

successful, failed := processDeletes(deletes)
successful, failed := processDeletes(context.Background(), deletes)

assert.Len(t, successful, 1)
assert.Len(t, failed, 1)
Expand Down
137 changes: 137 additions & 0 deletions internal/tuple/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package tuple

import (
"bufio"
"context"
"encoding/csv"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/openfga/go-sdk/client"
"gopkg.in/yaml.v3"
)

// TupleLogger writes tuple logs in various formats.
type TupleLogger struct {
file *os.File
writer *bufio.Writer
format string
headerWritten bool
isFailure bool
}

// NewTupleLogger creates a logger for the given file path.
func NewTupleLogger(path string, isFailure bool) (*TupleLogger, error) {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
if err != nil {
return nil, fmt.Errorf("failed to open log file %s: %w", path, err)
}

info, err := f.Stat()
if err != nil {
_ = f.Close()
return nil, fmt.Errorf("failed to stat log file %s: %w", path, err)
}

return &TupleLogger{
file: f,
writer: bufio.NewWriter(f),
format: strings.ToLower(filepath.Ext(path)),
headerWritten: info.Size() > 0,
isFailure: isFailure,
}, nil
}

func (l *TupleLogger) Close() error {
if l == nil {
return nil
}
_ = l.writer.Flush()
_ = l.file.Sync()
return l.file.Close()
}

func (l *TupleLogger) flush() {

Check failure on line 57 in internal/tuple/logger.go

View workflow job for this annotation

GitHub Actions / Lints

unexported method "flush" for struct "TupleLogger" should be placed after the exported method "LogFailure" (funcorder)
_ = l.writer.Flush()
_ = l.file.Sync()
}

// LogSuccess writes a successful tuple key.
func (l *TupleLogger) LogSuccess(key client.ClientTupleKey) {
if l == nil {
return
}
switch l.format {
case ".csv":
l.writeCSV([]string{key.User, key.Relation, key.Object})
case ".yaml", ".yml":
b, _ := yaml.Marshal(key)
l.writer.Write(b)

Check failure on line 72 in internal/tuple/logger.go

View workflow job for this annotation

GitHub Actions / Lints

Error return value of `l.writer.Write` is not checked (errcheck)
l.writer.Write([]byte("---\n"))

Check failure on line 73 in internal/tuple/logger.go

View workflow job for this annotation

GitHub Actions / Lints

Error return value of `l.writer.Write` is not checked (errcheck)
default: // json and jsonl
b, _ := json.Marshal(key)

Check failure on line 75 in internal/tuple/logger.go

View workflow job for this annotation

GitHub Actions / Lints

Error return value of `encoding/json.Marshal` is not checked: unsafe type `github.com/openfga/go-sdk/client.ClientTupleKey` found (errchkjson)
l.writer.Write(append(b, '\n'))

Check failure on line 76 in internal/tuple/logger.go

View workflow job for this annotation

GitHub Actions / Lints

Error return value of `l.writer.Write` is not checked (errcheck)
}
l.flush()
}

// LogFailure writes a failed tuple with reason.
func (l *TupleLogger) LogFailure(failed failedWriteResponse) {
if l == nil {
return
}
switch l.format {
case ".csv":
l.writeCSV([]string{failed.TupleKey.User, failed.TupleKey.Relation, failed.TupleKey.Object, failed.Reason})
case ".yaml", ".yml":
b, _ := yaml.Marshal(failed)

Check failure on line 90 in internal/tuple/logger.go

View workflow job for this annotation

GitHub Actions / Lints

the given struct should be annotated with the `yaml` tag (musttag)
l.writer.Write(b)
l.writer.Write([]byte("---\n"))
default:
b, _ := json.Marshal(failed)

Check failure on line 94 in internal/tuple/logger.go

View workflow job for this annotation

GitHub Actions / Lints

Error return value of `encoding/json.Marshal` is not checked: unsafe type `github.com/openfga/go-sdk/client.ClientTupleKey` found (errchkjson)
l.writer.Write(append(b, '\n'))
}
l.flush()
}

func (l *TupleLogger) writeCSV(record []string) {
w := csv.NewWriter(l.writer)
if !l.headerWritten {
if l.isFailure {
w.Write([]string{"user", "relation", "object", "reason"})

Check failure on line 104 in internal/tuple/logger.go

View workflow job for this annotation

GitHub Actions / Lints

Error return value of `w.Write` is not checked (errcheck)
} else {
w.Write([]string{"user", "relation", "object"})

Check failure on line 106 in internal/tuple/logger.go

View workflow job for this annotation

GitHub Actions / Lints

Error return value of `w.Write` is not checked (errcheck)
}
l.headerWritten = true
}
_ = w.Write(record)
w.Flush()
}

// Context utilities

type logKey struct{ name string }

var successLogKey = &logKey{"successLog"}

Check failure on line 118 in internal/tuple/logger.go

View workflow job for this annotation

GitHub Actions / Lints

File is not properly formatted (gofumpt)
var failureLogKey = &logKey{"failureLog"}

func WithSuccessLogger(ctx context.Context, l *TupleLogger) context.Context {
return context.WithValue(ctx, successLogKey, l)
}

func WithFailureLogger(ctx context.Context, l *TupleLogger) context.Context {
return context.WithValue(ctx, failureLogKey, l)
}

func getSuccessLogger(ctx context.Context) *TupleLogger {
logger, _ := ctx.Value(successLogKey).(*TupleLogger)
return logger
}

func getFailureLogger(ctx context.Context) *TupleLogger {
logger, _ := ctx.Value(failureLogKey).(*TupleLogger)
return logger
}
Loading