Skip to content
Merged
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
17 changes: 16 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ jobs:
docker run --rm --detach \
--name trino \
--net trino \
--volume "$(pwd)/test-data/test-trino-config.properties:/etc/trino/config.properties" \
--volume "$(pwd)/test-data/trino/test-trino-config.properties:/etc/trino/config.properties" \
--volume "$(pwd)/test-data/trino/catalog/hive.properties:/etc/trino/catalog/hive.properties" \
trinodb/trino:468

echo "Starting Grafana..."
Expand All @@ -97,6 +98,20 @@ jobs:
--volume "$(pwd):/var/lib/grafana/plugins/trino" \
--env "GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=trino-datasource" \
grafana/grafana:11.4.0

echo "Waiting for Trino to be ready..."
while true; do
if docker logs trino 2>&1 | grep -q '======== SERVER STARTED ========'; then
echo "Trino is ready!"
break
fi
echo "Waiting for Trino..."
sleep 5
done

echo "Preconfiguring trino..."
docker exec trino trino --user admin --execute "GRANT admin TO USER grafana IN hive;"
echo "Done."

- name: End to end test
run: |
Expand Down
26 changes: 13 additions & 13 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
module github.com/trinodb/grafana-trino

go 1.23.5

toolchain go1.24.2
go 1.24.7

require (
github.com/grafana/grafana-plugin-sdk-go v0.274.0
github.com/grafana/sqlds/v2 v2.7.2
github.com/pkg/errors v0.9.1
github.com/trinodb/trino-go-client v0.323.0
github.com/trinodb/trino-go-client v0.333.0
)

require (
Expand Down Expand Up @@ -52,7 +50,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
Expand All @@ -75,6 +73,7 @@ require (
github.com/oklog/run v1.1.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
Expand All @@ -99,15 +98,16 @@ require (
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.32.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755 // indirect
Expand Down
139 changes: 92 additions & 47 deletions go.sum

Large diffs are not rendered by default.

30 changes: 29 additions & 1 deletion pkg/trino/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import (
"database/sql"
"errors"
"fmt"
trinoClient "github.com/trinodb/grafana-trino/pkg/trino/client"
"net/http"
"strings"

trinoClient "github.com/trinodb/grafana-trino/pkg/trino/client"

"github.com/trinodb/grafana-trino/pkg/trino/models"
"github.com/trinodb/trino-go-client/trino"
_ "github.com/trinodb/trino-go-client/trino"
Expand Down Expand Up @@ -94,12 +95,19 @@ func Open(settings models.TrinoDatasourceSettings) (*sql.DB, error) {
if err != nil {
return nil, err
}

roles, err := parseRoles(settings.Roles)
if err != nil {
return nil, err
}

config := trino.Config{
ServerURI: settings.URL.String(),
Source: "grafana",
CustomClientName: "grafana",
ForwardAuthorizationHeader: true,
AccessToken: settings.AccessToken,
Roles: roles,
}

dsn, err := config.FormatDSN()
Expand All @@ -108,3 +116,23 @@ func Open(settings models.TrinoDatasourceSettings) (*sql.DB, error) {
}
return sql.Open(DriverName, dsn)
}

func parseRoles(roleStr string) (map[string]string, error) {
roles := make(map[string]string)
if strings.TrimSpace(roleStr) == "" {
return roles, nil
}
pairs := strings.Split(roleStr, ";")
for _, pair := range pairs {
parts := strings.SplitN(pair, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("Invalid role format. expected catalog:role, got '%s'", pair)
}
catalog := strings.TrimSpace(parts[0])
role := strings.TrimSpace(parts[1])
if catalog != "" && role != "" {
roles[catalog] = role
}
}
return roles, nil
}
1 change: 1 addition & 0 deletions pkg/trino/models/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type TrinoDatasourceSettings struct {
ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
ImpersonationUser string `json:"impersonationUser"`
Roles string `json:"roles"`
ClientTags string `json:"clientTags"`
}

Expand Down
16 changes: 16 additions & 0 deletions src/ConfigEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export class ConfigEditor extends PureComponent<Props, State> {
const onImpersonationUserChange = (event: ChangeEvent<HTMLInputElement>) => {
onOptionsChange({...options, jsonData: {...options.jsonData, impersonationUser: event.target.value}})
};
const onRolesChange = (event: ChangeEvent<HTMLInputElement>) => {
onOptionsChange({...options, jsonData: {...options.jsonData, roles: event.target.value}})
};
const onClientTagsChange = (event: ChangeEvent<HTMLInputElement>) => {
onOptionsChange({...options, jsonData: {...options.jsonData, clientTags: event.target.value}})
};
Expand Down Expand Up @@ -75,6 +78,19 @@ export class ConfigEditor extends PureComponent<Props, State> {
/>
</InlineField>
</div>
<div className="gf-form-inline">
<InlineField
label="Roles"
tooltip="Authorization roles to use for catalogs, specified as a list of key-value pairs for the catalog and role. For example, system:roleS;catalog1:roleA;catalog2:roleB"
labelWidth={26}
>
<Input
value={options.jsonData?.roles ?? ''}
onChange={onRolesChange}
width={40}
/>
</InlineField>
</div>
<div className="gf-form-inline">
<InlineField
label="Client Tags"
Expand Down
37 changes: 37 additions & 0 deletions src/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,40 @@ test('test with client tags', async ({ page }) => {
await setupDataSourceWithClientTags(page, 'tag1,tag2,tag3');
await runQueryAndCheckResults(page);
});

test('test with roles', async ({ page }) => {
await login(page);
await goToTrinoSettings(page);
await setupDataSourceWithRoles(page, 'system:ALL;hive:admin');
await runRoleQuery(page);
await expect(page.getByTestId('data-testid table body')).toContainText(/.*admin.*/);

});

test('test without role', async ({ page }) => {
await login(page);
await goToTrinoSettings(page);
await setupDataSourceWithRoles(page, '');
await runRoleQuery(page);
await expect(page.getByText(/Access Denied: Cannot show roles/)).toBeVisible();
});

async function setupDataSourceWithRoles(page: Page, roles: string) {
await page.getByTestId('data-testid Datasource HTTP settings url').fill('http://trino:8080');
await page.locator('div').filter({hasText: /^Roles$/}).locator('input').fill(roles);
await page.getByTestId('data-testid Data source settings page Save and Test button').click();
}

async function runRoleQuery(page: Page) {
await page.getByLabel(EXPORT_DATA).click();
await page.locator('div').filter({hasText: /^Format asChoose$/}).locator('svg').click();
await page.getByRole('option', {name: 'Table'}).click();
await setQuery(page, 'SHOW ROLES FROM hive')
await page.getByTestId('data-testid Code editor container').click();
await page.getByTestId('data-testid RefreshPicker run button').click();
}

async function setQuery(page: Page, query: string) {
await page.getByTestId('data-testid Code editor container').click({ clickCount: 4 });
await page.keyboard.type(query);
}
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface TrinoDataSourceOptions extends DataSourceJsonData {
tokenUrl?: string;
clientId?: string;
impersonationUser?: string;
roles?: string;
clientTags?: string;
}
/**
Expand Down
5 changes: 5 additions & 0 deletions test-data/trino/catalog/hive.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
connector.name=hive
hive.metastore=file
hive.metastore.catalog.dir=/tmp/metastore
hive.security=sql-standard
fs.hadoop.enabled=true
Loading