Skip to content

Commit 4f875c1

Browse files
feat: Improve messaging for common cloud config and rpc errors (#2885)
* feat: Improve messaging for common cloud config and rpc errors 1. Add a UnaryClientInterceptor for all gRPC client connections to catch and rewrite unauthenticated error messages 2. Put the sqlc auth token in the config struct, and add simple validation 3. Add more explanatory error messages in cases where users try to use cloud features without the proper configuration 4. Prevent sending the SQLC_AUTH_TOKEN env var to plugins Resolves #2881 * Don't continue
1 parent 4ec9bfa commit 4f875c1

File tree

9 files changed

+112
-20
lines changed

9 files changed

+112
-20
lines changed

internal/bundler/upload.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package bundler
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
6-
"os"
77

88
"google.golang.org/protobuf/encoding/protojson"
99

@@ -13,8 +13,20 @@ import (
1313
pb "github.com/sqlc-dev/sqlc/internal/quickdb/v1"
1414
)
1515

16+
var ErrNoProject = errors.New(`project uploads require a cloud project
17+
18+
If you don't have a project, you can create one from the sqlc Cloud
19+
dashboard at https://dashboard.sqlc.dev/. If you have a project, ensure
20+
you've set its id as the value of the "project" field within the "cloud"
21+
section of your sqlc configuration. The id will look similar to
22+
"01HA8TWGMYPHK0V2GGMB3R2TP9".`)
23+
var ErrNoAuthToken = errors.New(`project uploads require an auth token
24+
25+
If you don't have an auth token, you can create one from the sqlc Cloud
26+
dashboard at https://dashboard.sqlc.dev/. If you have an auth token, ensure
27+
you've set it as the value of the SQLC_AUTH_TOKEN environment variable.`)
28+
1629
type Uploader struct {
17-
token string
1830
configPath string
1931
config *config.Config
2032
dir string
@@ -23,7 +35,6 @@ type Uploader struct {
2335

2436
func NewUploader(configPath, dir string, conf *config.Config) *Uploader {
2537
return &Uploader{
26-
token: os.Getenv("SQLC_AUTH_TOKEN"),
2738
configPath: configPath,
2839
config: conf,
2940
dir: dir,
@@ -32,10 +43,10 @@ func NewUploader(configPath, dir string, conf *config.Config) *Uploader {
3243

3344
func (up *Uploader) Validate() error {
3445
if up.config.Cloud.Project == "" {
35-
return fmt.Errorf("cloud.project is not set")
46+
return ErrNoProject
3647
}
37-
if up.token == "" {
38-
return fmt.Errorf("SQLC_AUTH_TOKEN environment variable is not set")
48+
if up.config.Cloud.AuthToken == "" {
49+
return ErrNoAuthToken
3950
}
4051
if up.client == nil {
4152
client, err := quickdb.NewClientFromConfig(up.config.Cloud)

internal/cmd/vet.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ func (c *checker) fetchDatabaseUri(ctx context.Context, s config.SQL) (string, f
400400
uri, err := c.DSN(s.Database.URI)
401401
return uri, cleanup, err
402402
}
403+
403404
if s.Engine != config.EnginePostgreSQL {
404405
return "", cleanup, fmt.Errorf("managed: only PostgreSQL currently")
405406
}
@@ -418,8 +419,8 @@ func (c *checker) fetchDatabaseUri(ctx context.Context, s config.SQL) (string, f
418419
if err != nil {
419420
return "", cleanup, err
420421
}
421-
for _, query := range files {
422-
contents, err := os.ReadFile(query)
422+
for _, schema := range files {
423+
contents, err := os.ReadFile(schema)
423424
if err != nil {
424425
return "", cleanup, fmt.Errorf("read file: %w", err)
425426
}

internal/config/config.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type Cloud struct {
7272
Organization string `json:"organization" yaml:"organization"`
7373
Project string `json:"project" yaml:"project"`
7474
Hostname string `json:"hostname" yaml:"hostname"`
75+
AuthToken string `json:"-" yaml:"-"`
7576
}
7677

7778
type Plugin struct {
@@ -186,9 +187,23 @@ var ErrPluginNoName = errors.New("missing plugin name")
186187
var ErrPluginExists = errors.New("a plugin with that name already exists")
187188
var ErrPluginNotFound = errors.New("no plugin found")
188189
var ErrPluginNoType = errors.New("plugin: field `process` or `wasm` required")
189-
var ErrPluginBothTypes = errors.New("plugin: both `process` and `wasm` cannot both be defined")
190+
var ErrPluginBothTypes = errors.New("plugin: `process` and `wasm` cannot both be defined")
190191
var ErrPluginProcessNoCmd = errors.New("plugin: missing process command")
191192

193+
var ErrInvalidDatabase = errors.New("database must be managed or have a non-empty URI")
194+
var ErrManagedDatabaseNoProject = errors.New(`managed databases require a cloud project
195+
196+
If you don't have a project, you can create one from the sqlc Cloud
197+
dashboard at https://dashboard.sqlc.dev/. If you have a project, ensure
198+
you've set its id as the value of the "project" field within the "cloud"
199+
section of your sqlc configuration. The id will look similar to
200+
"01HA8TWGMYPHK0V2GGMB3R2TP9".`)
201+
var ErrManagedDatabaseNoAuthToken = errors.New(`managed databases require an auth token
202+
203+
If you don't have an auth token, you can create one from the sqlc Cloud
204+
dashboard at https://dashboard.sqlc.dev/. If you have an auth token, ensure
205+
you've set it as the value of the SQLC_AUTH_TOKEN environment variable.`)
206+
192207
func ParseConfig(rd io.Reader) (Config, error) {
193208
var buf bytes.Buffer
194209
var config Config
@@ -202,14 +217,26 @@ func ParseConfig(rd io.Reader) (Config, error) {
202217
if version.Number == "" {
203218
return config, ErrMissingVersion
204219
}
220+
var err error
205221
switch version.Number {
206222
case "1":
207-
return v1ParseConfig(&buf)
223+
config, err = v1ParseConfig(&buf)
224+
if err != nil {
225+
return config, err
226+
}
208227
case "2":
209-
return v2ParseConfig(&buf)
228+
config, err = v2ParseConfig(&buf)
229+
if err != nil {
230+
return config, err
231+
}
210232
default:
211233
return config, ErrUnknownVersion
212234
}
235+
err = config.addEnvVars()
236+
if err != nil {
237+
return config, err
238+
}
239+
return config, nil
213240
}
214241

215242
type CombinedSettings struct {

internal/config/env.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
)
8+
9+
func (c *Config) addEnvVars() error {
10+
authToken := os.Getenv("SQLC_AUTH_TOKEN")
11+
if authToken != "" && !strings.HasPrefix(authToken, "sqlc_") {
12+
return fmt.Errorf("$SQLC_AUTH_TOKEN doesn't start with \"sqlc_\"")
13+
}
14+
c.Cloud.AuthToken = authToken
15+
16+
return nil
17+
}

internal/config/validate.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package config
22

3-
import "fmt"
4-
53
func Validate(c *Config) error {
64
for _, sql := range c.SQL {
75
if sql.Database != nil {
86
if sql.Database.URI == "" && !sql.Database.Managed {
9-
return fmt.Errorf("invalid config: database must be managed or have a non-empty URI")
7+
return ErrInvalidDatabase
8+
}
9+
if sql.Database.Managed {
10+
if c.Cloud.Project == "" {
11+
return ErrManagedDatabaseNoProject
12+
}
13+
if c.Cloud.AuthToken == "" {
14+
return ErrManagedDatabaseNoAuthToken
15+
}
1016
}
1117
}
1218
}

internal/ext/process/gen.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ func (r Runner) Generate(ctx context.Context, req *plugin.CodeGenRequest) (*plug
3737
fmt.Sprintf("SQLC_VERSION=%s", req.SqlcVersion),
3838
}
3939
for _, key := range r.Env {
40+
if key == "SQLC_AUTH_TOKEN" {
41+
continue
42+
}
4043
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, os.Getenv(key)))
4144
}
4245

internal/quickdb/rpc.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,21 @@ package quickdb
22

33
import (
44
"crypto/tls"
5-
"os"
65

76
"github.com/riza-io/grpc-go/credentials/basic"
87
"google.golang.org/grpc"
98
"google.golang.org/grpc/credentials"
109

1110
"github.com/sqlc-dev/sqlc/internal/config"
1211
pb "github.com/sqlc-dev/sqlc/internal/quickdb/v1"
12+
"github.com/sqlc-dev/sqlc/internal/rpc"
1313
)
1414

1515
const defaultHostname = "grpc.sqlc.dev"
1616

1717
func NewClientFromConfig(cloudConfig config.Cloud) (pb.QuickClient, error) {
1818
projectID := cloudConfig.Project
19-
authToken := os.Getenv("SQLC_AUTH_TOKEN")
20-
return NewClient(projectID, authToken, WithHost(cloudConfig.Hostname))
19+
return NewClient(projectID, cloudConfig.AuthToken, WithHost(cloudConfig.Hostname))
2120
}
2221

2322
type options struct {
@@ -41,6 +40,7 @@ func NewClient(project, token string, opts ...Option) (pb.QuickClient, error) {
4140
dialOpts := []grpc.DialOption{
4241
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),
4342
grpc.WithPerRPCCredentials(basic.NewPerRPCCredentials(project, token)),
43+
grpc.WithUnaryInterceptor(rpc.UnaryInterceptor),
4444
}
4545

4646
hostname := o.hostname

internal/remote/rpc.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@ package remote
22

33
import (
44
"crypto/tls"
5-
"os"
65

76
"github.com/riza-io/grpc-go/credentials/basic"
87
"google.golang.org/grpc"
98
"google.golang.org/grpc/credentials"
109

1110
"github.com/sqlc-dev/sqlc/internal/config"
11+
"github.com/sqlc-dev/sqlc/internal/rpc"
1212
)
1313

1414
const defaultHostname = "remote.sqlc.dev"
1515

1616
func NewClient(cloudConfig config.Cloud) (GenClient, error) {
1717
authID := cloudConfig.Organization + "/" + cloudConfig.Project
18-
authToken := os.Getenv("SQLC_AUTH_TOKEN")
1918
opts := []grpc.DialOption{
2019
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),
21-
grpc.WithPerRPCCredentials(basic.NewPerRPCCredentials(authID, authToken)),
20+
grpc.WithPerRPCCredentials(basic.NewPerRPCCredentials(authID, cloudConfig.AuthToken)),
21+
grpc.WithUnaryInterceptor(rpc.UnaryInterceptor),
2222
}
2323

2424
hostname := cloudConfig.Hostname

internal/rpc/interceptor.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package rpc
2+
3+
import (
4+
"context"
5+
6+
"google.golang.org/grpc"
7+
"google.golang.org/grpc/codes"
8+
"google.golang.org/grpc/status"
9+
)
10+
11+
const errMessageUnauthenticated = `rpc authentication failed
12+
13+
You may be using a sqlc auth token that was created for a different project,
14+
or your auth token may have expired.`
15+
16+
func UnaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
17+
err := invoker(ctx, method, req, reply, cc, opts...)
18+
19+
switch status.Convert(err).Code() {
20+
case codes.OK:
21+
return nil
22+
case codes.Unauthenticated:
23+
return status.New(codes.Unauthenticated, errMessageUnauthenticated).Err()
24+
default:
25+
return err
26+
}
27+
}

0 commit comments

Comments
 (0)