Skip to content
This repository has been archived by the owner on Nov 5, 2024. It is now read-only.

Commit

Permalink
feat: support postgres (#7)
Browse files Browse the repository at this point in the history
Signed-off-by: Grant Linville <[email protected]>
  • Loading branch information
g-linville authored Nov 4, 2024
1 parent 288c4f2 commit ad7199e
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 88 deletions.
31 changes: 26 additions & 5 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ snapshot:
version_template: '{{ trimprefix .Summary "v" }}'

builds:
- id: default
- id: sqlite
main: ./sqlite
binary: gptscript-credential-sqlite
env:
- CGO_ENABLED=0
Expand All @@ -19,14 +20,34 @@ builds:
ldflags:
- -s
- -w
- id: postgres
main: ./postgres
binary: gptscript-credential-postgres
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
flags:
- -trimpath
ldflags:
- -s
- -w

archives:
# Disable archives, we just want the binaries
- id: no_archives
- id: sqlite
format: binary
name_template: "gptscript-credential-sqlite-{{ .Os }}-{{ .Arch }}"
builds:
- default
- sqlite
- id: postgres
format: binary
name_template: "gptscript-credential-postgres-{{ .Os }}-{{ .Arch }}"
builds:
- postgres

checksum:
name_template: "checksums.txt"
Expand All @@ -43,5 +64,5 @@ changelog:
release:
github:
owner: gptscript-ai
name: gptscript-credential-sqlite
name: gptscript-credential-database
prerelease: auto
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
.PHONY: build-sqlite
build-sqlite:
CGO_ENABLED=0 go build -o bin/gptscript-credential-sqlite -tags "${GO_TAGS}" -ldflags "-s -w" ./sqlite

.PHONY: build-postgres
build-postgres:
CGO_ENABLED=0 go build -o bin/gptscript-credential-postgres -tags "${GO_TAGS}" -ldflags "-s -w" ./postgres

.PHONY: build
build:
CGO_ENABLED=0 go build -o bin/gptscript-credential-sqlite -tags "${GO_TAGS}" -ldflags "-s -w" .
build: build-sqlite build-postgres
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# gptscript-credential-sqlite
# gptscript-credential-database

This is a GPTScript [credential helper](https://docs.gptscript.ai/credentials) for SQLite. When the `sqlite` credential store
is configured for GPTScript, it will use this helper to store credentials in a local SQLite file, located in the configuration directory.
By default, all credentials are stored unencrypted.
This is a set of GPTScript [credential helpers](https://docs.gptscript.ai/credentials) for databases.
Currently, SQLite and PostgreSQL are supported.
To use SQLite, set your GPTScript configuration to use `sqlite` as the credential store.
To use PostgreSQL, set your GPTScript configuration to use `postgres` as the credential store.

By default, all credentials are stored **unencrypted**.

Only macOS and Linux are supported.

Expand Down Expand Up @@ -44,5 +47,12 @@ resources:
## Environment Variables
- `GPTSCRIPT_SQLITE_FILE` - can be used to override the path to the SQLite file.
All helpers:
- `GPTSCRIPT_ENCRYPTION_CONFIG_FILE` - can be used to override the path to the encryption configuration file.

SQLite:
- `GPTSCRIPT_SQLITE_FILE` - can be used to override the path to the SQLite file.

PostgreSQL:
- `GPTSCRIPT_POSTGRES_DSN` - (required) the DSN (connection string) for the PostgreSQL database.

5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/adrg/xdg v0.4.0
github.com/docker/docker-credential-helpers v0.8.2
github.com/glebarez/sqlite v1.11.0
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.12
k8s.io/apimachinery v0.31.1
k8s.io/apiserver v0.31.1
Expand Down Expand Up @@ -38,6 +39,10 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
Expand Down Expand Up @@ -224,6 +232,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
Expand Down
19 changes: 0 additions & 19 deletions main.go

This file was deleted.

14 changes: 7 additions & 7 deletions pkg/sqlite/encryption.go → pkg/common/encryption.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sqlite
package common

import (
"context"
Expand Down Expand Up @@ -31,13 +31,13 @@ func readEncryptionConfig(ctx context.Context) (*encryptionconfig.EncryptionConf
return encryptionconfig.LoadEncryptionConfig(ctx, encryptionConfigPath, false, "gptscript")
}

func (s Sqlite) encryptCred(ctx context.Context, cred GptscriptCredential) (GptscriptCredential, error) {
if s.transformer == nil {
func (d Database) encryptCred(ctx context.Context, cred GptscriptCredential) (GptscriptCredential, error) {
if d.transformer == nil {
return cred, nil
}

secretBytes := []byte(cred.Secret)
encryptedSecretBytes, err := s.transformer.TransformToStorage(ctx, secretBytes, uid(cred.ServerURL))
encryptedSecretBytes, err := d.transformer.TransformToStorage(ctx, secretBytes, uid(cred.ServerURL))
if err != nil {
return GptscriptCredential{}, fmt.Errorf("failed to encrypt secret: %w", err)
}
Expand All @@ -46,8 +46,8 @@ func (s Sqlite) encryptCred(ctx context.Context, cred GptscriptCredential) (Gpts
return cred, nil
}

func (s Sqlite) decryptCred(ctx context.Context, cred GptscriptCredential) (GptscriptCredential, error) {
if s.transformer == nil {
func (d Database) decryptCred(ctx context.Context, cred GptscriptCredential) (GptscriptCredential, error) {
if d.transformer == nil {
return cred, nil
}

Expand All @@ -59,7 +59,7 @@ func (s Sqlite) decryptCred(ctx context.Context, cred GptscriptCredential) (Gpts
return GptscriptCredential{}, fmt.Errorf("failed to decode secret: %w", err)
}

secretBytes, _, err := s.transformer.TransformFromStorage(ctx, encryptedSecretBytes, uid(cred.ServerURL))
secretBytes, _, err := d.transformer.TransformFromStorage(ctx, encryptedSecretBytes, uid(cred.ServerURL))
if err != nil {
return GptscriptCredential{}, fmt.Errorf("failed to decrypt secret: %w", err)
}
Expand Down
75 changes: 25 additions & 50 deletions pkg/sqlite/sqlite.go → pkg/common/sqlite.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
package sqlite
package common

import (
"context"
"errors"
"fmt"
"log"
"os"
"time"

"github.com/adrg/xdg"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/storage/value"
)
Expand All @@ -31,53 +26,33 @@ var groupResource = schema.GroupResource{
Resource: "credentials",
}

type Sqlite struct {
type Database struct {
db *gorm.DB
transformer value.Transformer
}

func NewSqlite(ctx context.Context) (Sqlite, error) {
var (
dbPath string
err error
)
if os.Getenv("GPTSCRIPT_SQLITE_FILE") != "" {
dbPath = os.Getenv("GPTSCRIPT_SQLITE_FILE")
} else {
dbPath, err = xdg.ConfigFile("gptscript/credentials.db")
if err != nil {
return Sqlite{}, fmt.Errorf("failed to get credentials db path: %w", err)
}
}

db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{
LogLevel: logger.Error,
IgnoreRecordNotFoundError: true,
}),
})
if err != nil {
return Sqlite{}, fmt.Errorf("failed to open database: %w", err)
}

func NewDatabase(ctx context.Context, db *gorm.DB) (Database, error) {
if err := db.AutoMigrate(&GptscriptCredential{}); err != nil {
return Sqlite{}, fmt.Errorf("failed to auto migrate GptscriptCredential: %w", err)
return Database{}, fmt.Errorf("failed to auto migrate GptscriptCredential: %w", err)
}

s := Sqlite{db: db}

encryptionConf, err := readEncryptionConfig(ctx)
if err != nil {
return Sqlite{}, fmt.Errorf("failed to read encryption config: %w", err)
return Database{}, fmt.Errorf("failed to read encryption config: %w", err)
} else if encryptionConf != nil {
transformer, exists := encryptionConf.Transformers[groupResource]
if !exists {
return Sqlite{}, fmt.Errorf("failed to find encryption transformer for %s", groupResource.String())
return Database{}, fmt.Errorf("failed to find encryption transformer for %s", groupResource.String())
}
s.transformer = transformer
return Database{
db: db,
transformer: transformer,
}, nil
}

return s, nil
return Database{
db: db,
}, nil
}

type GptscriptCredential struct {
Expand All @@ -88,14 +63,14 @@ type GptscriptCredential struct {
Secret string
}

func (s Sqlite) Add(creds *credentials.Credentials) error {
func (d Database) Add(creds *credentials.Credentials) error {
cred := GptscriptCredential{
ServerURL: creds.ServerURL,
Username: creds.Username,
Secret: creds.Secret,
}

cred, err := s.encryptCred(context.Background(), cred)
cred, err := d.encryptCred(context.Background(), cred)
if err != nil {
return fmt.Errorf("failed to encrypt credential: %w", err)
}
Expand All @@ -104,61 +79,61 @@ func (s Sqlite) Add(creds *credentials.Credentials) error {
// If it does, delete it first.
// This would normally happen during a credential refresh.
var existing GptscriptCredential
if err := s.db.Where("server_url = ?", cred.ServerURL).First(&existing).Error; err != nil {
if err := d.db.Where("server_url = ?", cred.ServerURL).First(&existing).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("failed to get existing credential: %w", err)
}
} else {
if err := s.db.Delete(&existing).Error; err != nil {
if err := d.db.Delete(&existing).Error; err != nil {
return fmt.Errorf("failed to delete existing credential: %w", err)
}
}

if err := s.db.Create(&cred).Error; err != nil {
if err := d.db.Create(&cred).Error; err != nil {
return fmt.Errorf("failed to create credential: %w", err)
}

return nil
}

func (s Sqlite) Delete(serverURL string) error {
func (d Database) Delete(serverURL string) error {
var (
cred GptscriptCredential
err error
)
if err = s.db.Where("server_url = ?", serverURL).Delete(&cred).Error; err != nil {
if err = d.db.Where("server_url = ?", serverURL).Delete(&cred).Error; err != nil {
return fmt.Errorf("failed to delete credential: %w", err)
}

return nil
}

func (s Sqlite) Get(serverURL string) (string, string, error) {
func (d Database) Get(serverURL string) (string, string, error) {
var (
cred GptscriptCredential
err error
)
if err = s.db.Where("server_url = ?", serverURL).First(&cred).Error; err != nil {
if err = d.db.Where("server_url = ?", serverURL).First(&cred).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", "", nil
}
return "", "", fmt.Errorf("failed to get credential: %w", err)
}

cred, err = s.decryptCred(context.Background(), cred)
cred, err = d.decryptCred(context.Background(), cred)
if err != nil {
return "", "", fmt.Errorf("failed to decrypt credential: %w", err)
}

return cred.Username, cred.Secret, nil
}

func (s Sqlite) List() (map[string]string, error) {
func (d Database) List() (map[string]string, error) {
var (
creds []GptscriptCredential
err error
)
if err = s.db.Find(&creds).Error; err != nil {
if err = d.db.Find(&creds).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
Expand Down
Loading

0 comments on commit ad7199e

Please sign in to comment.