diff --git a/cli/command/export_test.go b/cli/command/export_test.go new file mode 100644 index 000000000000..2b0269d15560 --- /dev/null +++ b/cli/command/export_test.go @@ -0,0 +1,14 @@ +package command + +import ( + "testing" + + configtypes "github.com/docker/cli/cli/config/types" +) + +func SetDesktopAuthConfig(t *testing.T, fn func(registryHostname string) (configtypes.AuthConfig, bool)) { + t.Helper() + orig := desktopAuthConfig + desktopAuthConfig = fn + t.Cleanup(func() { desktopAuthConfig = orig }) +} diff --git a/cli/command/registry.go b/cli/command/registry.go index 2e767d5b72f3..4893a9fa7187 100644 --- a/cli/command/registry.go +++ b/cli/command/registry.go @@ -15,6 +15,7 @@ import ( "github.com/docker/cli/cli/hints" "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/prompt" + "github.com/docker/cli/internal/secretsengine" "github.com/docker/cli/internal/tui" "github.com/moby/moby/api/pkg/authconfig" registrytypes "github.com/moby/moby/api/types/registry" @@ -35,6 +36,16 @@ const ( // [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer const authConfigKey = "https://index.docker.io/v1/" +var desktopAuthConfig = secretsengine.AuthConfig + +func envAuthConfig(registryHostname string) (configtypes.AuthConfig, bool) { + if os.Getenv(configfile.DockerEnvConfigKey) == "" { + return configtypes.AuthConfig{}, false + } + authConfig, err := configfile.New("").GetAuthConfig(registryHostname) + return authConfig, err == nil && authConfig.Username != "" +} + // ResolveAuthConfig returns auth-config for the given registry from the // credential-store. It returns an empty AuthConfig if no credentials were // found. @@ -199,9 +210,16 @@ func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (strin if err != nil { return "", err } - authConfig, err := cfg.GetAuthConfig(reference.Domain(registryRef)) - if err != nil { - return "", err + domain := reference.Domain(registryRef) + authConfig, ok := envAuthConfig(domain) + if !ok { + authConfig, ok = desktopAuthConfig(domain) + } + if !ok { + authConfig, err = cfg.GetAuthConfig(domain) + if err != nil { + return "", err + } } return authconfig.Encode(registrytypes.AuthConfig{ diff --git a/cli/command/registry_test.go b/cli/command/registry_test.go index 751711f36391..27fc8f18e6d7 100644 --- a/cli/command/registry_test.go +++ b/cli/command/registry_test.go @@ -96,7 +96,84 @@ func TestGetDefaultAuthConfig_HelperError(t *testing.T) { assert.Check(t, is.ErrorContains(err, "docker-credential-fake-does-not-exist")) } +func noDesktopSession(string) (configtypes.AuthConfig, bool) { + return configtypes.AuthConfig{}, false +} + +func TestRetrieveAuthTokenFromImageDesktopSession(t *testing.T) { + const configFileContent = `{"auths": { + "https://index.docker.io/v1/": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}, + "ghcr.io": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="} + } +}` + cfg := configfile.ConfigFile{} + assert.NilError(t, cfg.LoadFromReader(bytes.NewReader([]byte(configFileContent)))) + + var lookups []string + command.SetDesktopAuthConfig(t, func(registryHostname string) (configtypes.AuthConfig, bool) { + lookups = append(lookups, registryHostname) + if registryHostname != "docker.io" { + return configtypes.AuthConfig{}, false + } + return configtypes.AuthConfig{ + Username: "hubuser", + Password: "desktop-token", + ServerAddress: "https://index.docker.io/v1/", + }, true + }) + + tests := []struct { + imageRef string + expected registry.AuthConfig + }{ + { + imageRef: "ubuntu", + expected: registry.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: "https://index.docker.io/v1/"}, + }, + { + imageRef: "ghcr.io/foo/bar", + expected: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "ghcr.io"}, + }, + } + for _, tc := range tests { + t.Run(tc.imageRef, func(t *testing.T) { + actual, err := command.RetrieveAuthTokenFromImage(&cfg, tc.imageRef) + assert.NilError(t, err) + expected, err := authconfig.Encode(tc.expected) + assert.NilError(t, err) + assert.Equal(t, actual, expected) + }) + } + assert.Check(t, is.DeepEqual(lookups, []string{"docker.io", "ghcr.io"})) +} + +func TestRetrieveAuthTokenFromImageEnvTakesPrecedenceOverDesktop(t *testing.T) { + t.Setenv(configfile.DockerEnvConfigKey, `{"auths":{"https://index.docker.io/v1/":{"auth":"ZW52dXNlcjplbnZwYXNz"}}}`) + + var lookups []string + command.SetDesktopAuthConfig(t, func(registryHostname string) (configtypes.AuthConfig, bool) { + lookups = append(lookups, registryHostname) + return configtypes.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: registryHostname}, true + }) + cfg := configfile.ConfigFile{} + + actual, err := command.RetrieveAuthTokenFromImage(&cfg, "ubuntu") + assert.NilError(t, err) + expected, err := authconfig.Encode(registry.AuthConfig{Username: "envuser", Password: "envpass", ServerAddress: "https://index.docker.io/v1/"}) + assert.NilError(t, err) + assert.Check(t, is.Equal(actual, expected)) + assert.Check(t, is.Len(lookups, 0), "Docker Desktop must not be asked for a registry in DOCKER_AUTH_CONFIG") + + actual, err = command.RetrieveAuthTokenFromImage(&cfg, "dhi.io/python") + assert.NilError(t, err) + expected, err = authconfig.Encode(registry.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: "dhi.io"}) + assert.NilError(t, err) + assert.Check(t, is.Equal(actual, expected)) + assert.Check(t, is.DeepEqual(lookups, []string{"dhi.io"})) +} + func TestRetrieveAuthTokenFromImage(t *testing.T) { + command.SetDesktopAuthConfig(t, noDesktopSession) // configFileContent contains a plain-text "username:password", as stored by // the plain-text store; // https://github.com/docker/cli/blob/v28.0.4/cli/config/configfile/file.go#L218-L229 diff --git a/docs/reference/commandline/login.md b/docs/reference/commandline/login.md index 458edd7b91b0..660e9258352f 100644 --- a/docs/reference/commandline/login.md +++ b/docs/reference/commandline/login.md @@ -70,6 +70,18 @@ With Docker Desktop, the credential store is already installed and configured for you. Unless you want to change the credential store used by Docker Desktop, you can skip the following steps. +#### Docker Desktop sign-in + +When Docker Desktop is running and you are signed in, commands that pull or +push images through the Docker Engine, such as `docker pull`, `docker push`, +`docker run`, and `docker service create`, use the Docker Desktop session for +Docker Hub (`docker.io`) and Docker Hardened Images (`dhi.io`). The session is +used before the credential store, credential helpers, and the configuration +file. Credentials set in the `DOCKER_AUTH_CONFIG` environment variable take +precedence over the Docker Desktop session. If Docker Desktop is not installed +or not running, or you are not signed in, the Docker CLI uses its own +credentials. + #### Configure the credential store You need to specify the credential store in `$HOME/.docker/config.json` diff --git a/internal/secretsengine/engine_test.go b/internal/secretsengine/engine_test.go new file mode 100644 index 000000000000..85182476907e --- /dev/null +++ b/internal/secretsengine/engine_test.go @@ -0,0 +1,404 @@ +//go:build go1.26 + +package secretsengine + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "sync" + "sync/atomic" + "testing" + "time" + + seclient "github.com/docker/secrets-engine/client" + "github.com/docker/secrets-engine/client/dockerhub" + "github.com/docker/secrets-engine/x/api/resolver" + "github.com/docker/secrets-engine/x/api/resolver/v1/resolverv1connect" + "github.com/docker/secrets-engine/x/secrets" + "gotest.tools/v3/assert" +) + +const benchSocketEnv = "SECRETSENGINE_BENCH_SOCKET" + +type fakeResolver struct { + latency time.Duration + values map[string][]byte + calls atomic.Int64 +} + +func (r *fakeResolver) GetSecrets(ctx context.Context, pattern secrets.Pattern) ([]secrets.Envelope, error) { + r.calls.Add(1) + if r.latency > 0 { + select { + case <-time.After(r.latency): + case <-ctx.Done(): + return nil, ctx.Err() + } + } + var envelopes []secrets.Envelope + for key, value := range r.values { + id := secrets.MustParseID(key) + if pattern.Match(id) { + envelopes = append(envelopes, secrets.Envelope{ID: id, Value: value, ResolvedAt: time.Now()}) + } + } + if len(envelopes) == 0 { + return nil, secrets.ErrNotFound + } + return envelopes, nil +} + +func fakeAccessToken() string { + enc := base64.RawURLEncoding.EncodeToString + return enc([]byte(`{"alg":"RS256","typ":"JWT"}`)) + "." + enc([]byte(`{"sub":"bench"}`)) + "." + enc([]byte("signature")) +} + +func signedIn(tb testing.TB, expiresAt time.Time) map[string][]byte { + tb.Helper() + const username = "hubuser" + userID := "docker/auth/hub/" + username + profile, err := json.Marshal(dockerhub.Profile{UserID: userID, Username: username, Email: username + "@example.com"}) + assert.NilError(tb, err) + s := dockerhub.UserSession{AccessToken: fakeAccessToken()} + s.Claims.Username = username + s.Claims.ExpiresAt = &dockerhub.NumericDate{Time: expiresAt} + userSession, err := json.Marshal(s) + assert.NilError(tb, err) + return map[string][]byte{ + "docker/auth/metadata/hub/default": profile, + userID: userSession, + } +} + +var socketDirs atomic.Int64 + +func socketDir(tb testing.TB) string { + tb.Helper() + dir := filepath.Join(os.TempDir(), fmt.Sprintf("se-%d-%d", os.Getpid(), socketDirs.Add(1))) + assert.NilError(tb, os.Mkdir(dir, 0o700)) + tb.Cleanup(func() { _ = os.RemoveAll(dir) }) + return dir +} + +type fakeEngine struct { + socketPath string + + mu sync.Mutex + conns map[net.Conn]struct{} +} + +func startEngine(tb testing.TB, r *fakeResolver) *fakeEngine { + tb.Helper() + e := &fakeEngine{ + socketPath: filepath.Join(socketDir(tb), "engine.sock"), + conns: map[net.Conn]struct{}{}, + } + ln, err := net.Listen("unix", e.socketPath) + assert.NilError(tb, err) + mux := http.NewServeMux() + mux.Handle(resolverv1connect.NewResolverServiceHandler(resolver.NewResolverHandler(r))) + srv := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second, ConnState: e.track} + go func() { _ = srv.Serve(ln) }() + tb.Cleanup(func() { _ = srv.Close() }) + return e +} + +func (e *fakeEngine) track(c net.Conn, state http.ConnState) { + e.mu.Lock() + defer e.mu.Unlock() + switch state { + case http.StateNew: + e.conns[c] = struct{}{} + case http.StateClosed, http.StateHijacked: + delete(e.conns, c) + case http.StateActive, http.StateIdle: + } +} + +func (e *fakeEngine) dropConnections() { + e.mu.Lock() + defer e.mu.Unlock() + for c := range e.conns { + _ = c.Close() + } + clear(e.conns) +} + +func startHungEngine(tb testing.TB) string { + tb.Helper() + socketPath := filepath.Join(socketDir(tb), "engine.sock") + ln, err := net.Listen("unix", socketPath) + assert.NilError(tb, err) + var mu sync.Mutex + var held []net.Conn + go func() { + for { + c, err := ln.Accept() + if err != nil { + return + } + mu.Lock() + held = append(held, c) + mu.Unlock() + } + }() + tb.Cleanup(func() { + _ = ln.Close() + mu.Lock() + defer mu.Unlock() + for _, c := range held { + _ = c.Close() + } + }) + return socketPath +} + +func staleSocket(tb testing.TB) string { + tb.Helper() + socketPath := filepath.Join(socketDir(tb), "engine.sock") + ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"}) + assert.NilError(tb, err) + ln.SetUnlinkOnClose(false) + assert.NilError(tb, ln.Close()) + _, err = os.Lstat(socketPath) + assert.NilError(tb, err) + return socketPath +} + +func socketSession(socketPath string, timeout time.Duration) *session { + return &session{ + name: "Docker Hub", + connect: func() (hubSessions, error) { + return connect(socketPath) + }, + timeout: timeout, + now: time.Now, + } +} + +func TestEngineSignedIn(t *testing.T) { + r := &fakeResolver{values: signedIn(t, time.Now().Add(time.Hour))} + e := startEngine(t, r) + p := &provider{hub: socketSession(e.socketPath, lookupTimeout)} + + got, ok := p.authConfig("docker.io") + assert.Assert(t, ok) + assert.Equal(t, got.Username, "hubuser") + assert.Equal(t, got.Password, fakeAccessToken()) + assert.Equal(t, got.ServerAddress, indexServer) + assert.Equal(t, got.IdentityToken, "") + + got, ok = p.authConfig("dhi.io") + assert.Assert(t, ok) + assert.Equal(t, got.ServerAddress, dhiRegistryHost) + assert.Equal(t, r.calls.Load(), int64(2), "the profile and the session are read once, then cached") +} + +func TestEngineSignedOut(t *testing.T) { + r := &fakeResolver{} + e := startEngine(t, r) + p := &provider{hub: socketSession(e.socketPath, lookupTimeout)} + + _, ok := p.authConfig("docker.io") + assert.Assert(t, !ok) + assert.Equal(t, r.calls.Load(), int64(1)) +} + +func TestMissingSocketFallsBackImmediately(t *testing.T) { + s := socketSession(filepath.Join(socketDir(t), "engine.sock"), lookupTimeout) + + start := time.Now() + _, ok := s.auth() + assert.Assert(t, !ok) + assert.Assert(t, time.Since(start) < time.Second) + assert.Assert(t, s.hub == nil, "no client is created without a socket") +} + +func TestStaleSocketFallsBackImmediately(t *testing.T) { + s := socketSession(staleSocket(t), lookupTimeout) + + start := time.Now() + _, ok := s.auth() + assert.Assert(t, !ok) + assert.Assert(t, time.Since(start) < time.Second) +} + +func TestHungEngineIsBounded(t *testing.T) { + const timeout = 200 * time.Millisecond + s := socketSession(startHungEngine(t), timeout) + + start := time.Now() + _, ok := s.auth() + elapsed := time.Since(start) + assert.Assert(t, !ok) + assert.Assert(t, elapsed >= timeout, elapsed) + assert.Assert(t, elapsed < timeout+2*time.Second, elapsed) +} + +func benchmarkColdLookup(b *testing.B, socketPath string, e *fakeEngine, want bool) { + b.Helper() + b.ReportAllocs() + for range b.N { + s := socketSession(socketPath, lookupTimeout) + if _, ok := s.auth(); ok != want { + b.Fatalf("got a Desktop session: %t, want %t", ok, want) + } + if e != nil { + b.StopTimer() + e.dropConnections() + b.StartTimer() + } + } +} + +func BenchmarkAuthConfig(b *testing.B) { + b.Run("other-registry", func(b *testing.B) { + p := &provider{hub: socketSession(filepath.Join(socketDir(b), "engine.sock"), lookupTimeout)} + b.ReportAllocs() + for range b.N { + if _, ok := p.authConfig("ghcr.io"); ok { + b.Fatal("unexpected Desktop session") + } + } + }) + b.Run("missing-socket", func(b *testing.B) { + benchmarkColdLookup(b, filepath.Join(socketDir(b), "engine.sock"), nil, false) + }) + b.Run("missing-socket-without-stat", func(b *testing.B) { + socketPath := filepath.Join(socketDir(b), "engine.sock") + b.ReportAllocs() + for range b.N { + c, err := seclient.New(seclient.WithSocketPath(socketPath), seclient.WithTimeout(lookupTimeout)) + if err != nil { + b.Fatal(err) + } + if _, err := c.HubAuth().GetDefaultSession(context.Background()); err == nil { + b.Fatal("unexpected Desktop session") + } + } + }) + b.Run("stale-socket", func(b *testing.B) { + benchmarkColdLookup(b, staleSocket(b), nil, false) + }) + b.Run("signed-out", func(b *testing.B) { + e := startEngine(b, &fakeResolver{}) + benchmarkColdLookup(b, e.socketPath, e, false) + }) + b.Run("signed-in", func(b *testing.B) { + e := startEngine(b, &fakeResolver{values: signedIn(b, time.Now().Add(time.Hour))}) + benchmarkColdLookup(b, e.socketPath, e, true) + }) + b.Run("signed-in-10ms-engine", func(b *testing.B) { + e := startEngine(b, &fakeResolver{latency: 10 * time.Millisecond, values: signedIn(b, time.Now().Add(time.Hour))}) + benchmarkColdLookup(b, e.socketPath, e, true) + }) + b.Run("signed-in-refetch", func(b *testing.B) { + e := startEngine(b, &fakeResolver{values: signedIn(b, time.Now().Add(time.Hour))}) + s := socketSession(e.socketPath, lookupTimeout) + if _, ok := s.auth(); !ok { + b.Fatal("no Desktop session") + } + b.ReportAllocs() + b.ResetTimer() + for range b.N { + s.cached = nil + if _, ok := s.auth(); !ok { + b.Fatal("no Desktop session") + } + } + }) + b.Run("signed-in-cached", func(b *testing.B) { + e := startEngine(b, &fakeResolver{values: signedIn(b, time.Now().Add(time.Hour))}) + s := socketSession(e.socketPath, lookupTimeout) + if _, ok := s.auth(); !ok { + b.Fatal("no Desktop session") + } + b.ReportAllocs() + b.ResetTimer() + for range b.N { + if _, ok := s.auth(); !ok { + b.Fatal("no Desktop session") + } + } + }) +} + +type trackingDialer struct { + mu sync.Mutex + conns []net.Conn +} + +func (d *trackingDialer) dial(socketPath string) func(ctx context.Context, _, _ string) (net.Conn, error) { + return func(ctx context.Context, _, _ string) (net.Conn, error) { + var nd net.Dialer + c, err := nd.DialContext(ctx, "unix", socketPath) + if err != nil { + return nil, err + } + d.mu.Lock() + d.conns = append(d.conns, c) + d.mu.Unlock() + return c, nil + } +} + +func (d *trackingDialer) closeAll() { + d.mu.Lock() + defer d.mu.Unlock() + for _, c := range d.conns { + _ = c.Close() + } + d.conns = nil +} + +func BenchmarkExternalEngine(b *testing.B) { + socketPath := os.Getenv(benchSocketEnv) + if socketPath == "" { + b.Skipf("%s is not set", benchSocketEnv) + } + b.Run("cold", func(b *testing.B) { + b.ReportAllocs() + for range b.N { + var dialer trackingDialer + s := &session{ + name: "Docker Hub", + connect: func() (hubSessions, error) { + c, err := seclient.New(seclient.WithDialContext(dialer.dial(socketPath)), seclient.WithTimeout(lookupTimeout)) + if err != nil { + return nil, err + } + return c.HubAuth(), nil + }, + timeout: lookupTimeout, + now: time.Now, + } + _, ok := s.auth() + b.StopTimer() + dialer.closeAll() + if !ok { + b.Fatal("no Docker Hub session from the engine at " + socketPath) + } + b.StartTimer() + } + }) + b.Run("refetch", func(b *testing.B) { + s := socketSession(socketPath, lookupTimeout) + if _, ok := s.auth(); !ok { + b.Fatal("no Docker Hub session from the engine at " + socketPath) + } + b.ReportAllocs() + b.ResetTimer() + for range b.N { + s.cached = nil + if _, ok := s.auth(); !ok { + b.Fatal("no Docker Hub session from the engine at " + socketPath) + } + } + }) +} diff --git a/internal/secretsengine/secretsengine.go b/internal/secretsengine/secretsengine.go new file mode 100644 index 000000000000..6840c5d53cba --- /dev/null +++ b/internal/secretsengine/secretsengine.go @@ -0,0 +1,215 @@ +//go:build go1.26 + +package secretsengine + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/docker/cli/cli/config/credentials" + "github.com/docker/cli/cli/config/types" + seclient "github.com/docker/secrets-engine/client" + "github.com/docker/secrets-engine/client/dockerhub" + "github.com/docker/secrets-engine/x/api" + "github.com/sirupsen/logrus" +) + +const ( + lookupTimeout = 5 * time.Second + sessionExpirySkew = 30 * time.Second + sessionTTL = time.Minute + retryInterval = time.Minute + + indexServer = "https://index.docker.io/v1/" + dhiRegistryHost = "dhi.io" + stagingRegistryHost = "registry-1-stage.docker.io" +) + +var ( + errNoUsername = errors.New("no username for the session") + errSessionExpired = errors.New("the session has expired (not signed in to Docker Desktop?)") +) + +type hubSessions interface { + GetDefaultSession(ctx context.Context) (dockerhub.UserSession, error) + GetDefaultProfile(ctx context.Context) (dockerhub.Profile, error) +} + +var _ hubSessions = dockerhub.ClientAuth(nil) + +type provider struct { + hub *session + staging *session +} + +var shared = &provider{ + hub: newSession("Docker Hub"), + staging: newSession("Docker Hub staging", dockerhub.Staging()), +} + +var disabled atomic.Bool + +func Disable() { + disabled.Store(true) +} + +func AuthConfig(registryHostname string) (types.AuthConfig, bool) { + if disabled.Load() { + return types.AuthConfig{}, false + } + return shared.authConfig(registryHostname) +} + +func (p *provider) authConfig(registryHostname string) (types.AuthConfig, bool) { + s, serverAddress := p.sessionFor(registryHostname) + if s == nil { + return types.AuthConfig{}, false + } + auth, ok := s.auth() + if !ok { + return types.AuthConfig{}, false + } + auth.ServerAddress = serverAddress + return auth, true +} + +func (p *provider) sessionFor(registryHostname string) (*session, string) { + switch credentials.ConvertToHostname(registryHostname) { + case "docker.io", "index.docker.io", "registry-1.docker.io": + return p.hub, indexServer + case dhiRegistryHost: + return p.hub, dhiRegistryHost + case stagingRegistryHost: + return p.staging, stagingRegistryHost + default: + return nil, "" + } +} + +type session struct { + name string + connect func() (hubSessions, error) + timeout time.Duration + now func() time.Time + + mu sync.Mutex + hub hubSessions + retryAt time.Time + cached *types.AuthConfig + validUntil time.Time +} + +func newSession(name string, opts ...dockerhub.Option) *session { + return &session{ + name: name, + connect: func() (hubSessions, error) { + return connect(api.DesktopSocketPath(), opts...) + }, + timeout: lookupTimeout, + now: time.Now, + } +} + +func connect(socketPath string, opts ...dockerhub.Option) (hubSessions, error) { + if _, err := os.Lstat(socketPath); errors.Is(err, fs.ErrNotExist) { + return nil, err + } + c, err := seclient.New( + seclient.WithSocketPath(socketPath), + seclient.WithTimeout(lookupTimeout), + ) + if err != nil { + return nil, err + } + return c.HubAuth(opts...), nil +} + +func (s *session) auth() (types.AuthConfig, bool) { + s.mu.Lock() + defer s.mu.Unlock() + + now := s.now() + if now.Before(s.retryAt) { + return types.AuthConfig{}, false + } + if s.cached != nil && now.Before(s.validUntil) { + return *s.cached, true + } + auth, validUntil, err := s.fetch() + if err != nil { + s.retryAt = s.now().Add(retryInterval) + s.cached = nil + logFallback(s.name, err) + return types.AuthConfig{}, false + } + logrus.Debugf("using the %s session of %q from Docker Desktop", s.name, auth.Username) + s.cached, s.validUntil = &auth, validUntil + return auth, true +} + +func (s *session) fetch() (types.AuthConfig, time.Time, error) { + if s.hub == nil { + hub, err := s.connect() + if err != nil { + return types.AuthConfig{}, time.Time{}, err + } + s.hub = hub + } + + ctx, cancel := context.WithTimeout(context.Background(), s.timeout) + defer cancel() + + userSession, err := s.hub.GetDefaultSession(ctx) + if err != nil { + return types.AuthConfig{}, time.Time{}, err + } + + username := userSession.Claims.Username + if username == "" { + profile, err := s.hub.GetDefaultProfile(ctx) + if err != nil { + return types.AuthConfig{}, time.Time{}, fmt.Errorf("resolving the Docker Hub username: %w", err) + } + username = profile.Username + } + if username == "" { + return types.AuthConfig{}, time.Time{}, errNoUsername + } + + now := s.now() + validUntil := now.Add(sessionTTL) + if exp := userSession.Claims.ExpiresAt; exp != nil { + validUntil = exp.Add(-sessionExpirySkew) + if !now.Before(validUntil) { + return types.AuthConfig{}, time.Time{}, errSessionExpired + } + } + + return types.AuthConfig{ + Username: username, + Password: userSession.AccessToken, + }, validUntil, nil +} + +func logFallback(name string, err error) { + if isSessionUnavailable(err) { + logrus.Debugf("no %s session available from Docker Desktop, using Docker CLI credentials: %v", name, err) + return + } + logrus.Warnf("could not use the %s session from Docker Desktop, using Docker CLI credentials instead: %v", name, err) +} + +func isSessionUnavailable(err error) bool { + if _, ok := errors.AsType[*seclient.ConnectError](err); ok { + return true + } + return errors.Is(err, fs.ErrNotExist) || + errors.Is(err, dockerhub.ErrNoSession) || + errors.Is(err, errSessionExpired) +} diff --git a/internal/secretsengine/secretsengine_test.go b/internal/secretsengine/secretsengine_test.go new file mode 100644 index 000000000000..71b799c825dd --- /dev/null +++ b/internal/secretsengine/secretsengine_test.go @@ -0,0 +1,402 @@ +//go:build go1.26 + +package secretsengine + +import ( + "context" + "errors" + "fmt" + "io/fs" + "path/filepath" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/docker/cli/cli/config/types" + seclient "github.com/docker/secrets-engine/client" + "github.com/docker/secrets-engine/client/dockerhub" + "gotest.tools/v3/assert" +) + +var testNow = time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC) + +type fakeHub struct { + session dockerhub.UserSession + sessionErr error + profile dockerhub.Profile + profileErr error + sessionCalls atomic.Int32 +} + +func (f *fakeHub) GetDefaultSession(context.Context) (dockerhub.UserSession, error) { + f.sessionCalls.Add(1) + return f.session, f.sessionErr +} + +func (f *fakeHub) GetDefaultProfile(context.Context) (dockerhub.Profile, error) { + return f.profile, f.profileErr +} + +type fakeClock struct { + mu sync.Mutex + t time.Time +} + +func (c *fakeClock) now() time.Time { + c.mu.Lock() + defer c.mu.Unlock() + return c.t +} + +func (c *fakeClock) advance(d time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + c.t = c.t.Add(d) +} + +func hubSession(username string, expiresAt time.Time) dockerhub.UserSession { + s := dockerhub.UserSession{AccessToken: "desktop-oauth-token"} + s.Claims.Username = username + if !expiresAt.IsZero() { + s.Claims.ExpiresAt = &dockerhub.NumericDate{Time: expiresAt} + } + return s +} + +func desktopCredentials() types.AuthConfig { + return types.AuthConfig{Username: "hubuser", Password: "desktop-oauth-token", ServerAddress: indexServer} +} + +func stagingCredentials() types.AuthConfig { + return types.AuthConfig{Username: "stageuser", Password: "staging-oauth-token", ServerAddress: stagingRegistryHost} +} + +type testProvider struct { + *provider + clock *fakeClock + connects atomic.Int32 + staging *fakeHub +} + +func newTestProvider(hub hubSessions, connectErr error) *testProvider { + tp := &testProvider{ + clock: &fakeClock{t: testNow}, + staging: &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile}, + } + tp.provider = &provider{ + hub: &session{ + name: "Docker Hub", + connect: func() (hubSessions, error) { + tp.connects.Add(1) + if connectErr != nil { + return nil, connectErr + } + return hub, nil + }, + timeout: time.Second, + now: tp.clock.now, + }, + staging: &session{ + name: "Docker Hub staging", + connect: func() (hubSessions, error) { + return tp.staging, nil + }, + timeout: time.Second, + now: tp.clock.now, + }, + } + return tp +} + +func (tp *testProvider) signInToStaging() { + tp.staging.sessionErr = nil + tp.staging.session = hubSession("stageuser", testNow.Add(time.Hour)) + tp.staging.session.AccessToken = "staging-oauth-token" +} + +func connectError(reason seclient.ConnectReason) error { + return &seclient.ConnectError{ + Reason: reason, + SocketPath: "/run/engine.sock", + Err: errors.New("dial unix /run/engine.sock"), + } +} + +func TestDisableSkipsDockerDesktop(t *testing.T) { + Disable() + + _, ok := AuthConfig("docker.io") + assert.Assert(t, !ok) + + shared.hub.mu.Lock() + defer shared.hub.mu.Unlock() + assert.Assert(t, shared.hub.retryAt.IsZero(), "Docker Desktop must not be contacted once disabled") + assert.Assert(t, shared.hub.hub == nil) +} + +func TestSharedProviderHasOneSessionPerEnvironment(t *testing.T) { + assert.Assert(t, shared.hub != nil) + assert.Assert(t, shared.staging != nil) + assert.Assert(t, shared.hub != shared.staging, "production and staging sessions must be separate") +} + +func TestDockerHubUsesDesktopSession(t *testing.T) { + for _, host := range []string{"docker.io", "index.docker.io", "registry-1.docker.io", indexServer} { + t.Run(host, func(t *testing.T) { + hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))} + p := newTestProvider(hub, nil) + + got, ok := p.authConfig(host) + assert.Assert(t, ok) + assert.DeepEqual(t, got, desktopCredentials()) + assert.Equal(t, got.IdentityToken, "", "an IdentityToken is sent as a refresh token") + }) + } +} + +func TestOtherRegistriesDoNotContactDesktop(t *testing.T) { + hosts := []string{ + "ghcr.io", "localhost:5000", "registry.example.com", "", + stagingRegistryHost + ".example.com", "example.com/" + stagingRegistryHost, + dhiRegistryHost + ".example.com", "example.com/" + dhiRegistryHost, "registry." + dhiRegistryHost, + "docker.io.example.com", + } + for _, host := range hosts { + t.Run(host, func(t *testing.T) { + hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))} + p := newTestProvider(hub, nil) + p.signInToStaging() + + _, ok := p.authConfig(host) + assert.Assert(t, !ok) + assert.Equal(t, p.connects.Load(), int32(0), "Docker Desktop must only be contacted for Docker Hub") + assert.Equal(t, p.staging.sessionCalls.Load(), int32(0), "Docker Desktop must only be contacted for Docker Hub") + }) + } +} + +func TestDHIUsesDockerHubSession(t *testing.T) { + for _, host := range []string{dhiRegistryHost, "https://" + dhiRegistryHost, "https://" + dhiRegistryHost + "/"} { + t.Run(host, func(t *testing.T) { + hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))} + p := newTestProvider(hub, nil) + p.signInToStaging() + + want := desktopCredentials() + want.ServerAddress = dhiRegistryHost + got, ok := p.authConfig(host) + assert.Assert(t, ok) + assert.DeepEqual(t, got, want) + assert.Equal(t, p.staging.sessionCalls.Load(), int32(0), "the staging session must not be read for DHI") + + got, ok = p.authConfig("docker.io") + assert.Assert(t, ok) + assert.DeepEqual(t, got, desktopCredentials()) + assert.Equal(t, hub.sessionCalls.Load(), int32(1)) + }) + } +} + +func TestStagingRegistryUsesStagingSession(t *testing.T) { + for _, host := range []string{stagingRegistryHost, "https://" + stagingRegistryHost + "/"} { + t.Run(host, func(t *testing.T) { + hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))} + p := newTestProvider(hub, nil) + p.signInToStaging() + + got, ok := p.authConfig(host) + assert.Assert(t, ok) + assert.DeepEqual(t, got, stagingCredentials()) + assert.Equal(t, hub.sessionCalls.Load(), int32(0), "the production session must not be read for the staging registry") + }) + } +} + +func TestEnvironmentsDoNotMix(t *testing.T) { + t.Run("staging session is not used for docker.io", func(t *testing.T) { + p := newTestProvider(&fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile}, nil) + p.signInToStaging() + + _, ok := p.authConfig("docker.io") + assert.Assert(t, !ok) + + got, ok := p.authConfig(stagingRegistryHost) + assert.Assert(t, ok) + assert.DeepEqual(t, got, stagingCredentials()) + }) + + t.Run("production session is not used for the staging registry", func(t *testing.T) { + p := newTestProvider(&fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}, nil) + + _, ok := p.authConfig(stagingRegistryHost) + assert.Assert(t, !ok) + + got, ok := p.authConfig("docker.io") + assert.Assert(t, ok) + assert.DeepEqual(t, got, desktopCredentials()) + }) +} + +func TestUsernameFromProfile(t *testing.T) { + hub := &fakeHub{ + session: hubSession("", testNow.Add(time.Hour)), + profile: dockerhub.Profile{Username: "hubuser"}, + } + p := newTestProvider(hub, nil) + + got, ok := p.authConfig("docker.io") + assert.Assert(t, ok) + assert.DeepEqual(t, got, desktopCredentials()) +} + +func TestFallsBackWithoutUsableSession(t *testing.T) { + future := testNow.Add(time.Hour) + tests := []struct { + name string + hub *fakeHub + }{ + {name: "secrets engine not running", hub: &fakeHub{sessionErr: connectError(seclient.ReasonNotRunning)}}, + {name: "secrets engine socket not accessible", hub: &fakeHub{sessionErr: connectError(seclient.ReasonPermissionDenied)}}, + {name: "nobody signed in", hub: &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile}}, + {name: "access denied", hub: &fakeHub{sessionErr: seclient.ErrAccessDenied}}, + {name: "no username", hub: &fakeHub{session: hubSession("", future)}}, + {name: "profile lookup fails", hub: &fakeHub{session: hubSession("", future), profileErr: errors.New("boom")}}, + {name: "expired", hub: &fakeHub{session: hubSession("hubuser", testNow.Add(-time.Minute))}}, + {name: "expires within the skew", hub: &fakeHub{session: hubSession("hubuser", testNow.Add(sessionExpirySkew/2))}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + p := newTestProvider(tc.hub, nil) + + for range 2 { + _, ok := p.authConfig("docker.io") + assert.Assert(t, !ok) + } + assert.Equal(t, tc.hub.sessionCalls.Load(), int32(1), "Desktop must not be asked again within retryInterval") + }) + } +} + +func TestFallsBackWhenClientCannotBeCreated(t *testing.T) { + p := newTestProvider(nil, errors.New("no socket path")) + + for range 2 { + _, ok := p.authConfig("docker.io") + assert.Assert(t, !ok) + } + assert.Equal(t, p.connects.Load(), int32(1)) +} + +func TestRetriesAfterInterval(t *testing.T) { + hub := &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile} + p := newTestProvider(hub, nil) + + _, ok := p.authConfig("docker.io") + assert.Assert(t, !ok) + + hub.sessionErr = nil + hub.session = hubSession("hubuser", testNow.Add(time.Hour)) + + p.clock.advance(retryInterval - time.Second) + _, ok = p.authConfig("docker.io") + assert.Assert(t, !ok) + assert.Equal(t, hub.sessionCalls.Load(), int32(1)) + + p.clock.advance(time.Second) + got, ok := p.authConfig("docker.io") + assert.Assert(t, ok) + assert.DeepEqual(t, got, desktopCredentials()) + assert.Equal(t, hub.sessionCalls.Load(), int32(2)) +} + +func TestReusesSessionUntilExpiry(t *testing.T) { + expiresAt := testNow.Add(10 * time.Minute) + hub := &fakeHub{session: hubSession("hubuser", expiresAt)} + p := newTestProvider(hub, nil) + + _, ok := p.authConfig("docker.io") + assert.Assert(t, ok) + p.clock.advance(5 * time.Minute) + _, ok = p.authConfig("docker.io") + assert.Assert(t, ok) + assert.Equal(t, hub.sessionCalls.Load(), int32(1)) + + hub.session = hubSession("hubuser", expiresAt.Add(time.Hour)) + hub.session.AccessToken = "refreshed-token" + p.clock.advance(5*time.Minute - sessionExpirySkew) + + got, ok := p.authConfig("docker.io") + assert.Assert(t, ok) + assert.Equal(t, got.Password, "refreshed-token") + assert.Equal(t, hub.sessionCalls.Load(), int32(2)) +} + +func TestRefetchesSessionWithoutExpiry(t *testing.T) { + hub := &fakeHub{session: hubSession("hubuser", time.Time{})} + p := newTestProvider(hub, nil) + + _, ok := p.authConfig("docker.io") + assert.Assert(t, ok) + p.clock.advance(sessionTTL - time.Second) + _, ok = p.authConfig("docker.io") + assert.Assert(t, ok) + assert.Equal(t, hub.sessionCalls.Load(), int32(1)) + + p.clock.advance(time.Second) + _, ok = p.authConfig("docker.io") + assert.Assert(t, ok) + assert.Equal(t, hub.sessionCalls.Load(), int32(2)) +} + +func TestConcurrentLookupsShareOneFetch(t *testing.T) { + hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))} + p := newTestProvider(hub, nil) + + const lookups = 20 + results := make([]types.AuthConfig, lookups) + var wg sync.WaitGroup + for i := range lookups { + wg.Go(func() { + auth, ok := p.authConfig("docker.io") + assert.Check(t, ok) + results[i] = auth + }) + } + wg.Wait() + + for _, auth := range results { + assert.DeepEqual(t, auth, desktopCredentials()) + } + assert.Equal(t, hub.sessionCalls.Load(), int32(1)) + assert.Equal(t, p.connects.Load(), int32(1)) +} + +func TestConnectSkipsMissingSocket(t *testing.T) { + _, err := connect(filepath.Join(t.TempDir(), "engine.sock")) + assert.Assert(t, errors.Is(err, fs.ErrNotExist), err) + assert.Assert(t, isSessionUnavailable(err)) +} + +func TestIsSessionUnavailable(t *testing.T) { + for _, err := range []error{ + connectError(seclient.ReasonNotRunning), + fmt.Errorf("get session: %w", connectError(seclient.ReasonNotRunning)), + connectError(seclient.ReasonPermissionDenied), + connectError(seclient.ReasonTimeout), + connectError(seclient.ReasonUnknown), + dockerhub.ErrNoSession, + dockerhub.ErrNoDefaultProfile, + errSessionExpired, + fs.ErrNotExist, + } { + assert.Assert(t, isSessionUnavailable(err), err) + } + for _, err := range []error{ + seclient.ErrAccessDenied, + errNoUsername, + context.DeadlineExceeded, + } { + assert.Assert(t, !isSessionUnavailable(err), err) + } +} diff --git a/internal/test/cli.go b/internal/test/cli.go index 4a5b0bf9a0fc..151dcadad074 100644 --- a/internal/test/cli.go +++ b/internal/test/cli.go @@ -12,6 +12,7 @@ import ( manifeststore "github.com/docker/cli/cli/manifest/store" "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/registryclient" + "github.com/docker/cli/internal/secretsengine" "github.com/moby/moby/client" ) @@ -35,6 +36,7 @@ type FakeCli struct { // NewFakeCli returns a fake for the command.Cli interface func NewFakeCli(apiClient client.APIClient, opts ...func(*FakeCli)) *FakeCli { + secretsengine.Disable() outBuffer := new(bytes.Buffer) errBuffer := new(bytes.Buffer) c := &FakeCli{ diff --git a/vendor.mod b/vendor.mod index f4263f736120..79c697acae61 100644 --- a/vendor.mod +++ b/vendor.mod @@ -4,7 +4,7 @@ module github.com/docker/cli // There is no 'go.mod' file, as that would imply opting in for all the rules // around SemVer, which this repo cannot abide by as it uses CalVer. -go 1.26.0 +go 1.26.10 tool ( github.com/cpuguy83/go-md2man/v2 // for scripts/docs/generate-man.sh @@ -24,6 +24,8 @@ require ( github.com/docker/docker-credential-helpers v0.9.9 github.com/docker/go-connections v0.8.2 github.com/docker/go-units v0.5.0 + github.com/docker/secrets-engine/client v0.2.2 + github.com/docker/secrets-engine/x v0.8.5 github.com/fvbommel/sortorder v1.2.1 github.com/go-jose/go-jose/v4 v4.1.5 github.com/go-viper/mapstructure/v2 v2.5.0 @@ -70,6 +72,7 @@ require ( ) require ( + connectrpc.com/connect v1.19.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect github.com/Microsoft/go-winio v0.6.3 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/vendor.sum b/vendor.sum index f0f2426bf585..e70b8f903f0f 100644 --- a/vendor.sum +++ b/vendor.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= +connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= @@ -47,6 +49,10 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/secrets-engine/client v0.2.2 h1:aPvmlzsxNA+C+xgp2F+WiiDbCSAXHC591HscFFOvPOE= +github.com/docker/secrets-engine/client v0.2.2/go.mod h1:c3l/N+fA0kiy9S8BE3kKrO5hOZu4+SLYZK9Jx+jc2cw= +github.com/docker/secrets-engine/x v0.8.5 h1:P7D/YESMjpX18S4ratBgoeJkAFIxgsPxe093uFmgiGc= +github.com/docker/secrets-engine/x v0.8.5/go.mod h1:jQ5CXCYCPn8Sw96TS8Ufq3oyIGH2GzUtVXIdwa2Q0VU= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fvbommel/sortorder v1.2.1 h1:ASc85RbthwLEqzuiCk7WQQiopfyGWQiEYZVJ3Qa4A+U= diff --git a/vendor/connectrpc.com/connect/.gitignore b/vendor/connectrpc.com/connect/.gitignore new file mode 100644 index 000000000000..355314397ca6 --- /dev/null +++ b/vendor/connectrpc.com/connect/.gitignore @@ -0,0 +1,5 @@ +/.tmp/ +*.pprof +*.svg +cover.out +connect.test diff --git a/vendor/connectrpc.com/connect/.golangci.yml b/vendor/connectrpc.com/connect/.golangci.yml new file mode 100644 index 000000000000..15eed4409f7e --- /dev/null +++ b/vendor/connectrpc.com/connect/.golangci.yml @@ -0,0 +1,130 @@ +linters-settings: + errcheck: + check-type-assertions: true + exhaustruct: + include: + # No zero values for param structs. + - 'connectrpc\.com/connect\..*[pP]arams' + forbidigo: + forbid: + - '^fmt\.Print' + - '^log\.' + - '^print$' + - '^println$' + - '^panic$' + godox: + # TODO, OPT, etc. comments are fine to commit. Use FIXME comments for + # temporary hacks, and use godox to prevent committing them. + keywords: [FIXME] + importas: + no-unaliased: true + alias: + - pkg: connectrpc.com/connect/internal/gen/connect/ping/v1 + alias: pingv1 + varnamelen: + ignore-decls: + - T any + - i int + - wg sync.WaitGroup +linters: + enable-all: true + disable: + - cyclop # covered by gocyclo + - depguard # unnecessary for small libraries + - funlen # rely on code review to limit function length + - gocognit # dubious "cognitive overhead" quantification + - gofumpt # prefer standard gofmt + - goimports # rely on gci instead + - inamedparam # convention is not followed + - ireturn # "accept interfaces, return structs" isn't ironclad + - lll # don't want hard limits for line length + - maintidx # covered by gocyclo + - mnd # status codes are clearer than constants + - nlreturn # generous whitespace violates house style + - nonamedreturns # named returns are fine; it's *bare* returns that are bad + - protogetter # too many false positives + - tenv # replaced by usetesting + - testpackage # internal tests are fine + - wrapcheck # don't _always_ need to wrap errors + - wsl # generous whitespace violates house style +issues: + exclude-dirs-use-default: false + + exclude: + # Don't ban use of fmt.Errorf to create new errors, but the remaining + # checks from err113 are useful. + - "do not define dynamic errors, use wrapped static errors instead: .*" + + exclude-rules: + # If future reflect.Kinds are nil-able, we'll find out when a test fails. + - linters: [exhaustive] + path: internal/assert/assert.go + # We need our duplex HTTP call to have access to the context. + - linters: [containedctx] + path: duplex_http_call.go + # We need to init a global in-mem HTTP server for testable examples. + - linters: [gochecknoinits, gochecknoglobals] + path: example_init_test.go + # We purposefully do an ineffectual assignment for an example. + - linters: [ineffassign] + path: client_example_test.go + # The generated file is effectively a global receiver. + - linters: [varnamelen] + path: cmd/protoc-gen-connect-go + text: "parameter name 'g' is too short" + # Thorough error logging and timeout config make this example unreadably long. + - linters: [errcheck, gosec] + path: error_writer_example_test.go + # It should be crystal clear that Connect uses plain *http.Clients. + - linters: [revive, stylecheck] + path: client_example_test.go + # Don't complain about timeout management or lack of output assertions in examples. + - linters: [gosec, testableexamples] + path: handler_example_test.go + # No output assertions needed for these examples. + - linters: [testableexamples] + path: error_writer_example_test.go + - linters: [testableexamples] + path: error_not_modified_example_test.go + - linters: [testableexamples] + path: error_example_test.go + # In examples, it's okay to use http.ListenAndServe. + - linters: [gosec] + path: error_not_modified_example_test.go + # There are many instances where we want to keep unused parameters + # as a matter of style or convention, for example when a context.Context + # is the first parameter, we choose to just globally ignore this. + - linters: [revive] + text: "^unused-parameter: " + # We want to return explicit nils in protocol_grpc.go + - linters: [revive] + text: "^if-return: " + path: protocol_grpc.go + # We want to return explicit nils in protocol_connect.go + - linters: [revive] + text: "^if-return: " + path: protocol_connect.go + # We want to return explicit nils in error_writer.go + - linters: [revive] + text: "^if-return: " + path: error_writer.go + # We want to set http.Server's logger + - linters: [forbidigo] + path: internal/memhttp + text: "use of `log.(New|Logger|Lshortfile)` forbidden by pattern .*" + # We want to show examples with http.Get + - linters: [noctx] + path: internal/memhttp/memhttp_test.go + # Allow fmt.Sprintf for cmd/protoc-gen-connect-go for consistency + - linters: [perfsprint] + path: cmd/protoc-gen-connect-go/main.go + # Allow non-canonical headers in tests + - linters: [canonicalheader] + path: '.*_test.go' + # Allow Code pointer receiver for UnmarshalText method + - linters: [recvcheck] + path: code.go + # Avoid false positives for int overflow in tests + - linters: [gosec] + text: "^G115: integer overflow conversion" + path: ".*_test.go" diff --git a/vendor/connectrpc.com/connect/LICENSE b/vendor/connectrpc.com/connect/LICENSE new file mode 100644 index 000000000000..62b825afba5a --- /dev/null +++ b/vendor/connectrpc.com/connect/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021-2025 The Connect Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/connectrpc.com/connect/MAINTAINERS.md b/vendor/connectrpc.com/connect/MAINTAINERS.md new file mode 100644 index 000000000000..0b7495c7047e --- /dev/null +++ b/vendor/connectrpc.com/connect/MAINTAINERS.md @@ -0,0 +1,12 @@ +Maintainers +=========== + +## Current +* [Peter Edge](https://github.com/bufdev), [Buf](https://buf.build) +* [Josh Humphries](https://github.com/jhump), [Buf](https://buf.build) +* [Matt Robenolt](https://github.com/mattrobenolt), [PlanetScale](https://planetscale.com) +* [Edward McFarlane](https://github.com/emcfarlane), [Buf](https://buf.build) + +## Former +* [Akshay Shah](https://github.com/akshayjshah) +* [Alex McKinney](https://github.com/amckinney) diff --git a/vendor/connectrpc.com/connect/Makefile b/vendor/connectrpc.com/connect/Makefile new file mode 100644 index 000000000000..8cba3fedfeba --- /dev/null +++ b/vendor/connectrpc.com/connect/Makefile @@ -0,0 +1,121 @@ +# See https://tech.davis-hansson.com/p/make/ +SHELL := bash +.DELETE_ON_ERROR: +.SHELLFLAGS := -eu -o pipefail -c +.DEFAULT_GOAL := all +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules +MAKEFLAGS += --no-print-directory +BIN := .tmp/bin +export PATH := $(abspath $(BIN)):$(PATH) +export GOBIN := $(abspath $(BIN)) +COPYRIGHT_YEARS := 2021-2025 +LICENSE_IGNORE := --ignore /testdata/ +BUF_VERSION := 1.50.1 + +.PHONY: help +help: ## Describe useful make targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' + +.PHONY: all +all: ## Build, test, and lint (default) + $(MAKE) test + $(MAKE) lint + +.PHONY: clean +clean: ## Delete intermediate build artifacts + @# -X only removes untracked files, -d recurses into directories, -f actually removes files/dirs + git clean -Xdf + +.PHONY: test +test: shorttest slowtest + +.PHONY: shorttest +shorttest: build ## Run unit tests + go test -vet=off -race -cover -short ./... + +.PHONY: slowtest +# Runs all tests, including known long/slow ones. The +# race detector is not used for a few reasons: +# 1. Race coverage of the short tests should be +# adequate to catch race conditions. +# 2. It slows tests down, which is not good if we +# know these are already slow tests. +# 3. Some of the slow tests can't repro issues and +# find regressions as reliably with the race +# detector enabled. +slowtest: build + go test ./... + +.PHONY: runconformance +runconformance: build ## Run conformance test suite + cd internal/conformance && ./runconformance.sh + +.PHONY: bench +bench: BENCH ?= .* +bench: build ## Run benchmarks for root package + go test -vet=off -run '^$$' -bench '$(BENCH)' -benchmem -cpuprofile cpu.pprof -memprofile mem.pprof . + +.PHONY: build +build: generate ## Build all packages + go build ./... + +.PHONY: install +install: ## Install all binaries + go install ./... + +.PHONY: lint +lint: $(BIN)/golangci-lint $(BIN)/buf ## Lint Go and protobuf + go vet ./... + golangci-lint run --modules-download-mode=readonly --timeout=3m0s + buf lint + buf format -d --exit-code + +.PHONY: lintfix +lintfix: $(BIN)/golangci-lint $(BIN)/buf ## Automatically fix some lint errors + golangci-lint run --fix --modules-download-mode=readonly --timeout=3m0s + buf format -w + +.PHONY: generate +generate: $(BIN)/buf $(BIN)/protoc-gen-go $(BIN)/protoc-gen-connect-go $(BIN)/license-header ## Regenerate code and licenses + go mod tidy + cd ./internal/conformance && go mod tidy + buf generate + cd ./cmd/protoc-gen-connect-go/internal && \ + find ./testdata -maxdepth 1 -type d \( ! -name testdata \) | xargs -n 1 -I % bash -c "cd '%' && buf generate" + license-header \ + --license-type apache \ + --copyright-holder "The Connect Authors" \ + --year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE) + +.PHONY: upgrade +upgrade: ## Upgrade dependencies + go get -u -t ./... && go mod tidy -v + +.PHONY: checkgenerate +checkgenerate: + @# Used in CI to verify that `make generate` doesn't produce a diff. + test -z "$$(git status --porcelain | tee /dev/stderr)" + +.PHONY: $(BIN)/protoc-gen-connect-go +$(BIN)/protoc-gen-connect-go: + @mkdir -p $(@D) + go build -o $(@) ./cmd/protoc-gen-connect-go + +$(BIN)/buf: Makefile + @mkdir -p $(@D) + go install github.com/bufbuild/buf/cmd/buf@v${BUF_VERSION} + +$(BIN)/license-header: Makefile + @mkdir -p $(@D) + go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v${BUF_VERSION} + +$(BIN)/golangci-lint: Makefile + @mkdir -p $(@D) + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.7 + +$(BIN)/protoc-gen-go: Makefile go.mod + @mkdir -p $(@D) + @# The version of protoc-gen-go is determined by the version in go.mod + go install google.golang.org/protobuf/cmd/protoc-gen-go + diff --git a/vendor/connectrpc.com/connect/README.md b/vendor/connectrpc.com/connect/README.md new file mode 100644 index 000000000000..57f4ef6329dd --- /dev/null +++ b/vendor/connectrpc.com/connect/README.md @@ -0,0 +1,184 @@ +Connect +======= + +[![Build](https://github.com/connectrpc/connect-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/connectrpc/connect-go/actions/workflows/ci.yaml) +[![Report Card](https://goreportcard.com/badge/connectrpc.com/connect)](https://goreportcard.com/report/connectrpc.com/connect) +[![GoDoc](https://pkg.go.dev/badge/connectrpc.com/connect.svg)](https://pkg.go.dev/connectrpc.com/connect) +[![Slack](https://img.shields.io/badge/slack-buf-%23e01563)][slack] +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8972/badge)](https://www.bestpractices.dev/projects/8972) + +Connect is a slim library for building browser and gRPC-compatible HTTP APIs. +You write a short [Protocol Buffer][protobuf] schema and implement your +application logic, and Connect generates code to handle marshaling, routing, +compression, and content type negotiation. It also generates an idiomatic, +type-safe client. Handlers and clients support three protocols: gRPC, gRPC-Web, +and Connect's own protocol. + +The [Connect protocol][protocol] is a simple protocol that works over HTTP/1.1 +or HTTP/2. It takes the best portions of gRPC and gRPC-Web, including +streaming, and packages them into a protocol that works equally well in +browsers, monoliths, and microservices. Calling a Connect API is as easy as +using `curl`. Try it with our live demo: + +``` +curl \ + --header "Content-Type: application/json" \ + --data '{"sentence": "I feel happy."}' \ + https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say +``` + +Handlers and clients also support the gRPC and gRPC-Web protocols, including +streaming, headers, trailers, and error details. gRPC-compatible [server +reflection][grpcreflect] and [health checks][grpchealth] are available as +standalone packages. Instead of cURL, we could call our API with a gRPC client: + +``` +go install github.com/bufbuild/buf/cmd/buf@latest +buf curl --protocol grpc \ + --data '{"sentence": "I feel happy."}' \ + https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say +``` + +Under the hood, Connect is just [Protocol Buffers][protobuf] and the standard +library: no custom HTTP implementation, no new name resolution or load +balancing APIs, and no surprises. Everything you already know about `net/http` +still applies, and any package that works with an `http.Server`, `http.Client`, +or `http.Handler` also works with Connect. + +For more on Connect, see the [announcement blog post][blog], the documentation +on [connectrpc.com][docs] (especially the [Getting Started] guide for Go), the +[demo service][examples-go], or the [protocol specification][protocol]. + +## A small example + +Curious what all this looks like in practice? From a [Protobuf +schema](internal/proto/connect/ping/v1/ping.proto), we generate [a small RPC +package](internal/gen/simple/connect/ping/v1/pingv1connect/ping.connect.go). Using that +package, we can build a server. This example is available at [internal/example](internal/example): + +```go +package main + +import ( + "context" + "log" + "net/http" + + "connectrpc.com/connect" + pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" + "connectrpc.com/connect/internal/gen/simple/connect/ping/v1/pingv1connect" + "connectrpc.com/validate" +) + +type PingServer struct { + pingv1connect.UnimplementedPingServiceHandler // returns errors from all methods +} + +func (ps *PingServer) Ping(ctx context.Context, req *pingv1.PingRequest) (*pingv1.PingResponse, error) { + return &pingv1.PingResponse{ + Number: req.Number, + }, nil +} + +func main() { + mux := http.NewServeMux() + // The generated constructors return a path and a plain net/http + // handler. + mux.Handle( + pingv1connect.NewPingServiceHandler( + &PingServer{}, + // Validation via Protovalidate is almost always recommended + connect.WithInterceptors(validate.NewInterceptor()), + ), + ) + p := new(http.Protocols) + p.SetHTTP1(true) + // For gRPC clients, it's convenient to support HTTP/2 without TLS. + p.SetUnencryptedHTTP2(true) + s := &http.Server{ + Addr: "localhost:8080", + Handler: mux, + Protocols: p, + } + if err := s.ListenAndServe(); err != nil { + log.Fatalf("listen failed: %v", err) + } +} +``` + +With that server running, you can make requests with any gRPC or Connect +client. To write a client using Connect: + +```go +package main + +import ( + "context" + "log" + "net/http" + + pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" + "connectrpc.com/connect/internal/gen/simple/connect/ping/v1/pingv1connect" +) + +func main() { + client := pingv1connect.NewPingServiceClient( + http.DefaultClient, + "http://localhost:8080/", + ) + req := &pingv1.PingRequest{Number: 42} + res, err := client.Ping(context.Background(), req) + if err != nil { + log.Fatalln(err) + } + log.Println(res) +} +``` + +Of course, `http.ListenAndServe` and `http.DefaultClient` aren't fit for +production use! See Connect's [deployment docs][docs-deployment] for a guide to +configuring timeouts, connection pools, observability, and h2c. + +## Ecosystem + +* [grpchealth]: gRPC-compatible health checks for connect-go +* [grpcreflect]: gRPC-compatible server reflection for connect-go +* [validate]: [Protovalidate][protovalidate] interceptor for connect-go +* [examples-go]: service powering [demo.connectrpc.com](https://demo.connectrpc.com), including bidi streaming +* [connect-es]: Type-safe APIs with Protobuf and TypeScript +* [Buf Studio]: web UI for ad-hoc RPCs +* [conformance]: Connect, gRPC, and gRPC-Web interoperability tests + +## Status: Stable + +This module is stable. It supports: + +* The two most recent major releases of Go (the same versions of Go that continue + to [receive security patches][go-support-policy]). +* [APIv2] of Protocol Buffers in Go (`google.golang.org/protobuf`). + +Within those parameters, `connect` follows semantic versioning. We will +_not_ make breaking changes in the 1.x series of releases. + +## Legal + +Offered under the [Apache 2 license][license]. + +[APIv2]: https://blog.golang.org/protobuf-apiv2 +[Buf Studio]: https://buf.build/studio +[Getting Started]: https://connectrpc.com/docs/go/getting-started +[blog]: https://buf.build/blog/connect-a-better-grpc +[conformance]: https://github.com/connectrpc/conformance +[grpchealth]: https://github.com/connectrpc/grpchealth-go +[grpcreflect]: https://github.com/connectrpc/grpcreflect-go +[connect-es]: https://github.com/connectrpc/connect-es +[examples-go]: https://github.com/connectrpc/examples-go +[docs-deployment]: https://connectrpc.com/docs/go/deployment +[docs]: https://connectrpc.com +[go-support-policy]: https://golang.org/doc/devel/release#policy +[license]: https://github.com/connectrpc/connect-go/blob/main/LICENSE +[protobuf]: https://developers.google.com/protocol-buffers +[protocol]: https://connectrpc.com/docs/protocol +[slack]: https://buf.build/links/slack +[validate]: https://github.com/connectrpc/validate-go +[protovalidate]: https://protovalidate.com diff --git a/vendor/connectrpc.com/connect/RELEASE.md b/vendor/connectrpc.com/connect/RELEASE.md new file mode 100644 index 000000000000..22364cd144fa --- /dev/null +++ b/vendor/connectrpc.com/connect/RELEASE.md @@ -0,0 +1,44 @@ +# Releasing connect-go + +This document outlines how to create a release of connect-go. + +1. Clone the repo, ensuring you have the latest main. + +2. On a new branch, open [connect.go](connect.go) and change the `Version` constant to an appropriate [semantic version](https://semver.org/). To select the correct version, look at the version number of the [latest release] and the changes that are included in this new release. + * If there are only bug fixes and no new features, remove the `-dev` suffix, set MINOR number to be equal to the [latest release], and set the PATCH number to be 1 more than the PATCH number of the [latest release]. + * If there are features being released, remove the `-dev` suffix, set the MINOR number to be 1 more than the MINOR number of the [latest release], and set the PATCH number to `0`. In the common case, the diff here will just be to remove the `-dev` suffix. + + ```patch + -const Version = "1.14.0-dev" + +const Version = "1.14.0" + ``` + +3. Check for any changes in [cmd/protoc-gen-connect-go/main.go](cmd/protoc-gen-connect-go/main.go) that require a version restriction. A constant `IsAtLeastVersionX_Y_Z` should be defined in [connect.go](connect.go) if generated code has begun to use a new API. Make sure the generated code references this constant. If a new constant has been added since the last release, ensure that the name of the constant matches the version being released ([Example PR #496](https://github.com/connectrpc/connect-go/pull/496)). + +4. Open a PR titled "Prepare for vX.Y.Z" ([Example PR #661](https://github.com/connectrpc/connect-go/pull/661)) and a description tagging all current maintainers. Once it's reviewed and CI passes, merge it. + + *Make sure no new commits are merged until the release is complete.* + +5. Review all commits in the new release and for each PR check an appropriate label is used and edit the title to be meaninful to end users. This will help auto-generated release notes match the final notes as closely as possible. + +6. Using the Github UI, create a new release. + - Under “Choose a tag”, type in “vX.Y.Z” to create a new tag for the release upon publish. + - Target the main branch. + - Title the Release “vX.Y.Z”. + - Click “set as latest release”. + - Set the last version as the “Previous tag”. + - Click “Generate release notes” to autogenerate release notes. + - Edit the release notes. A summary and other sub categories may be added if required but should, in most cases, be left as ### Enhancements and ### Bugfixes. Feel free to collect multiple small changes to docs or Github config into one line, but try to tag every contributor. Make especially sure to credit new external contributors! + +7. Publish the release. + +8. On a new branch, open [connect.go](connect.go) and change the `Version` to increment the minor tag and append the `-dev` suffix. Use the next minor release - we never anticipate bugs and patch releases. + + ```patch + -const Version = "1.14.0" + +const Version = "1.15.0-dev" + ``` + +9. Open a PR titled "Back to development" ([Example PR #662](https://github.com/connectrpc/connect-go/pull/662)). Once it's reviewed and CI passes, merge it. + +[latest release]: https://github.com/connectrpc/connect-go/releases/latest diff --git a/vendor/connectrpc.com/connect/SECURITY.md b/vendor/connectrpc.com/connect/SECURITY.md new file mode 100644 index 000000000000..04dcde521005 --- /dev/null +++ b/vendor/connectrpc.com/connect/SECURITY.md @@ -0,0 +1,5 @@ +Security Policy +=============== + +This project follows the [Connect security policy and reporting +process](https://connectrpc.com/docs/governance/security). diff --git a/vendor/connectrpc.com/connect/buf.gen.yaml b/vendor/connectrpc.com/connect/buf.gen.yaml new file mode 100644 index 000000000000..45beb82af63a --- /dev/null +++ b/vendor/connectrpc.com/connect/buf.gen.yaml @@ -0,0 +1,19 @@ +version: v2 +managed: + enabled: true + override: + - file_option: go_package_prefix + value: connectrpc.com/connect/internal/gen +plugins: + - local: protoc-gen-go + out: internal/gen + opt: paths=source_relative + - local: protoc-gen-connect-go + out: internal/gen/generics + opt: paths=source_relative + - local: protoc-gen-connect-go + out: internal/gen/simple + opt: + - paths=source_relative + - simple +clean: true diff --git a/vendor/connectrpc.com/connect/buf.yaml b/vendor/connectrpc.com/connect/buf.yaml new file mode 100644 index 000000000000..0ab5a094491b --- /dev/null +++ b/vendor/connectrpc.com/connect/buf.yaml @@ -0,0 +1,14 @@ +version: v2 +modules: + - path: internal/proto +lint: + use: + - STANDARD + ignore: + - internal/proto/connectext/grpc/health/v1/health.proto + - internal/proto/connectext/grpc/reflection/v1alpha/reflection.proto + - internal/proto/connectext/grpc/status/v1/status.proto + disallow_comment_ignores: true +breaking: + use: + - WIRE_JSON diff --git a/vendor/connectrpc.com/connect/buffer_pool.go b/vendor/connectrpc.com/connect/buffer_pool.go new file mode 100644 index 000000000000..8ef9b1dfd1df --- /dev/null +++ b/vendor/connectrpc.com/connect/buffer_pool.go @@ -0,0 +1,54 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "bytes" + "sync" +) + +const ( + initialBufferSize = 512 + maxRecycleBufferSize = 8 * 1024 * 1024 // if >8MiB, don't hold onto a buffer +) + +type bufferPool struct { + sync.Pool +} + +func newBufferPool() *bufferPool { + return &bufferPool{ + Pool: sync.Pool{ + New: func() any { + return bytes.NewBuffer(make([]byte, 0, initialBufferSize)) + }, + }, + } +} + +func (b *bufferPool) Get() *bytes.Buffer { + if buf, ok := b.Pool.Get().(*bytes.Buffer); ok { + return buf + } + return bytes.NewBuffer(make([]byte, 0, initialBufferSize)) +} + +func (b *bufferPool) Put(buffer *bytes.Buffer) { + if buffer.Cap() > maxRecycleBufferSize { + return + } + buffer.Reset() + b.Pool.Put(buffer) +} diff --git a/vendor/connectrpc.com/connect/client.go b/vendor/connectrpc.com/connect/client.go new file mode 100644 index 000000000000..9cd34ef1f34a --- /dev/null +++ b/vendor/connectrpc.com/connect/client.go @@ -0,0 +1,392 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// Client is a reusable, concurrency-safe client for a single procedure. +// Depending on the procedure's type, use the CallUnary, CallClientStream, +// CallServerStream, or CallBidiStream method. +// +// By default, clients use the Connect protocol with the binary Protobuf Codec, +// ask for gzipped responses, and send uncompressed requests. To use the gRPC +// or gRPC-Web protocols, use the [WithGRPC] or [WithGRPCWeb] options. +type Client[Req, Res any] struct { + config *clientConfig + callUnary func(context.Context, *Request[Req]) (*Response[Res], error) + protocolClient protocolClient + err error +} + +// NewClient constructs a new Client. +func NewClient[Req, Res any](httpClient HTTPClient, url string, options ...ClientOption) *Client[Req, Res] { + client := &Client[Req, Res]{} + config, err := newClientConfig(url, options) + if err != nil { + client.err = err + return client + } + client.config = config + protocolClient, protocolErr := client.config.Protocol.NewClient( + &protocolClientParams{ + CompressionName: config.RequestCompressionName, + CompressionPools: newReadOnlyCompressionPools( + config.CompressionPools, + config.CompressionNames, + ), + Codec: config.Codec, + Protobuf: config.protobuf(), + CompressMinBytes: config.CompressMinBytes, + HTTPClient: httpClient, + URL: config.URL, + BufferPool: config.BufferPool, + ReadMaxBytes: config.ReadMaxBytes, + SendMaxBytes: config.SendMaxBytes, + EnableGet: config.EnableGet, + GetURLMaxBytes: config.GetURLMaxBytes, + GetUseFallback: config.GetUseFallback, + }, + ) + if protocolErr != nil { + client.err = protocolErr + return client + } + client.protocolClient = protocolClient + // Rather than applying unary interceptors along the hot path, we can do it + // once at client creation. + unarySpec := config.newSpec(StreamTypeUnary) + unaryFunc := UnaryFunc(func(ctx context.Context, request AnyRequest) (AnyResponse, error) { + conn := client.protocolClient.NewConn(ctx, unarySpec, request.Header()) + conn.onRequestSend(func(r *http.Request) { + request.setRequestMethod(r.Method) + callInfo, ok := clientCallInfoForContext(ctx) + if ok { + callInfo.method = r.Method + callInfo.responseSource = conn + } + }) + // Send always returns an io.EOF unless the error is from the client-side. + // We want the user to continue to call Receive in those cases to get the + // full error from the server-side. + if err := conn.Send(request.Any()); err != nil && !errors.Is(err, io.EOF) { + _ = conn.CloseRequest() + _ = conn.CloseResponse() + return nil, err + } + if err := conn.CloseRequest(); err != nil { + _ = conn.CloseResponse() + return nil, err + } + response, err := receiveUnaryResponse[Res](conn, config.Initializer) + if err != nil { + _ = conn.CloseResponse() + return nil, err + } + return response, conn.CloseResponse() + }) + if interceptor := config.Interceptor; interceptor != nil { + // interceptor is the full chain of all interceptors provided + unaryFunc = interceptor.WrapUnary(unaryFunc) + } + client.callUnary = func(ctx context.Context, request *Request[Req]) (*Response[Res], error) { + // To make the specification, peer, and RPC headers visible to the full + // interceptor chain (as though they were supplied by the caller), we'll + // add them here. + request.spec = unarySpec + request.peer = client.protocolClient.Peer() + protocolClient.WriteRequestHeader(StreamTypeUnary, request.Header()) + + // Also set them in the context if there's a call info present + callInfo, callInfoOk := clientCallInfoForContext(ctx) + if callInfoOk { + callInfo.peer = request.Peer() + callInfo.spec = request.Spec() + // A client could have set request headers in the call info OR the request wrapper + // So if a callInfo exists in context, merge any headers from there into the request wrapper + // so that all headers are sent in the request + mergeHeaders(request.Header(), callInfo.requestHeader) + + // Copy the call info into a sentinel value. This is so we can compare + // the sentinel value against the call info in context. If they're different, + // we can stop the request. This protects against changing the context in interceptors. + ctx = context.WithValue(ctx, sentinelContextKey{}, callInfo) + } + + response, err := unaryFunc(ctx, request) + if err != nil { + return nil, err + } + typed, ok := response.(*Response[Res]) + if !ok { + return nil, errorf(CodeInternal, "unexpected client response type %T", response) + } + return typed, nil + } + return client +} + +// CallUnary calls a request-response procedure. +func (c *Client[Req, Res]) CallUnary(ctx context.Context, request *Request[Req]) (*Response[Res], error) { + if c.err != nil { + return nil, c.err + } + return c.callUnary(ctx, request) +} + +// CallClientStream calls a client streaming procedure. +// +// Request headers can be sent via the [ClientStreamForClient.RequestHeader] method on the stream. Note that the +// request headers are not sent automatically when this method is invoked and instead require an explicit call to +// [ClientStreamForClient.Send]. +func (c *Client[Req, Res]) CallClientStream(ctx context.Context) *ClientStreamForClient[Req, Res] { + if c.err != nil { + return &ClientStreamForClient[Req, Res]{err: c.err} + } + return &ClientStreamForClient[Req, Res]{ + conn: c.newConn(ctx, StreamTypeClient, nil), + initializer: c.config.Initializer, + } +} + +// CallClientStreamSimple calls a client streaming procedure. +// +// Request headers should be set in a [CallInfo] object inside the context using [NewClientContext]. These headers are +// transmitted when this method is called and do not require an explicit call to [ClientStreamForClientSimple.Send]. +// +// In addition, when calling [ClientStreamForClientSimple.CloseAndReceive] on the returned stream, the returned response +// is the response type defined for the stream and _not_ a Connect [Response] wrapper type. As a result, any response +// headers and trailers should be read from the [CallInfo] object in context. +func (c *Client[Req, Res]) CallClientStreamSimple(ctx context.Context) (*ClientStreamForClientSimple[Req, Res], error) { + if c.err != nil { + return &ClientStreamForClientSimple[Req, Res]{ + stream: &ClientStreamForClient[Req, Res]{err: c.err}, + }, c.err + } + + stream := &ClientStreamForClientSimple[Req, Res]{ + stream: &ClientStreamForClient[Req, Res]{ + conn: c.newConn(ctx, StreamTypeClient, nil), + initializer: c.config.Initializer, + }, + } + if err := stream.Send(nil); err != nil { + return nil, err + } + return stream, nil +} + +// CallServerStream calls a server streaming procedure. +func (c *Client[Req, Res]) CallServerStream(ctx context.Context, request *Request[Req]) (*ServerStreamForClient[Res], error) { + if c.err != nil { + return nil, c.err + } + conn := c.newConn(ctx, StreamTypeServer, func(r *http.Request) { + request.method = r.Method + }) + request.peer = conn.Peer() + request.spec = conn.Spec() + + mergeHeaders(conn.RequestHeader(), request.header) + + // Send always returns an io.EOF unless the error is from the client-side. + // We want the user to continue to call Receive in those cases to get the + // full error from the server-side. + if err := conn.Send(request.Msg); err != nil && !errors.Is(err, io.EOF) { + _ = conn.CloseRequest() + _ = conn.CloseResponse() + return nil, err + } + if err := conn.CloseRequest(); err != nil { + return nil, err + } + return &ServerStreamForClient[Res]{ + conn: conn, + initializer: c.config.Initializer, + }, nil +} + +// CallBidiStream calls a bidirectional streaming procedure. +// +// Request headers can be sent via the [BidiStreamForClient.RequestHeader] method. Note that the +// request headers are not sent automatically when this method is invoked and instead require an explicit call to +// [BidiStreamForClient.Send]. +func (c *Client[Req, Res]) CallBidiStream(ctx context.Context) *BidiStreamForClient[Req, Res] { + if c.err != nil { + return &BidiStreamForClient[Req, Res]{err: c.err} + } + return &BidiStreamForClient[Req, Res]{ + conn: c.newConn(ctx, StreamTypeBidi, nil), + initializer: c.config.Initializer, + } +} + +// CallBidiStreamSimple calls a bidirectional streaming procedure. +// +// Request headers should be set in a [CallInfo] object inside the context using [NewClientContext]. These headers +// are transmitted when this method is called and do not require an explicit call to [BidiStreamForClient.Send]. +// +// Likewise, response headers and trailers should be read from the [CallInfo] object in context. +func (c *Client[Req, Res]) CallBidiStreamSimple(ctx context.Context) (*BidiStreamForClientSimple[Req, Res], error) { + if c.err != nil { + return &BidiStreamForClientSimple[Req, Res]{ + stream: &BidiStreamForClient[Req, Res]{err: c.err}, + }, c.err + } + + stream := &BidiStreamForClientSimple[Req, Res]{ + stream: &BidiStreamForClient[Req, Res]{ + conn: c.newConn(ctx, StreamTypeBidi, nil), + initializer: c.config.Initializer, + }, + } + + if err := stream.Send(nil); err != nil { + return nil, err + } + return stream, nil +} + +func (c *Client[Req, Res]) newConn(ctx context.Context, streamType StreamType, onRequestSend func(r *http.Request)) StreamingClientConn { + callInfo, callInfoOk := clientCallInfoForContext(ctx) + // Set values in the context if there's a call info present + if callInfoOk { + // Copy the call info into a sentinel value. This is so we can compare + // the sentinel value against the call info in context. If they're different, + // we can stop the request. This protects against changing the context in interceptors. + ctx = context.WithValue(ctx, sentinelContextKey{}, callInfo) + } + newConn := func(ctx context.Context, spec Spec) StreamingClientConn { + header := make(http.Header, 8) // arbitrary power of two, prevent immediate resizing + c.protocolClient.WriteRequestHeader(streamType, header) + conn := c.protocolClient.NewConn(ctx, spec, header) + conn.onRequestSend(onRequestSend) + return conn + } + if interceptor := c.config.Interceptor; interceptor != nil { + newConn = interceptor.WrapStreamingClient(newConn) + } + conn := newConn(ctx, c.config.newSpec(streamType)) + + // Set values in the context if there's a call info present + if callInfoOk { + callInfo.peer = conn.Peer() + callInfo.spec = conn.Spec() + callInfo.responseSource = conn + + // Merge any callInfo request headers first, then do the request, + // so that context headers show first in the list of headers. + mergeHeaders(conn.RequestHeader(), callInfo.RequestHeader()) + } + + return conn +} + +type clientConfig struct { + URL *url.URL + Protocol protocol + Procedure string + Schema any + Initializer maybeInitializer + CompressMinBytes int + Interceptor Interceptor + CompressionPools map[string]*compressionPool + CompressionNames []string + Codec Codec + RequestCompressionName string + BufferPool *bufferPool + ReadMaxBytes int + SendMaxBytes int + EnableGet bool + GetURLMaxBytes int + GetUseFallback bool + IdempotencyLevel IdempotencyLevel +} + +func newClientConfig(rawURL string, options []ClientOption) (*clientConfig, *Error) { + url, err := parseRequestURL(rawURL) + if err != nil { + return nil, err + } + protoPath := extractProtoPath(url.Path) + config := clientConfig{ + URL: url, + Protocol: &protocolConnect{}, + Procedure: protoPath, + CompressionPools: make(map[string]*compressionPool), + BufferPool: newBufferPool(), + } + withProtoBinaryCodec().applyToClient(&config) + withGzip().applyToClient(&config) + for _, opt := range options { + opt.applyToClient(&config) + } + if err := config.validate(); err != nil { + return nil, err + } + return &config, nil +} + +func (c *clientConfig) validate() *Error { + if c.Codec == nil || c.Codec.Name() == "" { + return errorf(CodeUnknown, "no codec configured") + } + if c.RequestCompressionName != "" && c.RequestCompressionName != compressionIdentity { + if _, ok := c.CompressionPools[c.RequestCompressionName]; !ok { + return errorf(CodeUnknown, "unknown compression %q", c.RequestCompressionName) + } + } + return nil +} + +func (c *clientConfig) protobuf() Codec { + if c.Codec.Name() == codecNameProto { + return c.Codec + } + return &protoBinaryCodec{} +} + +func (c *clientConfig) newSpec(t StreamType) Spec { + return Spec{ + StreamType: t, + Procedure: c.Procedure, + Schema: c.Schema, + IsClient: true, + IdempotencyLevel: c.IdempotencyLevel, + } +} + +func parseRequestURL(rawURL string) (*url.URL, *Error) { + url, err := url.ParseRequestURI(rawURL) + if err == nil { + return url, nil + } + if !strings.Contains(rawURL, "://") { + // URL doesn't have a scheme, so the user is likely accustomed to + // grpc-go's APIs. + err = fmt.Errorf( + "URL %q missing scheme: use http:// or https:// (unlike grpc-go)", + rawURL, + ) + } + return nil, NewError(CodeUnavailable, err) +} diff --git a/vendor/connectrpc.com/connect/client_stream.go b/vendor/connectrpc.com/connect/client_stream.go new file mode 100644 index 000000000000..5a24f4a3d020 --- /dev/null +++ b/vendor/connectrpc.com/connect/client_stream.go @@ -0,0 +1,441 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "errors" + "io" + "net/http" +) + +var ( + // errNoStreamInitialized signals that a no stream has been initialized when + // attempting to access stream-related methods. + errNoStreamInitialized = errors.New("no stream initialized") +) + +// ClientStreamForClient is the client's view of a client streaming RPC. +// +// It's returned from [Client].CallClientStream, but doesn't currently have an +// exported constructor function. +// +// When using this stream, request headers should be set via the [ClientStreamForClient.RequestHeader] method. +type ClientStreamForClient[Req, Res any] struct { + conn StreamingClientConn + initializer maybeInitializer + // Error from client construction. If non-nil, return for all calls. + err error +} + +// Spec returns the specification for the RPC. +func (c *ClientStreamForClient[_, _]) Spec() Spec { + return c.conn.Spec() +} + +// Peer describes the server for the RPC. +func (c *ClientStreamForClient[_, _]) Peer() Peer { + return c.conn.Peer() +} + +// RequestHeader returns the request headers. Headers are sent to the server with the +// first call to Send. +// +// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. +func (c *ClientStreamForClient[Req, Res]) RequestHeader() http.Header { + if c.err != nil { + return http.Header{} + } + return c.conn.RequestHeader() +} + +// Send a message to the server. The first call to Send also sends the request +// headers. +// +// If the server returns an error, Send returns an error that wraps [io.EOF]. +// Clients should check for case using the standard library's [errors.Is] and +// unmarshal the error using CloseAndReceive. +func (c *ClientStreamForClient[Req, Res]) Send(request *Req) error { + if c.err != nil { + return c.err + } + if request == nil { + return c.conn.Send(nil) + } + return c.conn.Send(request) +} + +// CloseAndReceive closes the send side of the stream and waits for the +// response. +func (c *ClientStreamForClient[Req, Res]) CloseAndReceive() (*Response[Res], error) { + if c.err != nil { + return nil, c.err + } + if err := c.conn.CloseRequest(); err != nil { + _ = c.conn.CloseResponse() + return nil, err + } + response, err := receiveUnaryResponse[Res](c.conn, c.initializer) + if err != nil { + _ = c.conn.CloseResponse() + return nil, err + } + return response, c.conn.CloseResponse() +} + +// Conn exposes the underlying StreamingClientConn. This may be useful if +// you'd prefer to wrap the connection in a different high-level API. +func (c *ClientStreamForClient[Req, Res]) Conn() (StreamingClientConn, error) { + return c.conn, c.err +} + +// ClientStreamForClientSimple is the client's view of a client streaming RPC. +// +// It's returned from [Client.CallClientStreamSimple], but doesn't currently have an +// exported constructor function. +// +// Usage of this stream requires that request headers be set in a [CallInfo] object in context via [NewClientContext]. +// In addition, the response returned by [ClientStreamForClientSimple.CloseAndReceive] is the response type defined for +// the stream and _not_ a Connect [Response] wrapper type. As a result, response headers/trailers should be read from +// the [CallInfo] object in context. +type ClientStreamForClientSimple[Req, Res any] struct { + stream *ClientStreamForClient[Req, Res] +} + +// Spec returns the specification for the RPC. +func (c *ClientStreamForClientSimple[_, _]) Spec() Spec { + if c.stream == nil { + return Spec{} + } + return c.stream.Spec() +} + +// Peer describes the server for the RPC. +func (c *ClientStreamForClientSimple[_, _]) Peer() Peer { + if c.stream == nil { + return Peer{} + } + return c.stream.Peer() +} + +// Send a message to the server. The first call to Send also sends the request +// headers. +// +// If the server returns an error, Send returns an error that wraps [io.EOF]. +// Clients should check for case using the standard library's [errors.Is] and +// unmarshal the error using CloseAndReceive. +func (c *ClientStreamForClientSimple[Req, Res]) Send(request *Req) error { + if c.stream == nil { + return errNoStreamInitialized + } + return c.stream.Send(request) +} + +// CloseAndReceive closes the send side of the stream and waits for the +// response. +func (c *ClientStreamForClientSimple[Req, Res]) CloseAndReceive() (*Res, error) { + if c.stream == nil { + return nil, errNoStreamInitialized + } + res, err := c.stream.CloseAndReceive() + if err != nil { + return nil, err + } + return res.Msg, nil +} + +// ServerStreamForClient is the client's view of a server streaming RPC. +// +// It's returned from [Client].CallServerStream, but doesn't currently have an +// exported constructor function. +type ServerStreamForClient[Res any] struct { + conn StreamingClientConn + initializer maybeInitializer + msg *Res + // Error from client construction. If non-nil, return for all calls. + constructErr error + // Error from conn.Receive(). + receiveErr error +} + +// Receive advances the stream to the next message, which will then be +// available through the Msg method. It returns false when the stream stops, +// either by reaching the end or by encountering an unexpected error. After +// Receive returns false, the Err method will return any unexpected error +// encountered. +func (s *ServerStreamForClient[Res]) Receive() bool { + if s.constructErr != nil || s.receiveErr != nil { + return false + } + s.msg = new(Res) + if err := s.initializer.maybe(s.conn.Spec(), s.msg); err != nil { + s.receiveErr = err + return false + } + s.receiveErr = s.conn.Receive(s.msg) + return s.receiveErr == nil +} + +// Msg returns the most recent message unmarshaled by a call to Receive. +func (s *ServerStreamForClient[Res]) Msg() *Res { + if s.msg == nil { + s.msg = new(Res) + } + return s.msg +} + +// Err returns the first non-EOF error that was encountered by Receive. +func (s *ServerStreamForClient[Res]) Err() error { + if s.constructErr != nil { + return s.constructErr + } + if s.receiveErr != nil && !errors.Is(s.receiveErr, io.EOF) { + return s.receiveErr + } + return nil +} + +// ResponseHeader returns the headers received from the server. It blocks until +// the first call to Receive returns. +func (s *ServerStreamForClient[Res]) ResponseHeader() http.Header { + if s.constructErr != nil { + return http.Header{} + } + return s.conn.ResponseHeader() +} + +// ResponseTrailer returns the trailers received from the server. Trailers +// aren't fully populated until Receive() returns an error wrapping io.EOF. +func (s *ServerStreamForClient[Res]) ResponseTrailer() http.Header { + if s.constructErr != nil { + return http.Header{} + } + return s.conn.ResponseTrailer() +} + +// Close the receive side of the stream. +// +// Close is non-blocking. To gracefully close the stream and allow for +// connection resuse ensure all messages have been received before calling +// Close. All messages are received when Receive returns false. +func (s *ServerStreamForClient[Res]) Close() error { + if s.constructErr != nil { + return s.constructErr + } + return s.conn.CloseResponse() +} + +// Conn exposes the underlying StreamingClientConn. This may be useful if +// you'd prefer to wrap the connection in a different high-level API. +func (s *ServerStreamForClient[Res]) Conn() (StreamingClientConn, error) { + return s.conn, s.constructErr +} + +// BidiStreamForClient is the client's view of a bidirectional streaming RPC. +// +// It's returned from [Client].CallBidiStream, but doesn't currently have an +// exported constructor function. +type BidiStreamForClient[Req, Res any] struct { + conn StreamingClientConn + initializer maybeInitializer + // Error from client construction. If non-nil, return for all calls. + err error +} + +// Spec returns the specification for the RPC. +func (b *BidiStreamForClient[_, _]) Spec() Spec { + return b.conn.Spec() +} + +// Peer describes the server for the RPC. +func (b *BidiStreamForClient[_, _]) Peer() Peer { + return b.conn.Peer() +} + +// RequestHeader returns the request headers. Headers are sent with the first +// call to Send. +// +// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. +func (b *BidiStreamForClient[Req, Res]) RequestHeader() http.Header { + if b.err != nil { + return http.Header{} + } + return b.conn.RequestHeader() +} + +// Send a message to the server. The first call to Send also sends the request +// headers. To send just the request headers, without a body, call Send with a +// nil pointer. +// +// If the server returns an error, Send returns an error that wraps [io.EOF]. +// Clients should check for EOF using the standard library's [errors.Is] and +// call Receive to retrieve the error. +func (b *BidiStreamForClient[Req, Res]) Send(msg *Req) error { + if b.err != nil { + return b.err + } + if msg == nil { + return b.conn.Send(nil) + } + return b.conn.Send(msg) +} + +// CloseRequest closes the send side of the stream. +func (b *BidiStreamForClient[Req, Res]) CloseRequest() error { + if b.err != nil { + return b.err + } + return b.conn.CloseRequest() +} + +// Receive a message. When the server is done sending messages and no other +// errors have occurred, Receive will return an error that wraps [io.EOF]. +func (b *BidiStreamForClient[Req, Res]) Receive() (*Res, error) { + if b.err != nil { + return nil, b.err + } + var msg Res + if err := b.initializer.maybe(b.conn.Spec(), &msg); err != nil { + return nil, err + } + if err := b.conn.Receive(&msg); err != nil { + return nil, err + } + return &msg, nil +} + +// CloseResponse closes the receive side of the stream. +// +// CloseResponse is non-blocking. To gracefully close the stream and allow for +// connection resuse ensure all messages have been received before calling +// CloseResponse. All messages are received when Receive returns an error +// wrapping [io.EOF]. +func (b *BidiStreamForClient[Req, Res]) CloseResponse() error { + if b.err != nil { + return b.err + } + return b.conn.CloseResponse() +} + +// ResponseHeader returns the headers received from the server. It blocks until +// the first call to Receive returns. +func (b *BidiStreamForClient[Req, Res]) ResponseHeader() http.Header { + if b.err != nil { + return http.Header{} + } + return b.conn.ResponseHeader() +} + +// ResponseTrailer returns the trailers received from the server. Trailers +// aren't fully populated until Receive() returns an error wrapping [io.EOF]. +func (b *BidiStreamForClient[Req, Res]) ResponseTrailer() http.Header { + if b.err != nil { + return http.Header{} + } + return b.conn.ResponseTrailer() +} + +// Conn exposes the underlying StreamingClientConn. This may be useful if +// you'd prefer to wrap the connection in a different high-level API. +func (b *BidiStreamForClient[Req, Res]) Conn() (StreamingClientConn, error) { + return b.conn, b.err +} + +// BidiStreamForClientSimple is the client's view of a bidirectional streaming RPC. +// +// It's returned from [Client].CallBidiStream, but doesn't currently have an +// exported constructor function. +type BidiStreamForClientSimple[Req, Res any] struct { + stream *BidiStreamForClient[Req, Res] +} + +// Spec returns the specification for the RPC. +func (b *BidiStreamForClientSimple[_, _]) Spec() Spec { + if b.stream == nil { + return Spec{} + } + return b.stream.Spec() +} + +// Peer describes the server for the RPC. +func (b *BidiStreamForClientSimple[_, _]) Peer() Peer { + if b.stream == nil { + return Peer{} + } + return b.stream.Peer() +} + +// Send a message to the server. The first call to Send also sends the request +// headers. To send just the request headers, without a body, call Send with a +// nil pointer. +// +// If the server returns an error, Send returns an error that wraps [io.EOF]. +// Clients should check for EOF using the standard library's [errors.Is] and +// call Receive to retrieve the error. +func (b *BidiStreamForClientSimple[Req, Res]) Send(msg *Req) error { + if b.stream == nil { + return errNoStreamInitialized + } + return b.stream.Send(msg) +} + +// CloseRequest closes the send side of the stream. +func (b *BidiStreamForClientSimple[Req, Res]) CloseRequest() error { + if b.stream == nil { + return errNoStreamInitialized + } + return b.stream.CloseRequest() +} + +// Receive a message. When the server is done sending messages and no other +// errors have occurred, Receive will return an error that wraps [io.EOF]. +func (b *BidiStreamForClientSimple[Req, Res]) Receive() (*Res, error) { + if b.stream == nil { + return nil, errNoStreamInitialized + } + return b.stream.Receive() +} + +// CloseResponse closes the receive side of the stream. +// +// CloseResponse is non-blocking. To gracefully close the stream and allow for +// connection resuse ensure all messages have been received before calling +// CloseResponse. All messages are received when Receive returns an error +// wrapping [io.EOF]. +func (b *BidiStreamForClientSimple[Req, Res]) CloseResponse() error { + if b.stream == nil { + return errNoStreamInitialized + } + return b.stream.CloseResponse() +} + +// ResponseHeader returns the headers received from the server. It blocks until +// the first call to Receive returns. +func (b *BidiStreamForClientSimple[Req, Res]) ResponseHeader() http.Header { + if b.stream == nil { + return make(http.Header) + } + return b.stream.ResponseHeader() +} + +// ResponseTrailer returns the trailers received from the server. Trailers +// aren't fully populated until Receive() returns an error wrapping [io.EOF]. +func (b *BidiStreamForClientSimple[Req, Res]) ResponseTrailer() http.Header { + if b.stream == nil { + return make(http.Header) + } + return b.stream.ResponseTrailer() +} diff --git a/vendor/connectrpc.com/connect/code.go b/vendor/connectrpc.com/connect/code.go new file mode 100644 index 000000000000..3577c072a5a4 --- /dev/null +++ b/vendor/connectrpc.com/connect/code.go @@ -0,0 +1,226 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "fmt" + "strconv" + "strings" +) + +// A Code is one of the Connect protocol's error codes. There are no user-defined +// codes, so only the codes enumerated below are valid. In both name and +// semantics, these codes match the gRPC status codes. +// +// The descriptions below are optimized for brevity rather than completeness. +// See the [Connect protocol specification] for detailed descriptions of each +// code and example usage. +// +// [Connect protocol specification]: https://connectrpc.com/docs/protocol +type Code uint32 + +const ( + // The zero code in gRPC is OK, which indicates that the operation was a + // success. We don't define a constant for it because it overlaps awkwardly + // with Go's error semantics: what does it mean to have a non-nil error with + // an OK status? (Also, the Connect protocol doesn't use a code for + // successes.) + + // CodeCanceled indicates that the operation was canceled, typically by the + // caller. + CodeCanceled Code = 1 + + // CodeUnknown indicates that the operation failed for an unknown reason. + CodeUnknown Code = 2 + + // CodeInvalidArgument indicates that client supplied an invalid argument. + CodeInvalidArgument Code = 3 + + // CodeDeadlineExceeded indicates that deadline expired before the operation + // could complete. + CodeDeadlineExceeded Code = 4 + + // CodeNotFound indicates that some requested entity (for example, a file or + // directory) was not found. + CodeNotFound Code = 5 + + // CodeAlreadyExists indicates that client attempted to create an entity (for + // example, a file or directory) that already exists. + CodeAlreadyExists Code = 6 + + // CodePermissionDenied indicates that the caller doesn't have permission to + // execute the specified operation. + CodePermissionDenied Code = 7 + + // CodeResourceExhausted indicates that some resource has been exhausted. For + // example, a per-user quota may be exhausted or the entire file system may + // be full. + CodeResourceExhausted Code = 8 + + // CodeFailedPrecondition indicates that the system is not in a state + // required for the operation's execution. + CodeFailedPrecondition Code = 9 + + // CodeAborted indicates that operation was aborted by the system, usually + // because of a concurrency issue such as a sequencer check failure or + // transaction abort. + CodeAborted Code = 10 + + // CodeOutOfRange indicates that the operation was attempted past the valid + // range (for example, seeking past end-of-file). + CodeOutOfRange Code = 11 + + // CodeUnimplemented indicates that the operation isn't implemented, + // supported, or enabled in this service. + CodeUnimplemented Code = 12 + + // CodeInternal indicates that some invariants expected by the underlying + // system have been broken. This code is reserved for serious errors. + CodeInternal Code = 13 + + // CodeUnavailable indicates that the service is currently unavailable. This + // is usually temporary, so clients can back off and retry idempotent + // operations. + CodeUnavailable Code = 14 + + // CodeDataLoss indicates that the operation has resulted in unrecoverable + // data loss or corruption. + CodeDataLoss Code = 15 + + // CodeUnauthenticated indicates that the request does not have valid + // authentication credentials for the operation. + CodeUnauthenticated Code = 16 + + minCode = CodeCanceled + maxCode = CodeUnauthenticated +) + +func (c Code) String() string { + switch c { + case CodeCanceled: + return "canceled" + case CodeUnknown: + return "unknown" + case CodeInvalidArgument: + return "invalid_argument" + case CodeDeadlineExceeded: + return "deadline_exceeded" + case CodeNotFound: + return "not_found" + case CodeAlreadyExists: + return "already_exists" + case CodePermissionDenied: + return "permission_denied" + case CodeResourceExhausted: + return "resource_exhausted" + case CodeFailedPrecondition: + return "failed_precondition" + case CodeAborted: + return "aborted" + case CodeOutOfRange: + return "out_of_range" + case CodeUnimplemented: + return "unimplemented" + case CodeInternal: + return "internal" + case CodeUnavailable: + return "unavailable" + case CodeDataLoss: + return "data_loss" + case CodeUnauthenticated: + return "unauthenticated" + } + return fmt.Sprintf("code_%d", c) +} + +// MarshalText implements [encoding.TextMarshaler]. +func (c Code) MarshalText() ([]byte, error) { + return []byte(c.String()), nil +} + +// UnmarshalText implements [encoding.TextUnmarshaler]. +func (c *Code) UnmarshalText(data []byte) error { + dataStr := string(data) + switch dataStr { + case "canceled": + *c = CodeCanceled + return nil + case "unknown": + *c = CodeUnknown + return nil + case "invalid_argument": + *c = CodeInvalidArgument + return nil + case "deadline_exceeded": + *c = CodeDeadlineExceeded + return nil + case "not_found": + *c = CodeNotFound + return nil + case "already_exists": + *c = CodeAlreadyExists + return nil + case "permission_denied": + *c = CodePermissionDenied + return nil + case "resource_exhausted": + *c = CodeResourceExhausted + return nil + case "failed_precondition": + *c = CodeFailedPrecondition + return nil + case "aborted": + *c = CodeAborted + return nil + case "out_of_range": + *c = CodeOutOfRange + return nil + case "unimplemented": + *c = CodeUnimplemented + return nil + case "internal": + *c = CodeInternal + return nil + case "unavailable": + *c = CodeUnavailable + return nil + case "data_loss": + *c = CodeDataLoss + return nil + case "unauthenticated": + *c = CodeUnauthenticated + return nil + } + // Ensure that non-canonical codes round-trip through MarshalText and + // UnmarshalText. + if after, ok := strings.CutPrefix(dataStr, "code_"); ok { + dataStr = after + code, err := strconv.ParseUint(dataStr, 10 /* base */, 32 /* bitsize */) + if err == nil && (code < uint64(minCode) || code > uint64(maxCode)) { + *c = Code(code) + return nil + } + } + return fmt.Errorf("invalid code %q", dataStr) +} + +// CodeOf returns the error's status code if it is or wraps an [*Error] and +// [CodeUnknown] otherwise. +func CodeOf(err error) Code { + if connectErr, ok := asError(err); ok { + return connectErr.Code() + } + return CodeUnknown +} diff --git a/vendor/connectrpc.com/connect/codec.go b/vendor/connectrpc.com/connect/codec.go new file mode 100644 index 000000000000..729a28ab6cc1 --- /dev/null +++ b/vendor/connectrpc.com/connect/codec.go @@ -0,0 +1,259 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/runtime/protoiface" +) + +const ( + codecNameProto = "proto" + codecNameJSON = "json" + codecNameJSONCharsetUTF8 = codecNameJSON + "; charset=utf-8" +) + +// Codec marshals structs (typically generated from a schema) to and from bytes. +type Codec interface { + // Name returns the name of the Codec. + // + // This may be used as part of the Content-Type within HTTP. For example, + // with gRPC this is the content subtype, so "application/grpc+proto" will + // map to the Codec with name "proto". + // + // Names must not be empty. + Name() string + // Marshal marshals the given message. + // + // Marshal may expect a specific type of message, and will error if this type + // is not given. + Marshal(any) ([]byte, error) + // Unmarshal unmarshals the given message. + // + // Unmarshal may expect a specific type of message, and will error if this + // type is not given. + Unmarshal([]byte, any) error +} + +// marshalAppender is an extension to Codec for appending to a byte slice. +type marshalAppender interface { + Codec + + // MarshalAppend marshals the given message and appends it to the given + // byte slice. + // + // MarshalAppend may expect a specific type of message, and will error if + // this type is not given. + MarshalAppend([]byte, any) ([]byte, error) +} + +// stableCodec is an extension to Codec for serializing with stable output. +type stableCodec interface { + Codec + + // MarshalStable marshals the given message with stable field ordering. + // + // MarshalStable should return the same output for a given input. Although + // it is not guaranteed to be canonicalized, the marshalling routine for + // MarshalStable will opt for the most normalized output available for a + // given serialization. + // + // For practical reasons, it is possible for MarshalStable to return two + // different results for two inputs considered to be "equal" in their own + // domain, and it may change in the future with codec updates, but for + // any given concrete value and any given version, it should return the + // same output. + MarshalStable(any) ([]byte, error) + + // IsBinary returns true if the marshalled data is binary for this codec. + // + // If this function returns false, the data returned from Marshal and + // MarshalStable are considered valid text and may be used in contexts + // where text is expected. + IsBinary() bool +} + +type protoBinaryCodec struct{} + +var _ Codec = (*protoBinaryCodec)(nil) + +func (c *protoBinaryCodec) Name() string { return codecNameProto } + +func (c *protoBinaryCodec) Marshal(message any) ([]byte, error) { + protoMessage, ok := message.(proto.Message) + if !ok { + return nil, errNotProto(message) + } + return proto.Marshal(protoMessage) +} + +func (c *protoBinaryCodec) MarshalAppend(dst []byte, message any) ([]byte, error) { + protoMessage, ok := message.(proto.Message) + if !ok { + return nil, errNotProto(message) + } + return proto.MarshalOptions{}.MarshalAppend(dst, protoMessage) +} + +func (c *protoBinaryCodec) Unmarshal(data []byte, message any) error { + protoMessage, ok := message.(proto.Message) + if !ok { + return errNotProto(message) + } + err := proto.Unmarshal(data, protoMessage) + if err != nil { + return fmt.Errorf("unmarshal into %T: %w", message, err) + } + return nil +} + +func (c *protoBinaryCodec) MarshalStable(message any) ([]byte, error) { + protoMessage, ok := message.(proto.Message) + if !ok { + return nil, errNotProto(message) + } + // protobuf does not offer a canonical output today, so this format is not + // guaranteed to match deterministic output from other protobuf libraries. + // In addition, unknown fields may cause inconsistent output for otherwise + // equal messages. + // https://github.com/golang/protobuf/issues/1121 + options := proto.MarshalOptions{Deterministic: true} + return options.Marshal(protoMessage) +} + +func (c *protoBinaryCodec) IsBinary() bool { + return true +} + +type protoJSONCodec struct { + name string +} + +var _ Codec = (*protoJSONCodec)(nil) + +func (c *protoJSONCodec) Name() string { return c.name } + +func (c *protoJSONCodec) Marshal(message any) ([]byte, error) { + protoMessage, ok := message.(proto.Message) + if !ok { + return nil, errNotProto(message) + } + return protojson.MarshalOptions{}.Marshal(protoMessage) +} + +func (c *protoJSONCodec) MarshalAppend(dst []byte, message any) ([]byte, error) { + protoMessage, ok := message.(proto.Message) + if !ok { + return nil, errNotProto(message) + } + return protojson.MarshalOptions{}.MarshalAppend(dst, protoMessage) +} + +func (c *protoJSONCodec) Unmarshal(binary []byte, message any) error { + protoMessage, ok := message.(proto.Message) + if !ok { + return errNotProto(message) + } + if len(binary) == 0 { + return errors.New("zero-length payload is not a valid JSON object") + } + // Discard unknown fields so clients and servers aren't forced to always use + // exactly the same version of the schema. + options := protojson.UnmarshalOptions{DiscardUnknown: true} + err := options.Unmarshal(binary, protoMessage) + if err != nil { + return fmt.Errorf("unmarshal into %T: %w", message, err) + } + return nil +} + +func (c *protoJSONCodec) MarshalStable(message any) ([]byte, error) { + // protojson does not offer a "deterministic" field ordering, but fields + // are still ordered consistently by their index. However, protojson can + // output inconsistent whitespace for some reason, therefore it is + // suggested to use a formatter to ensure consistent formatting. + // https://github.com/golang/protobuf/issues/1373 + messageJSON, err := c.Marshal(message) + if err != nil { + return nil, err + } + compactedJSON := bytes.NewBuffer(messageJSON[:0]) + if err = json.Compact(compactedJSON, messageJSON); err != nil { + return nil, err + } + return compactedJSON.Bytes(), nil +} + +func (c *protoJSONCodec) IsBinary() bool { + return false +} + +// readOnlyCodecs is a read-only interface to a map of named codecs. +type readOnlyCodecs interface { + // Get gets the Codec with the given name. + Get(string) Codec + // Protobuf gets the user-supplied protobuf codec, falling back to the default + // implementation if necessary. + // + // This is helpful in the gRPC protocol, where the wire protocol requires + // marshaling protobuf structs to binary even if the RPC procedures were + // generated from a different IDL. + Protobuf() Codec + // Names returns a copy of the registered codec names. The returned slice is + // safe for the caller to mutate. + Names() []string +} + +func newReadOnlyCodecs(nameToCodec map[string]Codec) readOnlyCodecs { + return &codecMap{ + nameToCodec: nameToCodec, + } +} + +type codecMap struct { + nameToCodec map[string]Codec +} + +func (m *codecMap) Get(name string) Codec { + return m.nameToCodec[name] +} + +func (m *codecMap) Protobuf() Codec { + if pb, ok := m.nameToCodec[codecNameProto]; ok { + return pb + } + return &protoBinaryCodec{} +} + +func (m *codecMap) Names() []string { + names := make([]string, 0, len(m.nameToCodec)) + for name := range m.nameToCodec { + names = append(names, name) + } + return names +} + +func errNotProto(message any) error { + if _, ok := message.(protoiface.MessageV1); ok { + return fmt.Errorf("%T uses github.com/golang/protobuf, but connect-go only supports google.golang.org/protobuf: see https://go.dev/blog/protobuf-apiv2", message) + } + return fmt.Errorf("%T doesn't implement proto.Message", message) +} diff --git a/vendor/connectrpc.com/connect/compression.go b/vendor/connectrpc.com/connect/compression.go new file mode 100644 index 000000000000..4ff8ab0329b9 --- /dev/null +++ b/vendor/connectrpc.com/connect/compression.go @@ -0,0 +1,224 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "bytes" + "errors" + "io" + "math" + "net/http" + "strings" + "sync" +) + +const ( + compressionGzip = "gzip" + compressionIdentity = "identity" +) + +// A Decompressor is a reusable wrapper that decompresses an underlying data +// source. The standard library's [*gzip.Reader] implements Decompressor. +type Decompressor interface { + io.Reader + + // Close closes the Decompressor, but not the underlying data source. It may + // return an error if the Decompressor wasn't read to EOF. + Close() error + + // Reset discards the Decompressor's internal state, if any, and prepares it + // to read from a new source of compressed data. + Reset(io.Reader) error +} + +// A Compressor is a reusable wrapper that compresses data written to an +// underlying sink. The standard library's [*gzip.Writer] implements Compressor. +type Compressor interface { + io.Writer + + // Close flushes any buffered data to the underlying sink, then closes the + // Compressor. It must not close the underlying sink. + Close() error + + // Reset discards the Compressor's internal state, if any, and prepares it to + // write compressed data to a new sink. + Reset(io.Writer) +} + +type compressionPool struct { + decompressors sync.Pool + compressors sync.Pool +} + +func newCompressionPool( + newDecompressor func() Decompressor, + newCompressor func() Compressor, +) *compressionPool { + if newDecompressor == nil && newCompressor == nil { + return nil + } + return &compressionPool{ + decompressors: sync.Pool{ + New: func() any { return newDecompressor() }, + }, + compressors: sync.Pool{ + New: func() any { return newCompressor() }, + }, + } +} + +func (c *compressionPool) Decompress(dst *bytes.Buffer, src *bytes.Buffer, readMaxBytes int64) *Error { + decompressor, err := c.getDecompressor(src) + if err != nil { + return errorf(CodeInvalidArgument, "get decompressor: %w", err) + } + reader := io.Reader(decompressor) + if readMaxBytes > 0 && readMaxBytes < math.MaxInt64 { + reader = io.LimitReader(decompressor, readMaxBytes+1) + } + bytesRead, err := dst.ReadFrom(reader) + if err != nil { + _ = c.putDecompressor(decompressor) + err = wrapIfContextError(err) + if connectErr, ok := asError(err); ok { + return connectErr + } + return errorf(CodeInvalidArgument, "decompress: %w", err) + } + if readMaxBytes > 0 && bytesRead > readMaxBytes { + discardedBytes, err := io.Copy(io.Discard, decompressor) + _ = c.putDecompressor(decompressor) + if err != nil { + return errorf(CodeResourceExhausted, "message is larger than configured max %d - unable to determine message size: %w", readMaxBytes, err) + } + return errorf(CodeResourceExhausted, "message size %d is larger than configured max %d", bytesRead+discardedBytes, readMaxBytes) + } + if err := c.putDecompressor(decompressor); err != nil { + return errorf(CodeUnknown, "recycle decompressor: %w", err) + } + return nil +} + +func (c *compressionPool) Compress(dst *bytes.Buffer, src *bytes.Buffer) *Error { + compressor, err := c.getCompressor(dst) + if err != nil { + return errorf(CodeUnknown, "get compressor: %w", err) + } + if _, err := src.WriteTo(compressor); err != nil { + _ = c.putCompressor(compressor) + err = wrapIfContextError(err) + if connectErr, ok := asError(err); ok { + return connectErr + } + return errorf(CodeInternal, "compress: %w", err) + } + if err := c.putCompressor(compressor); err != nil { + return errorf(CodeInternal, "recycle compressor: %w", err) + } + return nil +} + +func (c *compressionPool) getDecompressor(reader io.Reader) (Decompressor, error) { + decompressor, ok := c.decompressors.Get().(Decompressor) + if !ok { + return nil, errors.New("expected Decompressor, got incorrect type from pool") + } + return decompressor, decompressor.Reset(reader) +} + +func (c *compressionPool) putDecompressor(decompressor Decompressor) error { + if err := decompressor.Close(); err != nil { + return err + } + // While it's in the pool, we don't want the decompressor to retain a + // reference to the underlying reader. However, most decompressors attempt to + // read some header data from the new data source when Reset; since we don't + // know the compression format, we can't provide a valid header. Since we + // also reset the decompressor when it's pulled out of the pool, we can + // ignore errors here. + _ = decompressor.Reset(http.NoBody) + c.decompressors.Put(decompressor) + return nil +} + +func (c *compressionPool) getCompressor(writer io.Writer) (Compressor, error) { + compressor, ok := c.compressors.Get().(Compressor) + if !ok { + return nil, errors.New("expected Compressor, got incorrect type from pool") + } + compressor.Reset(writer) + return compressor, nil +} + +func (c *compressionPool) putCompressor(compressor Compressor) error { + if err := compressor.Close(); err != nil { + return err + } + compressor.Reset(io.Discard) // don't keep references + c.compressors.Put(compressor) + return nil +} + +// readOnlyCompressionPools is a read-only interface to a map of named +// compressionPools. +type readOnlyCompressionPools interface { + Get(string) *compressionPool + Contains(string) bool + // Wordy, but clarifies how this is different from readOnlyCodecs.Names(). + CommaSeparatedNames() string +} + +func newReadOnlyCompressionPools( + nameToPool map[string]*compressionPool, + reversedNames []string, +) readOnlyCompressionPools { + // Client and handler configs keep compression names in registration order, + // but we want the last registered to be the most preferred. + names := make([]string, 0, len(reversedNames)) + seen := make(map[string]struct{}, len(reversedNames)) + for i := len(reversedNames) - 1; i >= 0; i-- { + name := reversedNames[i] + if _, ok := seen[name]; ok { + continue + } + seen[name] = struct{}{} + names = append(names, name) + } + return &namedCompressionPools{ + nameToPool: nameToPool, + commaSeparatedNames: strings.Join(names, ","), + } +} + +type namedCompressionPools struct { + nameToPool map[string]*compressionPool + commaSeparatedNames string +} + +func (m *namedCompressionPools) Get(name string) *compressionPool { + if name == "" || name == compressionIdentity { + return nil + } + return m.nameToPool[name] +} + +func (m *namedCompressionPools) Contains(name string) bool { + _, ok := m.nameToPool[name] + return ok +} + +func (m *namedCompressionPools) CommaSeparatedNames() string { + return m.commaSeparatedNames +} diff --git a/vendor/connectrpc.com/connect/connect.go b/vendor/connectrpc.com/connect/connect.go new file mode 100644 index 000000000000..04bbca85cbe8 --- /dev/null +++ b/vendor/connectrpc.com/connect/connect.go @@ -0,0 +1,482 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package connect is a slim RPC framework built on Protocol Buffers and +// [net/http]. In addition to supporting its own protocol, Connect handlers and +// clients are wire-compatible with gRPC and gRPC-Web, including streaming. +// +// This documentation is intended to explain each type and function in +// isolation. Walkthroughs, FAQs, and other narrative docs are available on the +// [Connect website], and there's a working [demonstration service] on Github. +// +// [Connect website]: https://connectrpc.com +// [demonstration service]: https://github.com/connectrpc/examples-go +package connect + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" +) + +// Version is the semantic version of the connect module. +const Version = "1.19.1" + +// These constants are used in compile-time handshakes with connect's generated +// code. +const ( + IsAtLeastVersion0_0_1 = true + IsAtLeastVersion0_1_0 = true + IsAtLeastVersion1_7_0 = true + IsAtLeastVersion1_13_0 = true +) + +// StreamType describes whether the client, server, neither, or both is +// streaming. +type StreamType uint8 + +const ( + StreamTypeUnary StreamType = 0b00 + StreamTypeClient StreamType = 0b01 + StreamTypeServer StreamType = 0b10 + StreamTypeBidi = StreamTypeClient | StreamTypeServer +) + +func (s StreamType) String() string { + switch s { + case StreamTypeUnary: + return "unary" + case StreamTypeClient: + return "client" + case StreamTypeServer: + return "server" + case StreamTypeBidi: + return "bidi" + } + return fmt.Sprintf("stream_%d", s) +} + +// StreamingHandlerConn is the server's view of a bidirectional message +// exchange. Interceptors for streaming RPCs may wrap StreamingHandlerConns. +// +// Like the standard library's [http.ResponseWriter], StreamingHandlerConns write +// response headers to the network with the first call to Send. Any subsequent +// mutations are effectively no-ops. Handlers may mutate response trailers at +// any time before returning. When the client has finished sending data, +// Receive returns an error wrapping [io.EOF]. Handlers should check for this +// using the standard library's [errors.Is]. +// +// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for +// use by the gRPC and Connect protocols: applications may read them but +// shouldn't write them. +// +// StreamingHandlerConn implementations provided by this module guarantee that +// all returned errors can be cast to [*Error] using the standard library's +// [errors.As]. +// +// StreamingHandlerConn implementations do not need to be safe for concurrent use. +type StreamingHandlerConn interface { + Spec() Spec + Peer() Peer + + Receive(any) error + RequestHeader() http.Header + + Send(any) error + ResponseHeader() http.Header + ResponseTrailer() http.Header +} + +// StreamingClientConn is the client's view of a bidirectional message exchange. +// Interceptors for streaming RPCs may wrap StreamingClientConns. +// +// StreamingClientConns write request headers to the network with the first +// call to Send. Any subsequent mutations are effectively no-ops. When the +// server is done sending data, the StreamingClientConn's Receive method +// returns an error wrapping [io.EOF]. Clients should check for this using the +// standard library's [errors.Is]. If the server encounters an error during +// processing, subsequent calls to the StreamingClientConn's Send method will +// return an error wrapping [io.EOF]; clients may then call Receive to unmarshal +// the error. +// +// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for +// use by the gRPC and Connect protocols: applications may read them but +// shouldn't write them. +// +// StreamingClientConn implementations provided by this module guarantee that +// all returned errors can be cast to [*Error] using the standard library's +// [errors.As]. +// +// In order to support bidirectional streaming RPCs, all StreamingClientConn +// implementations must support limited concurrent use. See the comments on +// each group of methods for details. +type StreamingClientConn interface { + // Spec and Peer must be safe to call concurrently with all other methods. + Spec() Spec + Peer() Peer + + // Send, RequestHeader, and CloseRequest may race with each other, but must + // be safe to call concurrently with all other methods. + Send(any) error + RequestHeader() http.Header + CloseRequest() error + + // Receive, ResponseHeader, ResponseTrailer, and CloseResponse may race with + // each other, but must be safe to call concurrently with all other methods. + Receive(any) error + ResponseHeader() http.Header + ResponseTrailer() http.Header + CloseResponse() error +} + +// Request is a wrapper around a generated request message. It provides +// access to metadata like headers and the RPC specification, as well as +// strongly-typed access to the message itself. +type Request[T any] struct { + Msg *T + + spec Spec + peer Peer + header http.Header + method string +} + +// NewRequest wraps a generated request message. +func NewRequest[T any](message *T) *Request[T] { + return &Request[T]{ + Msg: message, + // Initialized lazily so we don't allocate unnecessarily. + header: nil, + } +} + +// Any returns the concrete request message as an empty interface, so that +// *Request implements the [AnyRequest] interface. +func (r *Request[_]) Any() any { + return r.Msg +} + +// Spec returns a description of this RPC. +func (r *Request[_]) Spec() Spec { + return r.spec +} + +// Peer describes the other party for this RPC. +func (r *Request[_]) Peer() Peer { + return r.peer +} + +// Header returns the HTTP headers for this request. Headers beginning with +// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC +// protocols: applications may read them but shouldn't write them. +func (r *Request[_]) Header() http.Header { + if r.header == nil { + r.header = make(http.Header) + } + return r.header +} + +// HTTPMethod returns the HTTP method for this request. This is nearly always +// POST, but side-effect-free unary RPCs could be made via a GET. +// +// On a newly created request, via NewRequest, this will return the empty +// string until the actual request is actually sent and the HTTP method +// determined. This means that client interceptor functions will see the +// empty string until *after* they delegate to the handler they wrapped. It +// is even possible for this to return the empty string after such delegation, +// if the request was never actually sent to the server (and thus no +// determination ever made about the HTTP method). +func (r *Request[_]) HTTPMethod() string { + return r.method +} + +// internalOnly implements AnyRequest. +func (r *Request[_]) internalOnly() {} + +// setRequestMethod sets the request method to the given value. +func (r *Request[_]) setRequestMethod(method string) { + r.method = method +} + +// AnyRequest is the common method set of every [Request], regardless of type +// parameter. It's used in unary interceptors. +// +// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for +// use by the gRPC and Connect protocols: applications may read them but +// shouldn't write them. +// +// To preserve our ability to add methods to this interface without breaking +// backward compatibility, only types defined in this package can implement +// AnyRequest. +type AnyRequest interface { + Any() any + Spec() Spec + Peer() Peer + Header() http.Header + HTTPMethod() string + + internalOnly() + setRequestMethod(string) +} + +// Response is a wrapper around a generated response message. It provides +// access to metadata like headers and trailers, as well as strongly-typed +// access to the message itself. +type Response[T any] struct { + Msg *T + + header http.Header + trailer http.Header +} + +// NewResponse wraps a generated response message. +func NewResponse[T any](message *T) *Response[T] { + return &Response[T]{ + Msg: message, + // Initialized lazily so we don't allocate unnecessarily. + header: nil, + trailer: nil, + } +} + +// Any returns the concrete response message as an empty interface, so that +// *Response implements the [AnyResponse] interface. +func (r *Response[_]) Any() any { + return r.Msg +} + +// Header returns the HTTP headers for this response. Headers beginning with +// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC +// protocols: applications may read them but shouldn't write them. +func (r *Response[_]) Header() http.Header { + if r.header == nil { + r.header = make(http.Header) + } + return r.header +} + +// Trailer returns the trailers for this response. Depending on the underlying +// RPC protocol, trailers may be sent as HTTP trailers or a protocol-specific +// block of in-body metadata. +// +// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols: applications may read them but shouldn't write +// them. +func (r *Response[_]) Trailer() http.Header { + if r.trailer == nil { + r.trailer = make(http.Header) + } + return r.trailer +} + +// internalOnly implements AnyResponse. +func (r *Response[_]) internalOnly() {} + +// AnyResponse is the common method set of every [Response], regardless of type +// parameter. It's used in unary interceptors. +// +// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for +// use by the gRPC and Connect protocols: applications may read them but +// shouldn't write them. +// +// To preserve our ability to add methods to this interface without breaking +// backward compatibility, only types defined in this package can implement +// AnyResponse. +type AnyResponse interface { + Any() any + Header() http.Header + Trailer() http.Header + + internalOnly() +} + +// HTTPClient is the interface connect expects HTTP clients to implement. The +// standard library's *http.Client implements HTTPClient. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// Spec is a description of a client call or a handler invocation. +// +// If you're using Protobuf, protoc-gen-connect-go generates a constant for the +// fully-qualified Procedure corresponding to each RPC in your schema. +type Spec struct { + StreamType StreamType + Schema any // for protobuf RPCs, a protoreflect.MethodDescriptor + Procedure string // for example, "/acme.foo.v1.FooService/Bar" + IsClient bool // otherwise we're in a handler + IdempotencyLevel IdempotencyLevel +} + +// Peer describes the other party to an RPC. +// +// When accessed client-side, Addr contains the host or host:port from the +// server's URL. When accessed server-side, Addr contains the client's address +// in IP:port format. +// +// On both the client and the server, Protocol is the RPC protocol in use. +// Currently, it's either [ProtocolConnect], [ProtocolGRPC], or +// [ProtocolGRPCWeb], but additional protocols may be added in the future. +// +// Query contains the query parameters for the request. For the server, this +// will reflect the actual query parameters sent. For the client, it is unset. +type Peer struct { + Addr string + Protocol string + Query url.Values // server-only +} + +func newPeerForURL(url *url.URL, protocol string) Peer { + return Peer{ + Addr: url.Host, + Protocol: protocol, + } +} + +// handlerConnCloser extends StreamingHandlerConn with a method for handlers to +// terminate the message exchange (and optionally send an error to the client). +type handlerConnCloser interface { + StreamingHandlerConn + + Close(error) error +} + +// receiveConn represents the shared methods of both StreamingClientConn and StreamingHandlerConn +// that the below helper functions use for implementing the rules around a "unary" stream, that +// is expected to have exactly one message (or zero messages followed by a non-EOF error). +type receiveConn interface { + Spec() Spec + Receive(any) error +} + +// hasHTTPMethod is implemented by streaming connections that support HTTP methods other than +// POST. +type hasHTTPMethod interface { + getHTTPMethod() string +} + +// errStreamingClientConn is a sentinel error implementation of StreamingClientConn. +type errStreamingClientConn struct { + err error +} + +func (c *errStreamingClientConn) Receive(msg any) error { + return c.err +} + +func (c *errStreamingClientConn) Spec() Spec { + return Spec{} +} + +func (c *errStreamingClientConn) Peer() Peer { + return Peer{} +} + +func (c *errStreamingClientConn) Send(msg any) error { + return c.err +} + +func (c *errStreamingClientConn) CloseRequest() error { + return c.err +} + +func (c *errStreamingClientConn) CloseResponse() error { + return c.err +} + +func (c *errStreamingClientConn) RequestHeader() http.Header { + return make(http.Header) +} + +func (c *errStreamingClientConn) ResponseHeader() http.Header { + return make(http.Header) +} + +func (c *errStreamingClientConn) ResponseTrailer() http.Header { + return make(http.Header) +} + +// receiveUnaryResponse unmarshals a message from a StreamingClientConn, then +// envelopes the message and attaches headers and trailers. It attempts to +// consume the response stream and isn't appropriate when receiving multiple +// messages. +func receiveUnaryResponse[T any](conn StreamingClientConn, initializer maybeInitializer) (*Response[T], error) { + msg, err := receiveUnaryMessage[T](conn, initializer, "response") + if err != nil { + return nil, err + } + return &Response[T]{ + Msg: msg, + header: conn.ResponseHeader(), + trailer: conn.ResponseTrailer(), + }, nil +} + +// receiveUnaryRequest unmarshals a message from a StreamingClientConn, then +// envelopes the message and attaches headers and other request properties. It +// attempts to consume the request stream and isn't appropriate when receiving +// multiple messages. +func receiveUnaryRequest[T any](conn StreamingHandlerConn, initializer maybeInitializer) (*Request[T], error) { + msg, err := receiveUnaryMessage[T](conn, initializer, "request") + if err != nil { + return nil, err + } + method := http.MethodPost + if hasRequestMethod, ok := conn.(hasHTTPMethod); ok { + method = hasRequestMethod.getHTTPMethod() + } + return &Request[T]{ + Msg: msg, + spec: conn.Spec(), + peer: conn.Peer(), + header: conn.RequestHeader(), + method: method, + }, nil +} + +func receiveUnaryMessage[T any](conn receiveConn, initializer maybeInitializer, what string) (*T, error) { + var msg T + if err := initializer.maybe(conn.Spec(), &msg); err != nil { + return nil, err + } + // Possibly counter-intuitive, but the gRPC specs about error codes state that both clients + // and servers should return "unimplemented" when they encounter a cardinality violation: where + // the number of messages in the stream is wrong. Search for "cardinality violation" in the + // following docs: + // https://grpc.github.io/grpc/core/md_doc_statuscodes.html + if err := conn.Receive(&msg); err != nil { + if errors.Is(err, io.EOF) { + err = NewError(CodeUnimplemented, fmt.Errorf("unary %s has zero messages", what)) + } + return nil, err + } + // In a well-formed stream, the one message must be the only content in the body. + // To verify that it is well-formed, try to read another message from the stream. + // TODO: optimize this second receive: ideally do it w/out allocation, w/out + // fully reading next message (if one is present), and w/out trying to + // actually unmarshal the bytes) + var msg2 T + if err := initializer.maybe(conn.Spec(), &msg2); err != nil { + return nil, err + } + if err := conn.Receive(&msg2); !errors.Is(err, io.EOF) { + if err == nil { + err = NewError(CodeUnimplemented, fmt.Errorf("unary %s has multiple messages", what)) + } + return nil, err + } + return &msg, nil +} diff --git a/vendor/connectrpc.com/connect/context.go b/vendor/connectrpc.com/connect/context.go new file mode 100644 index 000000000000..9f2d01808631 --- /dev/null +++ b/vendor/connectrpc.com/connect/context.go @@ -0,0 +1,242 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "context" + "net/http" +) + +// CallInfo represents information relevant to an RPC call. +type CallInfo interface { + // Spec returns a description of this call. + Spec() Spec + // Peer describes the other party for this call. + Peer() Peer + // RequestHeader returns the HTTP headers for this request. Headers beginning with + // "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC + // protocols: applications may read them but shouldn't write them. + RequestHeader() http.Header + // ResponseHeader returns the HTTP headers for this response. Headers beginning with + // "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC + // protocols: applications may read them but shouldn't write them. + // On the client side, this method returns nil before + // the call is actually made. After the call is made, for streaming operations, + // this method will block for the server to actually return response headers. + ResponseHeader() http.Header + // ResponseTrailer returns the trailers for this response. Depending on the underlying + // RPC protocol, trailers may be sent as HTTP trailers or a protocol-specific + // block of in-body metadata. + // + // Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the + // Connect and gRPC protocols: applications may read them but shouldn't write + // them. + // + // On the client side, this method returns nil before the call is actually made. + // After the call is made, for streaming operations, this method will block + // for the server to actually return response trailers. + ResponseTrailer() http.Header + // HTTPMethod returns the HTTP method for this request. This is nearly always + // POST, but side-effect-free unary RPCs could be made via a GET. + // + // On a newly created request, via NewRequest, this will return the empty + // string until the actual request is actually sent and the HTTP method + // determined. This means that client interceptor functions will see the + // empty string until *after* they delegate to the handler they wrapped. It + // is even possible for this to return the empty string after such delegation, + // if the request was never actually sent to the server (and thus no + // determination ever made about the HTTP method). + HTTPMethod() string + + internalOnly() +} + +// Create a new client (i.e. outgoing) context for use from a client. When the +// returned context is passed to RPCs, the returned call info can be used to set +// request metadata before the RPC is invoked and to inspect response +// metadata after the RPC completes. +// +// The returned context may be re-used across RPCs as long as they are +// not concurrent. Results of all CallInfo methods other than +// RequestHeader() are undefined if the context is used with concurrent RPCs. +func NewClientContext(ctx context.Context) (context.Context, CallInfo) { + info := &clientCallInfo{} + return context.WithValue(ctx, clientCallInfoContextKey{}, info), info +} + +// CallInfoForHandlerContext returns the CallInfo for the given handler (i.e. incoming) context, if there is one. +func CallInfoForHandlerContext(ctx context.Context) (CallInfo, bool) { + value, ok := ctx.Value(handlerCallInfoContextKey{}).(CallInfo) + return value, ok +} + +// handlerCallInfo is a CallInfo implementation used for unary handlers. +type handlerCallInfo struct { + spec Spec + peer Peer + method string + requestHeader http.Header + responseHeader http.Header + responseTrailer http.Header +} + +func (c *handlerCallInfo) Spec() Spec { + return c.spec +} + +func (c *handlerCallInfo) Peer() Peer { + return c.peer +} + +func (c *handlerCallInfo) RequestHeader() http.Header { + if c.requestHeader == nil { + c.requestHeader = make(http.Header) + } + return c.requestHeader +} + +func (c *handlerCallInfo) ResponseHeader() http.Header { + if c.responseHeader == nil { + c.responseHeader = make(http.Header) + } + return c.responseHeader +} + +func (c *handlerCallInfo) ResponseTrailer() http.Header { + if c.responseTrailer == nil { + c.responseTrailer = make(http.Header) + } + return c.responseTrailer +} + +func (c *handlerCallInfo) HTTPMethod() string { + return c.method +} + +// internalOnly implements CallInfo. +func (c *handlerCallInfo) internalOnly() {} + +// streamingHandlerCallInfo is a CallInfo implementation used for streaming RPC handlers. +type streamingHandlerCallInfo struct { + conn StreamingHandlerConn +} + +func (c *streamingHandlerCallInfo) Spec() Spec { + return c.conn.Spec() +} + +func (c *streamingHandlerCallInfo) Peer() Peer { + return c.conn.Peer() +} + +func (c *streamingHandlerCallInfo) RequestHeader() http.Header { + return c.conn.RequestHeader() +} + +func (c *streamingHandlerCallInfo) ResponseHeader() http.Header { + return c.conn.ResponseHeader() +} + +func (c *streamingHandlerCallInfo) ResponseTrailer() http.Header { + return c.conn.ResponseTrailer() +} + +func (c *streamingHandlerCallInfo) HTTPMethod() string { + // All stream calls are POSTs + return http.MethodPost +} + +// internalOnly implements CallInfo. +func (c *streamingHandlerCallInfo) internalOnly() {} + +// clientCallInfo is a CallInfo implementation used for clients. +type clientCallInfo struct { + responseSource + spec Spec + peer Peer + method string + requestHeader http.Header +} + +func (c *clientCallInfo) Spec() Spec { + return c.spec +} + +func (c *clientCallInfo) Peer() Peer { + return c.peer +} + +func (c *clientCallInfo) RequestHeader() http.Header { + if c.requestHeader == nil { + c.requestHeader = make(http.Header) + } + return c.requestHeader +} + +func (c *clientCallInfo) ResponseHeader() http.Header { + if c.responseSource == nil { + return nil + } + return c.responseSource.ResponseHeader() +} + +func (c *clientCallInfo) ResponseTrailer() http.Header { + if c.responseSource == nil { + return nil + } + return c.responseSource.ResponseTrailer() +} + +func (c *clientCallInfo) HTTPMethod() string { + return c.method +} + +// internalOnly implements CallInfo. +func (c *clientCallInfo) internalOnly() {} + +// clientCallInfoContextKey is the key used to store client call info in context. +type clientCallInfoContextKey struct{} + +// sentinelContextKey is the key used to store a copy of client call info in context +// when a request is made. +// Each step in an interceptor chain compares the actual call info with the +// sentinel call info. If the two values are different, the request will +// return an error in the interceptor. +// This protects against changing the call info in interceptors, which is prohibited +// as it would allow users to modify call info mid-flight independent of the actual +// request or response. +// Users who wish to modify call info data such as headers and trailers should instead +// use Connect [Request] and [Response] wrapper types. +type sentinelContextKey struct{} + +// handlerCallInfoContextKey is the key used to store handler call info in context. +type handlerCallInfoContextKey struct{} + +// responseSource indicates a type that manages response headers and trailers. +type responseSource interface { + ResponseHeader() http.Header + ResponseTrailer() http.Header +} + +// clientCallInfoForContext gets the call info from a client/outgoing context. +func clientCallInfoForContext(ctx context.Context) (*clientCallInfo, bool) { + info, ok := ctx.Value(clientCallInfoContextKey{}).(*clientCallInfo) + return info, ok +} + +// newHandlerContext creates a new handler/incoming context. +func newHandlerContext(ctx context.Context, info CallInfo) context.Context { + return context.WithValue(ctx, handlerCallInfoContextKey{}, info) +} diff --git a/vendor/connectrpc.com/connect/duplex_http_call.go b/vendor/connectrpc.com/connect/duplex_http_call.go new file mode 100644 index 000000000000..80f6b671cebc --- /dev/null +++ b/vendor/connectrpc.com/connect/duplex_http_call.go @@ -0,0 +1,471 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "context" + "errors" + "io" + "net/http" + "net/url" + "sync" + "sync/atomic" +) + +// duplexHTTPCall is a full-duplex stream between the client and server. The +// request body is the stream from client to server, and the response body is +// the reverse. +// +// Be warned: we need to use some lesser-known APIs to do this with net/http. +type duplexHTTPCall struct { + ctx context.Context + httpClient HTTPClient + streamType StreamType + onRequestSend func(*http.Request) + validateResponse func(*http.Response) *Error + + // io.Pipe is used to implement the request body for client streaming calls. + // If the request is unary, requestBodyWriter is nil. + requestBodyWriter *io.PipeWriter + + // requestSent ensures we only send the request once. + requestSent atomic.Bool + request *http.Request + + // responseReady is closed when the response is ready or when the request + // fails. Any error on request initialisation will be set on the + // responseErr. There's always a response if responseErr is nil. + responseReady chan struct{} + response *http.Response + responseErr error +} + +func newDuplexHTTPCall( + ctx context.Context, + httpClient HTTPClient, + url *url.URL, + spec Spec, + header http.Header, +) *duplexHTTPCall { + // ensure we make a copy of the url before we pass along to the + // Request. This ensures if a transport out of our control wants + // to mutate the req.URL, we don't feel the effects of it. + url = cloneURL(url) + + // This is mirroring what http.NewRequestContext did, but + // using an already parsed url.URL object, rather than a string + // and parsing it again. This is a bit funny with HTTP/1.1 + // explicitly, but this is logic copied over from + // NewRequestContext and doesn't effect the actual version + // being transmitted. + request := (&http.Request{ + Method: http.MethodPost, + URL: url, + Header: header, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Body: http.NoBody, + GetBody: getNoBody, + Host: url.Host, + }).WithContext(ctx) + return &duplexHTTPCall{ + ctx: ctx, + httpClient: httpClient, + streamType: spec.StreamType, + request: request, + responseReady: make(chan struct{}), + } +} + +// Send sends a message to the server. +func (d *duplexHTTPCall) Send(payload messagePayload) (int64, error) { + if d.streamType&StreamTypeClient == 0 { + return d.sendUnary(payload) + } + isFirst := d.requestSent.CompareAndSwap(false, true) + if isFirst { + // This is the first time we're sending a message to the server. + // We need to send the request headers and start the request. + pipeReader, pipeWriter := io.Pipe() + d.requestBodyWriter = pipeWriter + d.request.Body = pipeReader + d.request.GetBody = nil // GetBody not supported for client streaming + d.request.ContentLength = -1 + go d.makeRequest() // concurrent request + } + if err := d.ctx.Err(); err != nil { + return 0, wrapIfContextError(err) + } + if isFirst && payload.Len() == 0 { + // On first write a nil Send is used to send request headers. Avoid + // writing a zero-length payload to avoid superfluous errors with close. + return 0, nil + } + // It's safe to write to this side of the pipe while net/http concurrently + // reads from the other side. + bytesWritten, err := payload.WriteTo(d.requestBodyWriter) + if err != nil && errors.Is(err, io.ErrClosedPipe) { + // Signal that the stream is closed with the more-typical io.EOF instead of + // io.ErrClosedPipe. This makes it easier for protocol-specific wrappers to + // match grpc-go's behavior. + err = io.EOF + } + return bytesWritten, err +} + +func (d *duplexHTTPCall) sendUnary(payload messagePayload) (int64, error) { + // Unary messages are sent as a single HTTP request. We don't need to use a + // pipe for the request body and we don't need to send headers separately. + if !d.requestSent.CompareAndSwap(false, true) { + return 0, errors.New("request already sent") + } + payloadLength := int64(payload.Len()) + if payloadLength > 0 { + // Build the request body from the payload. + payloadBody := newPayloadCloser(payload) + d.request.Body = payloadBody + d.request.ContentLength = payloadLength + d.request.GetBody = func() (io.ReadCloser, error) { + if !payloadBody.Rewind() { + return nil, errors.New("payload cannot be retried") + } + return payloadBody, nil + } + // Release the payload ensuring that after Send returns the + // payload is safe to be reused. See [http.RoundTripper] for + // more details. + defer payloadBody.Release() + } + d.makeRequest() // synchronous request + if d.responseErr != nil { + // Check on response errors for context errors. Other errors are + // handled on read. + if err := d.ctx.Err(); err != nil { + return 0, wrapIfContextError(err) + } + } + return payloadLength, nil +} + +// CloseWrite closes the request body. Callers *must* call CloseWrite before Read when +// using HTTP/1.x. +func (d *duplexHTTPCall) CloseWrite() error { + // Even if Write was never called, we need to make an HTTP request. This + // ensures that we've sent any headers to the server and that we have an HTTP + // response to read from. + if d.requestSent.CompareAndSwap(false, true) { + go d.makeRequest() + // We never setup a request body, so it's effectively already closed. + // So nothing else to do. + return nil + } + // The user calls CloseWrite to indicate that they're done sending data. It's + // safe to close the write side of the pipe while net/http is reading from + // it. + // + // Because connect also supports some RPC types over HTTP/1.1, we need to be + // careful how we expose this method to users. HTTP/1.1 doesn't support + // bidirectional streaming - the write side of the stream (aka request body) + // must be closed before we start reading the response or we'll just block + // forever. To make sure users don't have to worry about this, the generated + // code for unary, client streaming, and server streaming RPCs must call + // CloseWrite automatically rather than requiring the user to do it. + if d.requestBodyWriter != nil { + return d.requestBodyWriter.Close() + } + return d.request.Body.Close() +} + +// Header returns the HTTP request headers. +func (d *duplexHTTPCall) Header() http.Header { + return d.request.Header +} + +// Trailer returns the HTTP request trailers. +func (d *duplexHTTPCall) Trailer() http.Header { + return d.request.Trailer +} + +// URL returns the URL for the request. +func (d *duplexHTTPCall) URL() *url.URL { + return d.request.URL +} + +// Method returns the HTTP method for the request (GET or POST). +func (d *duplexHTTPCall) Method() string { + return d.request.Method +} + +// SetMethod changes the method of the request before it is sent. +func (d *duplexHTTPCall) SetMethod(method string) { + d.request.Method = method +} + +// Read from the response body. Returns the first error passed to SetError. +func (d *duplexHTTPCall) Read(data []byte) (int, error) { + // First, we wait until we've gotten the response headers and established the + // server-to-client side of the stream. + if err := d.BlockUntilResponseReady(); err != nil { + // The stream is already closed or corrupted. + return 0, err + } + // Before we read, check if the context has been canceled. + if err := d.ctx.Err(); err != nil { + return 0, wrapIfContextError(err) + } + n, err := d.response.Body.Read(data) + if err != nil && !errors.Is(err, io.EOF) { + err = wrapIfContextDone(d.ctx, err) + err = wrapIfRSTError(err) + } + return n, err +} + +func (d *duplexHTTPCall) CloseRead() error { + _ = d.BlockUntilResponseReady() + if d.response == nil { + return nil + } + err := d.response.Body.Close() + err = wrapIfContextDone(d.ctx, err) + return wrapIfRSTError(err) +} + +// ResponseStatusCode is the response's HTTP status code. +func (d *duplexHTTPCall) ResponseStatusCode() (int, error) { + if err := d.BlockUntilResponseReady(); err != nil { + return 0, err + } + return d.response.StatusCode, nil +} + +// ResponseHeader returns the response HTTP headers. +func (d *duplexHTTPCall) ResponseHeader() http.Header { + _ = d.BlockUntilResponseReady() + if d.response != nil { + return d.response.Header + } + return make(http.Header) +} + +// ResponseTrailer returns the response HTTP trailers. +func (d *duplexHTTPCall) ResponseTrailer() http.Header { + _ = d.BlockUntilResponseReady() + if d.response != nil { + return d.response.Trailer + } + return make(http.Header) +} + +// SetValidateResponse sets the response validation function. The function runs +// in a background goroutine. +func (d *duplexHTTPCall) SetValidateResponse(validate func(*http.Response) *Error) { + d.validateResponse = validate +} + +// BlockUntilResponseReady returns when the response is ready or reports an +// error from initializing the request. +func (d *duplexHTTPCall) BlockUntilResponseReady() error { + <-d.responseReady + return d.responseErr +} + +func (d *duplexHTTPCall) makeRequest() { + // This runs concurrently with Write and CloseWrite. Read and CloseRead wait + // on d.responseReady, so we can't race with them. + defer close(d.responseReady) + + // Promote the header Host to the request object. + if host := getHeaderCanonical(d.request.Header, headerHost); len(host) > 0 { + d.request.Host = host + } + if d.onRequestSend != nil { + d.onRequestSend(d.request) + } + // Once we send a message to the server, they send a message back and + // establish the receive side of the stream. + // On error, we close the request body using the Write side of the pipe. + // This ensures HTTP2 streams receive an io.EOF from the Read side of the + // pipe. Write's check for io.ErrClosedPipe and will convert this to io.EOF. + response, err := d.httpClient.Do(d.request) //nolint:bodyclose + if err != nil { + if errors.Is(err, io.EOF) { + // We use io.EOF as a sentinel in many places and don't want this + // transport error to be confused for those other situations. + err = io.ErrUnexpectedEOF + } + err = wrapIfContextError(err) + err = wrapIfLikelyH2CNotConfiguredError(d.request, err) + err = wrapIfLikelyWithGRPCNotUsedError(err) + err = wrapIfRSTError(err) + if _, ok := asError(err); !ok { + err = NewError(CodeUnavailable, err) + } + d.responseErr = err + _ = d.CloseWrite() + return + } + // We've got a response. We can now read from the response body. + // Closing the response body is delegated to the caller even on error. + d.response = response + if err := d.validateResponse(response); err != nil { + d.responseErr = err + _ = d.CloseWrite() + return + } + if (d.streamType&StreamTypeBidi) == StreamTypeBidi && response.ProtoMajor < 2 { + // If we somehow dialed an HTTP/1.x server, fail with an explicit message + // rather than returning a more cryptic error later on. + d.responseErr = errorf( + CodeUnimplemented, + "response from %v is HTTP/%d.%d: bidi streams require at least HTTP/2", + d.request.URL, + response.ProtoMajor, + response.ProtoMinor, + ) + _ = d.CloseWrite() + } +} + +// getNoBody is a GetBody function for http.NoBody. +func getNoBody() (io.ReadCloser, error) { + return http.NoBody, nil +} + +// messagePayload is a sized and seekable message payload. The interface is +// implemented by [*bytes.Reader] and *envelope. Reads must be non-blocking. +type messagePayload interface { + io.Reader + io.WriterTo + io.Seeker + Len() int +} + +// nopPayload is a message payload that does nothing. It's used to send headers +// to the server. +type nopPayload struct{} + +var _ messagePayload = nopPayload{} + +func (nopPayload) Read([]byte) (int, error) { + return 0, io.EOF +} + +func (nopPayload) WriteTo(io.Writer) (int64, error) { + return 0, nil +} + +func (nopPayload) Seek(int64, int) (int64, error) { + return 0, nil +} + +func (nopPayload) Len() int { + return 0 +} + +// messageSender sends a message payload. The interface is implemented by +// [*duplexHTTPCall] and writeSender. +type messageSender interface { + Send(messagePayload) (int64, error) +} + +// writeSender is a sender that writes to an [io.Writer]. Useful for wrapping +// [http.ResponseWriter]. +type writeSender struct { + writer io.Writer +} + +var _ messageSender = writeSender{} + +func (w writeSender) Send(payload messagePayload) (int64, error) { + return payload.WriteTo(w.writer) +} + +// See: https://cs.opensource.google/go/go/+/refs/tags/go1.20.1:src/net/http/clone.go;l=22-33 +func cloneURL(oldURL *url.URL) *url.URL { + if oldURL == nil { + return nil + } + newURL := new(url.URL) + *newURL = *oldURL + if oldURL.User != nil { + newURL.User = new(url.Userinfo) + *newURL.User = *oldURL.User + } + return newURL +} + +// payloadCloser is an [io.ReadCloser] that wraps a messagePayload. It's used to +// implement the request body for unary calls. To safely reuse the buffer +// call Release after the response is received to ensure the payload is safe for +// reuse. +type payloadCloser struct { + mu sync.Mutex + payload messagePayload // nil after Release +} + +func newPayloadCloser(payload messagePayload) *payloadCloser { + return &payloadCloser{ + payload: payload, + } +} + +// Read implements [io.Reader]. +func (p *payloadCloser) Read(dst []byte) (readN int, err error) { + p.mu.Lock() + defer p.mu.Unlock() + if p.payload == nil { + return 0, io.EOF + } + return p.payload.Read(dst) +} + +// WriteTo implements [io.WriterTo]. +func (p *payloadCloser) WriteTo(dst io.Writer) (int64, error) { + p.mu.Lock() + defer p.mu.Unlock() + if p.payload == nil { + return 0, nil + } + return p.payload.WriteTo(dst) +} + +// Close implements [io.Closer]. +func (p *payloadCloser) Close() error { + return nil +} + +// Rewind rewinds the payload to the beginning. It returns false if the +// payload has been discarded from a previous call to Release. +func (p *payloadCloser) Rewind() bool { + p.mu.Lock() + defer p.mu.Unlock() + if p.payload == nil { + return false + } + if _, err := p.payload.Seek(0, io.SeekStart); err != nil { + return false + } + return true +} + +// Release discards the payload. After Release is called, the payload cannot be +// rewound and the payload is safe to reuse. +func (p *payloadCloser) Release() { + p.mu.Lock() + p.payload = nil + p.mu.Unlock() +} diff --git a/vendor/connectrpc.com/connect/envelope.go b/vendor/connectrpc.com/connect/envelope.go new file mode 100644 index 000000000000..21a897de9f67 --- /dev/null +++ b/vendor/connectrpc.com/connect/envelope.go @@ -0,0 +1,387 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "io" + "math" +) + +// flagEnvelopeCompressed indicates that the data is compressed. It has the +// same meaning in the gRPC-Web, gRPC-HTTP2, and Connect protocols. +const flagEnvelopeCompressed = 0b00000001 + +var errSpecialEnvelope = errorf( + CodeUnknown, + "final message has protocol-specific flags: %w", + // User code checks for end of stream with errors.Is(err, io.EOF). + io.EOF, +) + +// envelope is a block of arbitrary bytes wrapped in gRPC and Connect's framing +// protocol. +// +// Each message is preceded by a 5-byte prefix. The first byte is a uint8 used +// as a set of bitwise flags, and the remainder is a uint32 indicating the +// message length. gRPC and Connect interpret the bitwise flags differently, so +// envelope leaves their interpretation up to the caller. +type envelope struct { + Data *bytes.Buffer + Flags uint8 + offset int64 +} + +var _ messagePayload = (*envelope)(nil) + +func (e *envelope) IsSet(flag uint8) bool { + return e.Flags&flag == flag +} + +// Read implements [io.Reader]. +func (e *envelope) Read(data []byte) (readN int, err error) { + if e.offset < 5 { + prefix, err := makeEnvelopePrefix(e.Flags, e.Data.Len()) + if err != nil { + return 0, err + } + readN = copy(data, prefix[e.offset:]) + e.offset += int64(readN) + if e.offset < 5 { + return readN, nil + } + data = data[readN:] + } + n := copy(data, e.Data.Bytes()[e.offset-5:]) + e.offset += int64(n) + readN += n + if readN == 0 && e.offset == int64(e.Data.Len()+5) { + err = io.EOF + } + return readN, err +} + +// WriteTo implements [io.WriterTo]. +func (e *envelope) WriteTo(dst io.Writer) (wroteN int64, err error) { + if e.offset < 5 { + prefix, err := makeEnvelopePrefix(e.Flags, e.Data.Len()) + if err != nil { + return 0, err + } + prefixN, err := dst.Write(prefix[e.offset:]) + e.offset += int64(prefixN) + wroteN += int64(prefixN) + if e.offset < 5 { + return wroteN, err + } + } + n, err := dst.Write(e.Data.Bytes()[e.offset-5:]) + e.offset += int64(n) + wroteN += int64(n) + return wroteN, err +} + +// Seek implements [io.Seeker]. Based on the implementation of [bytes.Reader]. +func (e *envelope) Seek(offset int64, whence int) (int64, error) { + var abs int64 + switch whence { + case io.SeekStart: + abs = offset + case io.SeekCurrent: + abs = e.offset + offset + case io.SeekEnd: + abs = int64(e.Data.Len()) + offset + default: + return 0, errors.New("connect.envelope.Seek: invalid whence") + } + if abs < 0 { + return 0, errors.New("connect.envelope.Seek: negative position") + } + e.offset = abs + return abs, nil +} + +// Len returns the number of bytes of the unread portion of the envelope. +func (e *envelope) Len() int { + if length := int(int64(e.Data.Len()) + 5 - e.offset); length > 0 { + return length + } + return 0 +} + +type envelopeWriter struct { + ctx context.Context //nolint:containedctx + sender messageSender + codec Codec + compressMinBytes int + compressionPool *compressionPool + bufferPool *bufferPool + sendMaxBytes int +} + +func (w *envelopeWriter) Marshal(message any) *Error { + if message == nil { + // Send no-op message to create the request and send headers. + payload := nopPayload{} + if _, err := w.sender.Send(payload); err != nil { + if connectErr, ok := asError(err); ok { + return connectErr + } + return NewError(CodeUnknown, err) + } + return nil + } + if appender, ok := w.codec.(marshalAppender); ok { + return w.marshalAppend(message, appender) + } + return w.marshal(message) +} + +// Write writes the enveloped message, compressing as necessary. It doesn't +// retain any references to the supplied envelope or its underlying data. +func (w *envelopeWriter) Write(env *envelope) *Error { + if env.IsSet(flagEnvelopeCompressed) || + w.compressionPool == nil || + env.Data.Len() < w.compressMinBytes { + if w.sendMaxBytes > 0 && env.Data.Len() > w.sendMaxBytes { + return errorf(CodeResourceExhausted, "message size %d exceeds sendMaxBytes %d", env.Data.Len(), w.sendMaxBytes) + } + return w.write(env) + } + data := w.bufferPool.Get() + defer w.bufferPool.Put(data) + if err := w.compressionPool.Compress(data, env.Data); err != nil { + return err + } + if w.sendMaxBytes > 0 && data.Len() > w.sendMaxBytes { + return errorf(CodeResourceExhausted, "compressed message size %d exceeds sendMaxBytes %d", data.Len(), w.sendMaxBytes) + } + return w.write(&envelope{ + Data: data, + Flags: env.Flags | flagEnvelopeCompressed, + }) +} + +func (w *envelopeWriter) marshalAppend(message any, codec marshalAppender) *Error { + // Codec supports MarshalAppend; try to re-use a []byte from the pool. + buffer := w.bufferPool.Get() + defer w.bufferPool.Put(buffer) + raw, err := codec.MarshalAppend(buffer.Bytes(), message) + if err != nil { + return errorf(CodeInternal, "marshal message: %w", err) + } + if cap(raw) > buffer.Cap() { + // The buffer from the pool was too small, so MarshalAppend grew the slice. + // Pessimistically assume that the too-small buffer is insufficient for the + // application workload, so there's no point in keeping it in the pool. + // Instead, replace it with the larger, newly-allocated slice. This + // allocates, but it's a small, constant-size allocation. + *buffer = *bytes.NewBuffer(raw) + } else { + // MarshalAppend didn't allocate, but we need to fix the internal state of + // the buffer. Compared to replacing the buffer (as above), buffer.Write + // copies but avoids allocating. + buffer.Write(raw) + } + envelope := &envelope{Data: buffer} + return w.Write(envelope) +} + +func (w *envelopeWriter) marshal(message any) *Error { + // Codec doesn't support MarshalAppend; let Marshal allocate a []byte. + raw, err := w.codec.Marshal(message) + if err != nil { + return errorf(CodeInternal, "marshal message: %w", err) + } + buffer := bytes.NewBuffer(raw) + // Put our new []byte into the pool for later reuse. + defer w.bufferPool.Put(buffer) + envelope := &envelope{Data: buffer} + return w.Write(envelope) +} + +func (w *envelopeWriter) write(env *envelope) *Error { + if _, err := w.sender.Send(env); err != nil { + err = wrapIfContextDone(w.ctx, err) + if connectErr, ok := asError(err); ok { + return connectErr + } + return errorf(CodeUnknown, "write envelope: %w", err) + } + return nil +} + +type envelopeReader struct { + ctx context.Context //nolint:containedctx + reader io.Reader + bytesRead int64 // detect trailers-only gRPC responses + codec Codec + last envelope + compressionPool *compressionPool + bufferPool *bufferPool + readMaxBytes int +} + +func (r *envelopeReader) Unmarshal(message any) *Error { + buffer := r.bufferPool.Get() + var dontRelease *bytes.Buffer + defer func() { + if buffer != dontRelease { + r.bufferPool.Put(buffer) + } + }() + + env := &envelope{Data: buffer} + err := r.Read(env) + switch { + case err == nil && env.IsSet(flagEnvelopeCompressed) && r.compressionPool == nil: + return errorf( + CodeInternal, + "protocol error: sent compressed message without compression support", + ) + case err == nil && + (env.Flags == 0 || env.Flags == flagEnvelopeCompressed) && + env.Data.Len() == 0: + // This is a standard message (because none of the top 7 bits are set) and + // there's no data, so the zero value of the message is correct. + return nil + case err != nil && errors.Is(err, io.EOF): + // The stream has ended. Propagate the EOF to the caller. + return err + case err != nil: + // Something's wrong. + return err + } + + data := env.Data + if data.Len() > 0 && env.IsSet(flagEnvelopeCompressed) { + decompressed := r.bufferPool.Get() + defer func() { + if decompressed != dontRelease { + r.bufferPool.Put(decompressed) + } + }() + if err := r.compressionPool.Decompress(decompressed, data, int64(r.readMaxBytes)); err != nil { + return err + } + data = decompressed + } + + if env.Flags != 0 && env.Flags != flagEnvelopeCompressed { + // Drain the rest of the stream to ensure there is no extra data. + numBytes, err := discard(r.reader) + r.bytesRead += numBytes + if err != nil { + err = wrapIfContextError(err) + if connErr, ok := asError(err); ok { + return connErr + } + return errorf(CodeInternal, "corrupt response: I/O error after end-stream message: %w", err) + } else if numBytes > 0 { + return errorf(CodeInternal, "corrupt response: %d extra bytes after end of stream", numBytes) + } + // One of the protocol-specific flags are set, so this is the end of the + // stream. Save the message for protocol-specific code to process and + // return a sentinel error. We alias the buffer with dontRelease as a + // way of marking it so above defers don't release it to the pool. + r.last = envelope{ + Data: data, + Flags: env.Flags, + } + dontRelease = data + return errSpecialEnvelope + } + + if err := r.codec.Unmarshal(data.Bytes(), message); err != nil { + return errorf(CodeInvalidArgument, "unmarshal message: %w", err) + } + return nil +} + +func (r *envelopeReader) Read(env *envelope) *Error { + prefixes := [5]byte{} + // io.ReadFull reads the number of bytes requested, or returns an error. + // io.EOF will only be returned if no bytes were read. + n, err := io.ReadFull(r.reader, prefixes[:]) + r.bytesRead += int64(n) + if err != nil { + if errors.Is(err, io.EOF) { + // The stream ended cleanly. That's expected, but we need to propagate an EOF + // to the user so that they know that the stream has ended. We shouldn't + // add any alarming text about protocol errors, though. + return NewError(CodeUnknown, err) + } + err = wrapIfMaxBytesError(err, "read 5 byte message prefix") + err = wrapIfContextDone(r.ctx, err) + if connectErr, ok := asError(err); ok { + return connectErr + } + // Something else has gone wrong - the stream didn't end cleanly. + return errorf( + CodeInvalidArgument, + "protocol error: incomplete envelope: %w", err, + ) + } + size := int64(binary.BigEndian.Uint32(prefixes[1:5])) + if r.readMaxBytes > 0 && size > int64(r.readMaxBytes) { + n, err := io.CopyN(io.Discard, r.reader, size) + r.bytesRead += n + if err != nil && !errors.Is(err, io.EOF) { + return errorf(CodeResourceExhausted, "message is larger than configured max %d - unable to determine message size: %w", r.readMaxBytes, err) + } + return errorf(CodeResourceExhausted, "message size %d is larger than configured max %d", size, r.readMaxBytes) + } + // We've read the prefix, so we know how many bytes to expect. + // CopyN will return an error if it doesn't read the requested + // number of bytes. + readN, err := io.CopyN(env.Data, r.reader, size) + r.bytesRead += readN + if err != nil { + if errors.Is(err, io.EOF) { + // We've gotten fewer bytes than we expected, so the stream has ended + // unexpectedly. + return errorf( + CodeInvalidArgument, + "protocol error: promised %d bytes in enveloped message, got %d bytes", + size, + readN, + ) + } + err = wrapIfMaxBytesError(err, "read %d byte message", size) + err = wrapIfContextDone(r.ctx, err) + if connectErr, ok := asError(err); ok { + return connectErr + } + return errorf(CodeUnknown, "read enveloped message: %w", err) + } + env.Flags = prefixes[0] + return nil +} + +func makeEnvelopePrefix(flags uint8, size int) ([5]byte, error) { + // Cast as 64-bit to ensure comparison works on all architectures. + size64 := int64(size) + if size64 < 0 || size64 > math.MaxUint32 { + return [5]byte{}, fmt.Errorf("connect.makeEnvelopePrefix: size %d out of bounds", size) + } + prefix := [5]byte{} + prefix[0] = flags + binary.BigEndian.PutUint32(prefix[1:5], uint32(size64)) + return prefix, nil +} diff --git a/vendor/connectrpc.com/connect/error.go b/vendor/connectrpc.com/connect/error.go new file mode 100644 index 000000000000..65558065a426 --- /dev/null +++ b/vendor/connectrpc.com/connect/error.go @@ -0,0 +1,458 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "os" + "strings" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" +) + +const ( + commonErrorsURL = "https://connectrpc.com/docs/go/common-errors" + defaultAnyResolverPrefix = "type.googleapis.com/" +) + +var ( + // errNotModified signals Connect-protocol responses to GET requests to use the + // 304 Not Modified HTTP error code. + errNotModified = errors.New("not modified") + // errNotModifiedClient wraps ErrNotModified for use client-side. + errNotModifiedClient = fmt.Errorf("HTTP 304: %w", errNotModified) +) + +// An ErrorDetail is a self-describing Protobuf message attached to an [*Error]. +// Error details are sent over the network to clients, which can then work with +// strongly-typed data rather than trying to parse a complex error message. For +// example, you might use details to send a localized error message or retry +// parameters to the client. +// +// The [google.golang.org/genproto/googleapis/rpc/errdetails] package contains a +// variety of Protobuf messages commonly used as error details. +type ErrorDetail struct { + pbAny *anypb.Any + pbInner proto.Message // if nil, must be extracted from pbAny + wireJSON string // preserve human-readable JSON +} + +// NewErrorDetail constructs a new error detail. If msg is an *[anypb.Any] then +// it is used as is. Otherwise, it is first marshalled into an *[anypb.Any] +// value. This returns an error if msg cannot be marshalled. +func NewErrorDetail(msg proto.Message) (*ErrorDetail, error) { + // If it's already an Any, don't wrap it inside another. + if pb, ok := msg.(*anypb.Any); ok { + return &ErrorDetail{pbAny: pb}, nil + } + pb, err := anypb.New(msg) + if err != nil { + return nil, err + } + return &ErrorDetail{pbAny: pb, pbInner: msg}, nil +} + +// Type is the fully-qualified name of the detail's Protobuf message (for +// example, acme.foo.v1.FooDetail). +func (d *ErrorDetail) Type() string { + // proto.Any tries to make messages self-describing by using type URLs rather + // than plain type names, but there aren't any descriptor registries + // deployed. With the current state of the `Any` code, it's not possible to + // build a useful type registry either. To hide this from users, we should + // trim the URL prefix is added to the type name. + // + // If we ever want to support remote registries, we can add an explicit + // `TypeURL` method. + return typeNameForURL(d.pbAny.GetTypeUrl()) +} + +// Bytes returns a copy of the Protobuf-serialized detail. +func (d *ErrorDetail) Bytes() []byte { + out := make([]byte, len(d.pbAny.GetValue())) + copy(out, d.pbAny.GetValue()) + return out +} + +// Value uses the Protobuf runtime's package-global registry to unmarshal the +// Detail into a strongly-typed message. Typically, clients use Go type +// assertions to cast from the proto.Message interface to concrete types. +func (d *ErrorDetail) Value() (proto.Message, error) { + if d.pbInner != nil { + // We clone it so that if the caller mutates the returned value, + // they don't inadvertently corrupt this error detail value. + return proto.Clone(d.pbInner), nil + } + return d.pbAny.UnmarshalNew() +} + +// An Error captures four key pieces of information: a [Code], an underlying Go +// error, a map of metadata, and an optional collection of arbitrary Protobuf +// messages called "details" (more on those below). Servers send the code, the +// underlying error's Error() output, the metadata, and details over the wire +// to clients. Remember that the underlying error's message will be sent to +// clients - take care not to leak sensitive information from public APIs! +// +// Service implementations and interceptors should return errors that can be +// cast to an [*Error] (using the standard library's [errors.As]). If the returned +// error can't be cast to an [*Error], connect will use [CodeUnknown] and the +// returned error's message. +// +// Error details are an optional mechanism for servers, interceptors, and +// proxies to attach arbitrary Protobuf messages to the error code and message. +// They're a clearer and more performant alternative to HTTP header +// microformats. See [the documentation on errors] for more details. +// +// [the documentation on errors]: https://connectrpc.com/docs/go/errors +type Error struct { + code Code + err error + details []*ErrorDetail + meta http.Header + wireErr bool +} + +// NewError annotates any Go error with a status code. +func NewError(c Code, underlying error) *Error { + return &Error{code: c, err: underlying} +} + +// NewWireError is similar to [NewError], but the resulting *Error returns true +// when tested with [IsWireError]. +// +// This is useful for clients trying to propagate partial failures from +// streaming RPCs. Often, these RPCs include error information in their +// response messages (for example, [gRPC server reflection] and +// OpenTelemetry's [OTLP]). Clients propagating these errors up the stack +// should use NewWireError to clarify that the error code, message, and details +// (if any) were explicitly sent by the server rather than inferred from a +// lower-level networking error or timeout. +// +// [gRPC server reflection]: https://github.com/grpc/grpc/blob/v1.49.2/src/proto/grpc/reflection/v1alpha/reflection.proto#L132-L136 +// [OTLP]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md#partial-success +func NewWireError(c Code, underlying error) *Error { + err := NewError(c, underlying) + err.wireErr = true + return err +} + +// IsWireError checks whether the error was returned by the server, as opposed +// to being synthesized by the client. +// +// Clients may find this useful when deciding how to propagate errors. For +// example, an RPC-to-HTTP proxy might expose a server-sent CodeUnknown as an +// HTTP 500 but a client-synthesized CodeUnknown as a 503. +// +// Handlers will strip [Error.Meta] headers propagated from wire errors to avoid +// leaking response headers. To propagate headers recreate the error as a +// non-wire error. +func IsWireError(err error) bool { + se := new(Error) + if !errors.As(err, &se) { + return false + } + return se.wireErr +} + +// NewNotModifiedError indicates that the requested resource hasn't changed. It +// should be used only when handlers wish to respond to conditional HTTP GET +// requests with a 304 Not Modified. In all other circumstances, including all +// RPCs using the gRPC or gRPC-Web protocols, it's equivalent to sending an +// error with [CodeUnknown]. The supplied headers should include Etag, +// Cache-Control, or any other headers required by [RFC 9110 § 15.4.5]. +// +// Clients should check for this error using [IsNotModifiedError]. +// +// [RFC 9110 § 15.4.5]: https://httpwg.org/specs/rfc9110.html#status.304 +func NewNotModifiedError(headers http.Header) *Error { + err := NewError(CodeUnknown, errNotModified) + if headers != nil { + err.meta = headers + } + return err +} + +func (e *Error) Error() string { + message := e.Message() + if message == "" { + return e.code.String() + } + return e.code.String() + ": " + message +} + +// Message returns the underlying error message. It may be empty if the +// original error was created with a status code and a nil error. +func (e *Error) Message() string { + if e.err != nil { + return e.err.Error() + } + return "" +} + +// Unwrap allows [errors.Is] and [errors.As] access to the underlying error. +func (e *Error) Unwrap() error { + return e.err +} + +// Code returns the error's status code. +func (e *Error) Code() Code { + return e.code +} + +// Details returns the error's details. +func (e *Error) Details() []*ErrorDetail { + return e.details +} + +// AddDetail appends to the error's details. +func (e *Error) AddDetail(d *ErrorDetail) { + e.details = append(e.details, d) +} + +// Meta allows the error to carry additional information as key-value pairs. +// +// Protocol-specific headers and trailers may be removed to avoid breaking +// protocol semantics. For example, Content-Length and Content-Type headers +// won't be propagated. See the documentation for each protocol for more +// datails. +// +// When clients receive errors, the metadata contains the union of the HTTP +// headers and the protocol-specific trailers (either HTTP trailers or in-body +// metadata). +func (e *Error) Meta() http.Header { + if e.meta == nil { + e.meta = make(http.Header) + } + return e.meta +} + +func (e *Error) detailsAsAny() []*anypb.Any { + anys := make([]*anypb.Any, 0, len(e.details)) + for _, detail := range e.details { + anys = append(anys, detail.pbAny) + } + return anys +} + +// IsNotModifiedError checks whether the supplied error indicates that the +// requested resource hasn't changed. It only returns true if the server used +// [NewNotModifiedError] in response to a Connect-protocol RPC made with an +// HTTP GET. +func IsNotModifiedError(err error) bool { + return errors.Is(err, errNotModified) +} + +// errorf calls fmt.Errorf with the supplied template and arguments, then wraps +// the resulting error. +func errorf(c Code, template string, args ...any) *Error { + return NewError(c, fmt.Errorf(template, args...)) +} + +// asError uses errors.As to unwrap any error and look for a connect *Error. +func asError(err error) (*Error, bool) { + var connectErr *Error + ok := errors.As(err, &connectErr) + return connectErr, ok +} + +// wrapIfUncoded ensures that all errors are wrapped. It leaves already-wrapped +// errors unchanged, uses wrapIfContextError to apply codes to context.Canceled +// and context.DeadlineExceeded, and falls back to wrapping other errors with +// CodeUnknown. +func wrapIfUncoded(err error) error { + if err == nil { + return nil + } + maybeCodedErr := wrapIfContextError(err) + if _, ok := asError(maybeCodedErr); ok { + return maybeCodedErr + } + return NewError(CodeUnknown, maybeCodedErr) +} + +// wrapIfContextError applies CodeCanceled or CodeDeadlineExceeded to Go's +// context.Canceled and context.DeadlineExceeded errors, but only if they +// haven't already been wrapped. +func wrapIfContextError(err error) error { + if err == nil { + return nil + } + if _, ok := asError(err); ok { + return err + } + if errors.Is(err, context.Canceled) { + return NewError(CodeCanceled, err) + } + if errors.Is(err, context.DeadlineExceeded) { + return NewError(CodeDeadlineExceeded, err) + } + // Ick, some dial errors can be returned as os.ErrDeadlineExceeded + // instead of context.DeadlineExceeded :( + // https://github.com/golang/go/issues/64449 + if errors.Is(err, os.ErrDeadlineExceeded) { + return NewError(CodeDeadlineExceeded, err) + } + return err +} + +// wrapIfContextDone wraps errors with CodeCanceled or CodeDeadlineExceeded +// if the context is done. It leaves already-wrapped errors unchanged. +func wrapIfContextDone(ctx context.Context, err error) error { + if err == nil { + return nil + } + err = wrapIfContextError(err) + if _, ok := asError(err); ok { + return err + } + ctxErr := ctx.Err() + if errors.Is(ctxErr, context.Canceled) { + return NewError(CodeCanceled, err) + } else if errors.Is(ctxErr, context.DeadlineExceeded) { + return NewError(CodeDeadlineExceeded, err) + } + return err +} + +// wrapIfLikelyH2CNotConfiguredError adds a wrapping error that has a message +// telling the caller that they likely need to use h2c but are using a raw http.Client{}. +// +// This happens when running a gRPC-only server. +// This is fragile and may break over time, and this should be considered a best-effort. +func wrapIfLikelyH2CNotConfiguredError(request *http.Request, err error) error { + if err == nil { + return nil + } + if _, ok := asError(err); ok { + return err + } + if url := request.URL; url != nil && url.Scheme != "http" { + // If the scheme is not http, we definitely do not have an h2c error, so just return. + return err + } + // net/http code has been investigated and there is no typing of any of these errors + // they are all created with fmt.Errorf + // grpc-go returns the first error 2/3-3/4 of the time, and the second error 1/4-1/3 of the time + if errString := err.Error(); strings.HasPrefix(errString, `Post "`) && + (strings.Contains(errString, `net/http: HTTP/1.x transport connection broken: malformed HTTP response`) || + strings.HasSuffix(errString, `write: broken pipe`)) { + return fmt.Errorf("possible h2c configuration issue when talking to gRPC server, see %s: %w", commonErrorsURL, err) + } + return err +} + +// wrapIfLikelyWithGRPCNotUsedError adds a wrapping error that has a message +// telling the caller that they likely forgot to use connect.WithGRPC(). +// +// This happens when running a gRPC-only server. +// This is fragile and may break over time, and this should be considered a best-effort. +func wrapIfLikelyWithGRPCNotUsedError(err error) error { + if err == nil { + return nil + } + if _, ok := asError(err); ok { + return err + } + // golang.org/x/net code has been investigated and there is no typing of this error + // it is created with fmt.Errorf + // http2/transport.go:573: return nil, fmt.Errorf("http2: Transport: cannot retry err [%v] after Request.Body was written; define Request.GetBody to avoid this error", err) + if errString := err.Error(); strings.HasPrefix(errString, `Post "`) && + strings.Contains(errString, `http2: Transport: cannot retry err`) && + strings.HasSuffix(errString, `after Request.Body was written; define Request.GetBody to avoid this error`) { + return fmt.Errorf("possible missing connect.WithGPRC() client option when talking to gRPC server, see %s: %w", commonErrorsURL, err) + } + return err +} + +// HTTP/2 has its own set of error codes, which it sends in RST_STREAM frames. +// When the server sends one of these errors, we should map it back into our +// RPC error codes following +// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#http2-transport-mapping. +// +// This would be vastly simpler if we were using x/net/http2 directly, since +// the StreamError type is exported. When x/net/http2 gets vendored into +// net/http, though, all these types become unexported...so we're left with +// string munging. +func wrapIfRSTError(err error) error { + const ( + streamErrPrefix = "stream error: " + fromPeerSuffix = "; received from peer" + ) + if err == nil { + return nil + } + if _, ok := asError(err); ok { + return err + } + if urlErr := new(url.Error); errors.As(err, &urlErr) { + // If we get an RST_STREAM error from http.Client.Do, it's wrapped in a + // *url.Error. + err = urlErr.Unwrap() + } + msg := err.Error() + if !strings.HasPrefix(msg, streamErrPrefix) { + return err + } + if !strings.HasSuffix(msg, fromPeerSuffix) { + return err + } + msg = strings.TrimSuffix(msg, fromPeerSuffix) + i := strings.LastIndex(msg, ";") + if i < 0 || i >= len(msg)-1 { + return err + } + msg = msg[i+1:] + msg = strings.TrimSpace(msg) + switch msg { + case "NO_ERROR", "PROTOCOL_ERROR", "INTERNAL_ERROR", "FLOW_CONTROL_ERROR", + "SETTINGS_TIMEOUT", "FRAME_SIZE_ERROR", "COMPRESSION_ERROR", "CONNECT_ERROR": + return NewError(CodeInternal, err) + case "REFUSED_STREAM": + return NewError(CodeUnavailable, err) + case "CANCEL": + return NewError(CodeCanceled, err) + case "ENHANCE_YOUR_CALM": + return NewError(CodeResourceExhausted, fmt.Errorf("bandwidth exhausted: %w", err)) + case "INADEQUATE_SECURITY": + return NewError(CodePermissionDenied, fmt.Errorf("transport protocol insecure: %w", err)) + default: + return err + } +} + +// wrapIfMaxBytesError wraps errors returned reading from a http.MaxBytesHandler +// whose limit has been exceeded. +func wrapIfMaxBytesError(err error, tmpl string, args ...any) error { + if err == nil { + return nil + } + if _, ok := asError(err); ok { + return err + } + var maxBytesErr *http.MaxBytesError + if ok := errors.As(err, &maxBytesErr); !ok { + return err + } + prefix := fmt.Sprintf(tmpl, args...) + return errorf(CodeResourceExhausted, "%s: exceeded %d byte http.MaxBytesReader limit", prefix, maxBytesErr.Limit) +} + +func typeNameForURL(url string) string { + return url[strings.LastIndexByte(url, '/')+1:] +} diff --git a/vendor/connectrpc.com/connect/error_writer.go b/vendor/connectrpc.com/connect/error_writer.go new file mode 100644 index 000000000000..b8e33459e30f --- /dev/null +++ b/vendor/connectrpc.com/connect/error_writer.go @@ -0,0 +1,179 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" +) + +// protocolType is one of the supported RPC protocols. +type protocolType uint8 + +const ( + unknownProtocol protocolType = iota + connectUnaryProtocol + connectStreamProtocol + grpcProtocol + grpcWebProtocol +) + +// An ErrorWriter writes errors to an [http.ResponseWriter] in the format +// expected by an RPC client. This is especially useful in server-side net/http +// middleware, where you may wish to handle requests from RPC and non-RPC +// clients with the same code. +// +// ErrorWriters are safe to use concurrently. +type ErrorWriter struct { + bufferPool *bufferPool + protobuf Codec + requireConnectProtocolHeader bool +} + +// NewErrorWriter constructs an ErrorWriter. Handler options may be passed to +// configure the error writer behaviour to match the handlers. +// [WithRequireConnectProtocolHeader] will assert that Connect protocol +// requests include the version header allowing the error writer to correctly +// classify the request. +// Options supplied via [WithConditionalHandlerOptions] are ignored. +func NewErrorWriter(opts ...HandlerOption) *ErrorWriter { + config := newHandlerConfig("", StreamTypeUnary, opts) + codecs := newReadOnlyCodecs(config.Codecs) + return &ErrorWriter{ + bufferPool: config.BufferPool, + protobuf: codecs.Protobuf(), + requireConnectProtocolHeader: config.RequireConnectProtocolHeader, + } +} + +func (w *ErrorWriter) classifyRequest(request *http.Request) protocolType { + ctype := canonicalizeContentType(getHeaderCanonical(request.Header, headerContentType)) + isPost := request.Method == http.MethodPost + isGet := request.Method == http.MethodGet + switch { + case isPost && (ctype == grpcContentTypeDefault || strings.HasPrefix(ctype, grpcContentTypePrefix)): + return grpcProtocol + case isPost && (ctype == grpcWebContentTypeDefault || strings.HasPrefix(ctype, grpcWebContentTypePrefix)): + return grpcWebProtocol + case isPost && strings.HasPrefix(ctype, connectStreamingContentTypePrefix): + // Streaming ignores the requireConnectProtocolHeader option as the + // Content-Type is enough to determine the protocol. + if err := connectCheckProtocolVersion(request, false /* required */); err != nil { + return unknownProtocol + } + return connectStreamProtocol + case isPost && strings.HasPrefix(ctype, connectUnaryContentTypePrefix): + if err := connectCheckProtocolVersion(request, w.requireConnectProtocolHeader); err != nil { + return unknownProtocol + } + return connectUnaryProtocol + case isGet: + if err := connectCheckProtocolVersion(request, w.requireConnectProtocolHeader); err != nil { + return unknownProtocol + } + return connectUnaryProtocol + default: + return unknownProtocol + } +} + +// IsSupported checks whether a request is using one of the ErrorWriter's +// supported RPC protocols. +func (w *ErrorWriter) IsSupported(request *http.Request) bool { + return w.classifyRequest(request) != unknownProtocol +} + +// Write an error, using the format appropriate for the RPC protocol in use. +// Callers should first use IsSupported to verify that the request is using one +// of the ErrorWriter's supported RPC protocols. If the protocol is unknown, +// Write will send the error as unprefixed, Connect-formatted JSON. +// +// Write does not read or close the request body. +func (w *ErrorWriter) Write(response http.ResponseWriter, request *http.Request, err error) error { + ctype := canonicalizeContentType(getHeaderCanonical(request.Header, headerContentType)) + switch protocolType := w.classifyRequest(request); protocolType { + case connectStreamProtocol: + setHeaderCanonical(response.Header(), headerContentType, ctype) + return w.writeConnectStreaming(response, err) + case grpcProtocol: + setHeaderCanonical(response.Header(), headerContentType, ctype) + return w.writeGRPC(response, err) + case grpcWebProtocol: + setHeaderCanonical(response.Header(), headerContentType, ctype) + return w.writeGRPCWeb(response, err) + case unknownProtocol, connectUnaryProtocol: + fallthrough + default: + // Unary errors are always JSON. Unknown protocols are treated as unary + // because they are likely to be Connect clients and will still be able to + // parse the error as it's in a human-readable format. + setHeaderCanonical(response.Header(), headerContentType, connectUnaryContentTypeJSON) + return w.writeConnectUnary(response, err) + } +} + +func (w *ErrorWriter) writeConnectUnary(response http.ResponseWriter, err error) error { + if connectErr, ok := asError(err); ok && !connectErr.wireErr { + mergeNonProtocolHeaders(response.Header(), connectErr.meta) + } + response.WriteHeader(connectCodeToHTTP(CodeOf(err))) + data, marshalErr := json.Marshal(newConnectWireError(err)) + if marshalErr != nil { + return fmt.Errorf("marshal error: %w", marshalErr) + } + _, writeErr := response.Write(data) + return writeErr +} + +func (w *ErrorWriter) writeConnectStreaming(response http.ResponseWriter, err error) error { + response.WriteHeader(http.StatusOK) + marshaler := &connectStreamingMarshaler{ + envelopeWriter: envelopeWriter{ + sender: writeSender{writer: response}, + bufferPool: w.bufferPool, + }, + } + // MarshalEndStream returns *Error: check return value to avoid typed nils. + if marshalErr := marshaler.MarshalEndStream(err, make(http.Header)); marshalErr != nil { + return marshalErr + } + return nil +} + +func (w *ErrorWriter) writeGRPC(response http.ResponseWriter, err error) error { + trailers := make(http.Header, 2) // need space for at least code & message + grpcErrorToTrailer(trailers, w.protobuf, err) + // To make net/http reliably send trailers without a body, we must set the + // Trailers header rather than using http.TrailerPrefix. See + // https://github.com/golang/go/issues/54723. + keys := make([]string, 0, len(trailers)) + for k := range trailers { + keys = append(keys, k) + } + setHeaderCanonical(response.Header(), headerTrailer, strings.Join(keys, ",")) + response.WriteHeader(http.StatusOK) + mergeHeaders(response.Header(), trailers) + return nil +} + +func (w *ErrorWriter) writeGRPCWeb(response http.ResponseWriter, err error) error { + // This is a trailers-only response. To match the behavior of Envoy and + // protocol_grpc.go, put the trailers in the HTTP headers. + grpcErrorToTrailer(response.Header(), w.protobuf, err) + response.WriteHeader(http.StatusOK) + return nil +} diff --git a/vendor/connectrpc.com/connect/handler.go b/vendor/connectrpc.com/connect/handler.go new file mode 100644 index 000000000000..e33934ea2ebf --- /dev/null +++ b/vendor/connectrpc.com/connect/handler.go @@ -0,0 +1,423 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "context" + "net/http" +) + +// A Handler is the server-side implementation of a single RPC defined by a +// service schema. +// +// By default, Handlers support the Connect, gRPC, and gRPC-Web protocols with +// the binary Protobuf and JSON codecs. They support gzip compression using the +// standard library's [compress/gzip]. +type Handler struct { + spec Spec + implementation StreamingHandlerFunc + protocolHandlers map[string][]protocolHandler // Method to protocol handlers + allowMethod string // Allow header + acceptPost string // Accept-Post header +} + +// NewUnaryHandler constructs a [Handler] for a request-response procedure. +func NewUnaryHandler[Req, Res any]( + procedure string, + unary func(context.Context, *Request[Req]) (*Response[Res], error), + options ...HandlerOption, +) *Handler { + // Wrap the strongly-typed implementation so we can apply interceptors. + untyped := UnaryFunc(func(ctx context.Context, request AnyRequest) (AnyResponse, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + typed, ok := request.(*Request[Req]) + if !ok { + return nil, errorf(CodeInternal, "unexpected handler request type %T", request) + } + res, err := unary(ctx, typed) + if res == nil && err == nil { + // This is going to panic during serialization. Debugging is much easier + // if we panic here instead, so we can include the procedure name. + panic(procedure + " returned nil *connect.Response and nil error") //nolint: forbidigo + } + return res, err + }) + config := newHandlerConfig(procedure, StreamTypeUnary, options) + if interceptor := config.Interceptor; interceptor != nil { + untyped = interceptor.WrapUnary(untyped) + } + // Given a stream, how should we call the unary function? + implementation := func(ctx context.Context, conn StreamingHandlerConn) error { + request, err := receiveUnaryRequest[Req](conn, config.Initializer) + if err != nil { + return err + } + // Add the request header to the context, and store the response header + // and trailer to propagate back to the caller. + info := &handlerCallInfo{ + peer: request.Peer(), + spec: request.Spec(), + method: request.HTTPMethod(), + requestHeader: request.Header(), + } + ctx = newHandlerContext(ctx, info) + response, err := untyped(ctx, request) + // Add response headers/trailers from the context callinfo into the conn if they exist + if info.responseHeader != nil { + mergeNonProtocolHeaders(conn.ResponseHeader(), info.responseHeader) + } + if info.responseTrailer != nil { + mergeNonProtocolHeaders(conn.ResponseTrailer(), info.responseTrailer) + } + if err != nil { + return err + } + + // Add response headers/trailers from the response into the conn if they exist + if len(response.Header()) != 0 { + mergeNonProtocolHeaders(conn.ResponseHeader(), response.Header()) + } + if len(response.Trailer()) != 0 { + mergeNonProtocolHeaders(conn.ResponseTrailer(), response.Trailer()) + } + return conn.Send(response.Any()) + } + + protocolHandlers := config.newProtocolHandlers() + return &Handler{ + spec: config.newSpec(), + implementation: implementation, + protocolHandlers: mappedMethodHandlers(protocolHandlers), + allowMethod: sortedAllowMethodValue(protocolHandlers), + acceptPost: sortedAcceptPostValue(protocolHandlers), + } +} + +// NewUnaryHandlerSimple constructs a [Handler] for a request-response procedure using the +// function signature associated with the "simple" generation option. +// +// This option eliminates the [Request] and [Response] wrappers, and instead uses the +// context.Context to propagate information such as headers. +func NewUnaryHandlerSimple[Req, Res any]( + procedure string, + unary func(context.Context, *Req) (*Res, error), + options ...HandlerOption, +) *Handler { + return NewUnaryHandler( + procedure, + func(ctx context.Context, request *Request[Req]) (*Response[Res], error) { + responseMsg, err := unary(ctx, request.Msg) + if err != nil { + return nil, err + } + return NewResponse(responseMsg), nil + }, + options..., + ) +} + +// NewClientStreamHandler constructs a [Handler] for a client streaming procedure. +func NewClientStreamHandler[Req, Res any]( + procedure string, + implementation func(context.Context, *ClientStream[Req]) (*Response[Res], error), + options ...HandlerOption, +) *Handler { + config := newHandlerConfig(procedure, StreamTypeClient, options) + return newStreamHandler( + config, + func(ctx context.Context, conn StreamingHandlerConn) error { + stream := &ClientStream[Req]{ + conn: conn, + initializer: config.Initializer, + } + ctx = newHandlerContext(ctx, &streamingHandlerCallInfo{ + conn: conn, + }) + res, err := implementation(ctx, stream) + if err != nil { + return err + } + if res == nil { + // This is going to panic during serialization. Debugging is much easier + // if we panic here instead, so we can include the procedure name. + panic(procedure + " returned nil *connect.Response and nil error") //nolint: forbidigo + } + mergeHeaders(conn.ResponseHeader(), res.header) + mergeHeaders(conn.ResponseTrailer(), res.trailer) + return conn.Send(res.Msg) + }, + ) +} + +// NewClientStreamHandlerSimple constructs a [Handler] for a request-streaming procedure +// using the function signature associated with the "simple" generation option. +// +// This option eliminates the [Response] wrapper, and instead uses the context.Context +// to propagate information such as headers. +func NewClientStreamHandlerSimple[Req, Res any]( + procedure string, + implementation func(context.Context, *ClientStream[Req]) (*Res, error), + options ...HandlerOption, +) *Handler { + return NewClientStreamHandler( + procedure, + func(ctx context.Context, stream *ClientStream[Req]) (*Response[Res], error) { + responseMsg, err := implementation(ctx, stream) + if err != nil { + return nil, err + } + return NewResponse(responseMsg), nil + }, + options..., + ) +} + +// NewServerStreamHandler constructs a [Handler] for a server streaming procedure. +func NewServerStreamHandler[Req, Res any]( + procedure string, + implementation func(context.Context, *Request[Req], *ServerStream[Res]) error, + options ...HandlerOption, +) *Handler { + config := newHandlerConfig(procedure, StreamTypeServer, options) + return newStreamHandler( + config, + func(ctx context.Context, conn StreamingHandlerConn) error { + req, err := receiveUnaryRequest[Req](conn, config.Initializer) + if err != nil { + return err + } + ctx = newHandlerContext(ctx, &streamingHandlerCallInfo{ + conn: conn, + }) + return implementation(ctx, req, &ServerStream[Res]{conn: conn}) + }, + ) +} + +// NewServerStreamHandlerSimple constructs a [Handler] a server streaming procedure using the function +// signature associated with the "simple" generation option. +// +// This option eliminates the [Request] wrapper, and instead uses the context.Context to +// propagate information such as headers. +func NewServerStreamHandlerSimple[Req, Res any]( + procedure string, + implementation func(context.Context, *Req, *ServerStream[Res]) error, + options ...HandlerOption, +) *Handler { + return NewServerStreamHandler( + procedure, + func(ctx context.Context, request *Request[Req], serverStream *ServerStream[Res]) error { + return implementation(ctx, request.Msg, serverStream) + }, + options..., + ) +} + +// NewBidiStreamHandler constructs a [Handler] for a bidirectional streaming procedure. +func NewBidiStreamHandler[Req, Res any]( + procedure string, + implementation func(context.Context, *BidiStream[Req, Res]) error, + options ...HandlerOption, +) *Handler { + config := newHandlerConfig(procedure, StreamTypeBidi, options) + return newStreamHandler( + config, + func(ctx context.Context, conn StreamingHandlerConn) error { + ctx = newHandlerContext(ctx, &streamingHandlerCallInfo{ + conn: conn, + }) + return implementation( + ctx, + &BidiStream[Req, Res]{ + conn: conn, + initializer: config.Initializer, + }, + ) + }, + ) +} + +// ServeHTTP implements [http.Handler]. +func (h *Handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { + // We don't need to defer functions to close the request body or read to + // EOF: the stream we construct later on already does that, and we only + // return early when dealing with misbehaving clients. In those cases, it's + // okay if we can't re-use the connection. + isBidi := (h.spec.StreamType & StreamTypeBidi) == StreamTypeBidi + if isBidi && request.ProtoMajor < 2 { + // Clients coded to expect full-duplex connections may hang if they've + // mistakenly negotiated HTTP/1.1. To unblock them, we must close the + // underlying TCP connection. + responseWriter.Header().Set("Connection", "close") + responseWriter.WriteHeader(http.StatusHTTPVersionNotSupported) + return + } + + protocolHandlers := h.protocolHandlers[request.Method] + if len(protocolHandlers) == 0 { + responseWriter.Header().Set("Allow", h.allowMethod) + responseWriter.WriteHeader(http.StatusMethodNotAllowed) + return + } + + contentType := canonicalizeContentType(getHeaderCanonical(request.Header, headerContentType)) + + // Find our implementation of the RPC protocol in use. + var protocolHandler protocolHandler + for _, handler := range protocolHandlers { + if handler.CanHandlePayload(request, contentType) { + protocolHandler = handler + break + } + } + if protocolHandler == nil { + responseWriter.Header().Set("Accept-Post", h.acceptPost) + responseWriter.WriteHeader(http.StatusUnsupportedMediaType) + return + } + + if request.Method == http.MethodGet { + // A body must not be present. + hasBody := request.ContentLength > 0 + if request.ContentLength < 0 { + // No content-length header. + // Test if body is empty by trying to read a single byte. + var b [1]byte + n, _ := request.Body.Read(b[:]) + hasBody = n > 0 + } + if hasBody { + responseWriter.WriteHeader(http.StatusUnsupportedMediaType) + return + } + _ = request.Body.Close() + } + + // Establish a stream and serve the RPC. + setHeaderCanonical(request.Header, headerContentType, contentType) + setHeaderCanonical(request.Header, headerHost, request.Host) + ctx, cancel, timeoutErr := protocolHandler.SetTimeout(request) //nolint: contextcheck + if timeoutErr != nil { + ctx = request.Context() + } + if cancel != nil { + defer cancel() + } + connCloser, ok := protocolHandler.NewConn( + responseWriter, + request.WithContext(ctx), + ) + if !ok { + // Failed to create stream, usually because client used an unknown + // compression algorithm. Nothing further to do. + return + } + if timeoutErr != nil { + _ = connCloser.Close(timeoutErr) + return + } + _ = connCloser.Close(h.implementation(ctx, connCloser)) +} + +type handlerConfig struct { + CompressionPools map[string]*compressionPool + CompressionNames []string + Codecs map[string]Codec + CompressMinBytes int + Interceptor Interceptor + Procedure string + Schema any + Initializer maybeInitializer + RequireConnectProtocolHeader bool + IdempotencyLevel IdempotencyLevel + BufferPool *bufferPool + ReadMaxBytes int + SendMaxBytes int + StreamType StreamType +} + +func newHandlerConfig(procedure string, streamType StreamType, options []HandlerOption) *handlerConfig { + protoPath := extractProtoPath(procedure) + config := handlerConfig{ + Procedure: protoPath, + CompressionPools: make(map[string]*compressionPool), + Codecs: make(map[string]Codec), + BufferPool: newBufferPool(), + StreamType: streamType, + } + withProtoBinaryCodec().applyToHandler(&config) + withProtoJSONCodecs().applyToHandler(&config) + withGzip().applyToHandler(&config) + for _, opt := range options { + opt.applyToHandler(&config) + } + return &config +} + +func (c *handlerConfig) newSpec() Spec { + return Spec{ + Procedure: c.Procedure, + Schema: c.Schema, + StreamType: c.StreamType, + IdempotencyLevel: c.IdempotencyLevel, + } +} + +func (c *handlerConfig) newProtocolHandlers() []protocolHandler { + protocols := []protocol{ + &protocolConnect{}, + &protocolGRPC{web: false}, + &protocolGRPC{web: true}, + } + handlers := make([]protocolHandler, 0, len(protocols)) + codecs := newReadOnlyCodecs(c.Codecs) + compressors := newReadOnlyCompressionPools( + c.CompressionPools, + c.CompressionNames, + ) + for _, protocol := range protocols { + handlers = append(handlers, protocol.NewHandler(&protocolHandlerParams{ + Spec: c.newSpec(), + Codecs: codecs, + CompressionPools: compressors, + CompressMinBytes: c.CompressMinBytes, + BufferPool: c.BufferPool, + ReadMaxBytes: c.ReadMaxBytes, + SendMaxBytes: c.SendMaxBytes, + RequireConnectProtocolHeader: c.RequireConnectProtocolHeader, + IdempotencyLevel: c.IdempotencyLevel, + })) + } + return handlers +} + +func newStreamHandler( + config *handlerConfig, + implementation StreamingHandlerFunc, +) *Handler { + if ic := config.Interceptor; ic != nil { + implementation = ic.WrapStreamingHandler(implementation) + } + protocolHandlers := config.newProtocolHandlers() + return &Handler{ + spec: config.newSpec(), + implementation: implementation, + protocolHandlers: mappedMethodHandlers(protocolHandlers), + allowMethod: sortedAllowMethodValue(protocolHandlers), + acceptPost: sortedAcceptPostValue(protocolHandlers), + } +} diff --git a/vendor/connectrpc.com/connect/handler_stream.go b/vendor/connectrpc.com/connect/handler_stream.go new file mode 100644 index 000000000000..21092717b01d --- /dev/null +++ b/vendor/connectrpc.com/connect/handler_stream.go @@ -0,0 +1,198 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "errors" + "io" + "net/http" +) + +// ClientStream is the handler's view of a client streaming RPC. +// +// It's constructed as part of [Handler] invocation, but doesn't currently have +// an exported constructor. +type ClientStream[Req any] struct { + conn StreamingHandlerConn + initializer maybeInitializer + msg *Req + err error +} + +// Spec returns the specification for the RPC. +func (c *ClientStream[_]) Spec() Spec { + return c.conn.Spec() +} + +// Peer describes the client for this RPC. +func (c *ClientStream[_]) Peer() Peer { + return c.conn.Peer() +} + +// RequestHeader returns the headers received from the client. +func (c *ClientStream[Req]) RequestHeader() http.Header { + return c.conn.RequestHeader() +} + +// Receive advances the stream to the next message, which will then be +// available through the Msg method. It returns false when the stream stops, +// either by reaching the end or by encountering an unexpected error. After +// Receive returns false, the Err method will return any unexpected error +// encountered. +func (c *ClientStream[Req]) Receive() bool { + if c.err != nil { + return false + } + c.msg = new(Req) + if err := c.initializer.maybe(c.Spec(), c.msg); err != nil { + c.err = err + return false + } + c.err = c.conn.Receive(c.msg) + return c.err == nil +} + +// Msg returns the most recent message unmarshaled by a call to Receive. +func (c *ClientStream[Req]) Msg() *Req { + if c.msg == nil { + c.msg = new(Req) + } + return c.msg +} + +// Err returns the first non-EOF error that was encountered by Receive. +func (c *ClientStream[Req]) Err() error { + if c.err == nil || errors.Is(c.err, io.EOF) { + return nil + } + return c.err +} + +// Conn exposes the underlying StreamingHandlerConn. This may be useful if +// you'd prefer to wrap the connection in a different high-level API. +func (c *ClientStream[Req]) Conn() StreamingHandlerConn { + return c.conn +} + +// ServerStream is the handler's view of a server streaming RPC. +// +// It's constructed as part of [Handler] invocation, but doesn't currently have +// an exported constructor. +type ServerStream[Res any] struct { + conn StreamingHandlerConn +} + +// ResponseHeader returns the response headers. Headers are sent with the first +// call to Send. +// +// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. +func (s *ServerStream[Res]) ResponseHeader() http.Header { + return s.conn.ResponseHeader() +} + +// ResponseTrailer returns the response trailers. Handlers may write to the +// response trailers at any time before returning. +// +// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. +func (s *ServerStream[Res]) ResponseTrailer() http.Header { + return s.conn.ResponseTrailer() +} + +// Send a message to the client. The first call to Send also sends the response +// headers. +func (s *ServerStream[Res]) Send(msg *Res) error { + if msg == nil { + return s.conn.Send(nil) + } + return s.conn.Send(msg) +} + +// Conn exposes the underlying StreamingHandlerConn. This may be useful if +// you'd prefer to wrap the connection in a different high-level API. +func (s *ServerStream[Res]) Conn() StreamingHandlerConn { + return s.conn +} + +// BidiStream is the handler's view of a bidirectional streaming RPC. +// +// It's constructed as part of [Handler] invocation, but doesn't currently have +// an exported constructor. +type BidiStream[Req, Res any] struct { + conn StreamingHandlerConn + initializer maybeInitializer +} + +// Spec returns the specification for the RPC. +func (b *BidiStream[_, _]) Spec() Spec { + return b.conn.Spec() +} + +// Peer describes the client for this RPC. +func (b *BidiStream[_, _]) Peer() Peer { + return b.conn.Peer() +} + +// RequestHeader returns the headers received from the client. +func (b *BidiStream[Req, Res]) RequestHeader() http.Header { + return b.conn.RequestHeader() +} + +// Receive a message. When the client is done sending messages, Receive will +// return an error that wraps [io.EOF]. +func (b *BidiStream[Req, Res]) Receive() (*Req, error) { + var req Req + if err := b.initializer.maybe(b.Spec(), &req); err != nil { + return nil, err + } + if err := b.conn.Receive(&req); err != nil { + return nil, err + } + return &req, nil +} + +// ResponseHeader returns the response headers. Headers are sent with the first +// call to Send. +// +// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. +func (b *BidiStream[Req, Res]) ResponseHeader() http.Header { + return b.conn.ResponseHeader() +} + +// ResponseTrailer returns the response trailers. Handlers may write to the +// response trailers at any time before returning. +// +// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. +func (b *BidiStream[Req, Res]) ResponseTrailer() http.Header { + return b.conn.ResponseTrailer() +} + +// Send a message to the client. The first call to Send also sends the response +// headers. +func (b *BidiStream[Req, Res]) Send(msg *Res) error { + if msg == nil { + return b.conn.Send(nil) + } + return b.conn.Send(msg) +} + +// Conn exposes the underlying StreamingHandlerConn. This may be useful if +// you'd prefer to wrap the connection in a different high-level API. +func (b *BidiStream[Req, Res]) Conn() StreamingHandlerConn { + return b.conn +} diff --git a/vendor/connectrpc.com/connect/header.go b/vendor/connectrpc.com/connect/header.go new file mode 100644 index 000000000000..081570a5e342 --- /dev/null +++ b/vendor/connectrpc.com/connect/header.go @@ -0,0 +1,128 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "encoding/base64" + "net/http" +) + +//nolint:gochecknoglobals +var protocolHeaders = map[string]struct{}{ + // HTTP headers. + headerContentType: {}, + headerContentLength: {}, + headerContentEncoding: {}, + headerHost: {}, + headerUserAgent: {}, + headerTrailer: {}, + headerDate: {}, + // Connect headers. + connectUnaryHeaderAcceptCompression: {}, + connectUnaryTrailerPrefix: {}, + connectStreamingHeaderCompression: {}, + connectStreamingHeaderAcceptCompression: {}, + connectHeaderTimeout: {}, + connectHeaderProtocolVersion: {}, + // gRPC headers. + grpcHeaderCompression: {}, + grpcHeaderAcceptCompression: {}, + grpcHeaderTimeout: {}, + grpcHeaderStatus: {}, + grpcHeaderMessage: {}, + grpcHeaderDetails: {}, +} + +// EncodeBinaryHeader base64-encodes the data. It always emits unpadded values. +// +// In the Connect, gRPC, and gRPC-Web protocols, binary headers must have keys +// ending in "-Bin". +func EncodeBinaryHeader(data []byte) string { + // gRPC specification says that implementations should emit unpadded values. + return base64.RawStdEncoding.EncodeToString(data) +} + +// DecodeBinaryHeader base64-decodes the data. It can decode padded or unpadded +// values. Following usual HTTP semantics, multiple base64-encoded values may +// be joined with a comma. When receiving such comma-separated values, split +// them with [strings.Split] before calling DecodeBinaryHeader. +// +// Binary headers sent using the Connect, gRPC, and gRPC-Web protocols have +// keys ending in "-Bin". +func DecodeBinaryHeader(data string) ([]byte, error) { + if len(data)%4 != 0 { + // Data definitely isn't padded. + return base64.RawStdEncoding.DecodeString(data) + } + // Either the data was padded, or padding wasn't necessary. In both cases, + // the padding-aware decoder works. + return base64.StdEncoding.DecodeString(data) +} + +func mergeHeaders(into, from http.Header) { + for key, vals := range from { + if len(vals) == 0 { + // For response trailers, net/http will pre-populate entries + // with nil values based on the "Trailer" header. But if there + // are no actual values for those keys, we skip them. + continue + } + into[key] = append(into[key], vals...) + } +} + +// mergeNonProtocolHeaders merges headers excluding protocol headers defined in +// protocolHeaders. +func mergeNonProtocolHeaders(into, from http.Header) { + for key, vals := range from { + if len(vals) == 0 { + // For response trailers, net/http will pre-populate entries + // with nil values based on the "Trailer" header. But if there + // are no actual values for those keys, we skip them. + continue + } + if _, isProtocolHeader := protocolHeaders[key]; !isProtocolHeader { + into[key] = append(into[key], vals...) + } + } +} + +// getHeaderCanonical is a shortcut for Header.Get() which +// bypasses the CanonicalMIMEHeaderKey operation when we +// know the key is already in canonical form. +func getHeaderCanonical(h http.Header, key string) string { + if h == nil { + return "" + } + v := h[key] + if len(v) == 0 { + return "" + } + return v[0] +} + +// setHeaderCanonical is a shortcut for Header.Set() which +// bypasses the CanonicalMIMEHeaderKey operation when we +// know the key is already in canonical form. +func setHeaderCanonical(h http.Header, key, value string) { + h[key] = []string{value} +} + +// delHeaderCanonical is a shortcut for Header.Del() which +// bypasses the CanonicalMIMEHeaderKey operation when we +// know the key is already in canonical form. +func delHeaderCanonical(h http.Header, key string) { + delete(h, key) +} diff --git a/vendor/connectrpc.com/connect/idempotency_level.go b/vendor/connectrpc.com/connect/idempotency_level.go new file mode 100644 index 000000000000..5dbf8f531f7e --- /dev/null +++ b/vendor/connectrpc.com/connect/idempotency_level.go @@ -0,0 +1,68 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import "fmt" + +// An IdempotencyLevel is a value that declares how "idempotent" an RPC is. This +// value can affect RPC behaviors, such as determining whether it is safe to +// retry a request, or what kinds of request modalities are allowed for a given +// procedure. +type IdempotencyLevel int + +// NOTE: For simplicity, these should be kept in sync with the values of the +// google.protobuf.MethodOptions.IdempotencyLevel enumeration. + +const ( + // IdempotencyUnknown is the default idempotency level. A procedure with + // this idempotency level may not be idempotent. This is appropriate for + // any kind of procedure. + IdempotencyUnknown IdempotencyLevel = 0 + + // IdempotencyNoSideEffects is the idempotency level that specifies that a + // given call has no side-effects. This is equivalent to [RFC 9110 § 9.2.1] + // "safe" methods in terms of semantics. This procedure should not mutate + // any state. This idempotency level is appropriate for queries, or anything + // that would be suitable for an HTTP GET request. In addition, due to the + // lack of side-effects, such a procedure would be suitable to retry and + // expect that the results will not be altered by preceding attempts. + // + // [RFC 9110 § 9.2.1]: https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.1 + IdempotencyNoSideEffects IdempotencyLevel = 1 + + // IdempotencyIdempotent is the idempotency level that specifies that a + // given call is "idempotent", such that multiple instances of the same + // request to this procedure would have the same side-effects as a single + // request. This is equivalent to [RFC 9110 § 9.2.2] "idempotent" methods. + // This level is a subset of the previous level. This idempotency level is + // appropriate for any procedure that is safe to retry multiple times + // and be guaranteed that the response and side-effects will not be altered + // as a result of multiple attempts, for example, entity deletion requests. + // + // [RFC 9110 § 9.2.2]: https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2 + IdempotencyIdempotent IdempotencyLevel = 2 +) + +func (i IdempotencyLevel) String() string { + switch i { + case IdempotencyUnknown: + return "idempotency_unknown" + case IdempotencyNoSideEffects: + return "no_side_effects" + case IdempotencyIdempotent: + return "idempotent" + } + return fmt.Sprintf("idempotency_%d", i) +} diff --git a/vendor/connectrpc.com/connect/interceptor.go b/vendor/connectrpc.com/connect/interceptor.go new file mode 100644 index 000000000000..d3bc137488fb --- /dev/null +++ b/vendor/connectrpc.com/connect/interceptor.go @@ -0,0 +1,138 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "context" + "errors" +) + +var ( + // errNewClientContextProhibited signals that a new client context was created + // in an interceptor, which is prohibited. + errNewClientContextProhibited = errors.New("creating a new context in an interceptor is prohibited") +) + +// UnaryFunc is the generic signature of a unary RPC. Interceptors may wrap +// Funcs. +// +// The type of the request and response structs depend on the codec being used. +// When using Protobuf, request.Any() and response.Any() will always be +// [proto.Message] implementations. +type UnaryFunc func(context.Context, AnyRequest) (AnyResponse, error) + +// StreamingClientFunc is the generic signature of a streaming RPC from the client's +// perspective. Interceptors may wrap StreamingClientFuncs. +type StreamingClientFunc func(context.Context, Spec) StreamingClientConn + +// StreamingHandlerFunc is the generic signature of a streaming RPC from the +// handler's perspective. Interceptors may wrap StreamingHandlerFuncs. +type StreamingHandlerFunc func(context.Context, StreamingHandlerConn) error + +// An Interceptor adds logic to a generated handler or client, like the +// decorators or middleware you may have seen in other libraries. Interceptors +// may mutate requests and responses, handle errors, retry, recover from panics, +// emit logs and metrics, or do nearly anything else. +// +// The returned functions must be safe to call concurrently. +type Interceptor interface { + WrapUnary(UnaryFunc) UnaryFunc + WrapStreamingClient(StreamingClientFunc) StreamingClientFunc + WrapStreamingHandler(StreamingHandlerFunc) StreamingHandlerFunc +} + +// UnaryInterceptorFunc is a simple Interceptor implementation that only +// wraps unary RPCs. It has no effect on streaming RPCs. +type UnaryInterceptorFunc func(UnaryFunc) UnaryFunc + +// WrapUnary implements [Interceptor] by applying the interceptor function. +func (f UnaryInterceptorFunc) WrapUnary(next UnaryFunc) UnaryFunc { return f(next) } + +// WrapStreamingClient implements [Interceptor] with a no-op. +func (f UnaryInterceptorFunc) WrapStreamingClient(next StreamingClientFunc) StreamingClientFunc { + return next +} + +// WrapStreamingHandler implements [Interceptor] with a no-op. +func (f UnaryInterceptorFunc) WrapStreamingHandler(next StreamingHandlerFunc) StreamingHandlerFunc { + return next +} + +// A chain composes multiple interceptors into one. +type chain struct { + interceptors []Interceptor +} + +// newChain composes multiple interceptors into one. +func newChain(interceptors []Interceptor) *chain { + // We usually wrap in reverse order to have the first interceptor from + // the slice act first. Rather than doing this dance repeatedly, reverse the + // interceptor order now. + var chain chain + for i := len(interceptors) - 1; i >= 0; i-- { + if interceptor := interceptors[i]; interceptor != nil { + chain.interceptors = append(chain.interceptors, interceptor) + } + } + return &chain +} + +func (c *chain) WrapUnary(next UnaryFunc) UnaryFunc { + for _, interceptor := range c.interceptors { + next = unaryThunk(next) + next = interceptor.WrapUnary(next) + } + return next +} + +func (c *chain) WrapStreamingClient(next StreamingClientFunc) StreamingClientFunc { + for _, interceptor := range c.interceptors { + next = streamingClientThunk(next) + next = interceptor.WrapStreamingClient(next) + } + return next +} + +func (c *chain) WrapStreamingHandler(next StreamingHandlerFunc) StreamingHandlerFunc { + for _, interceptor := range c.interceptors { + next = interceptor.WrapStreamingHandler(next) + } + return next +} + +func unaryThunk(next UnaryFunc) UnaryFunc { + return func(ctx context.Context, req AnyRequest) (AnyResponse, error) { + if err := checkSentinel(ctx); err != nil { + return nil, err + } + return next(ctx, req) + } +} + +func streamingClientThunk(next StreamingClientFunc) StreamingClientFunc { + return func(ctx context.Context, spec Spec) StreamingClientConn { + if err := checkSentinel(ctx); err != nil { + return &errStreamingClientConn{err: err} + } + return next(ctx, spec) + } +} + +func checkSentinel(ctx context.Context) error { + if ctx.Value(clientCallInfoContextKey{}) != ctx.Value(sentinelContextKey{}) { + return errNewClientContextProhibited + } + return nil +} diff --git a/vendor/connectrpc.com/connect/internal/gen/connectext/grpc/status/v1/status.pb.go b/vendor/connectrpc.com/connect/internal/gen/connectext/grpc/status/v1/status.pb.go new file mode 100644 index 000000000000..10dea4741050 --- /dev/null +++ b/vendor/connectrpc.com/connect/internal/gen/connectext/grpc/status/v1/status.pb.go @@ -0,0 +1,165 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.9 +// protoc (unknown) +// source: connectext/grpc/status/v1/status.proto + +// This package is for internal use by Connect, and provides no backward +// compatibility guarantees whatsoever. + +package statusv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + anypb "google.golang.org/protobuf/types/known/anypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// See https://cloud.google.com/apis/design/errors. +// +// This struct must remain binary-compatible with +// https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto. +type Status struct { + state protoimpl.MessageState `protogen:"open.v1"` + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` // a google.rpc.Code + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // developer-facing, English (localize in details or client-side) + Details []*anypb.Any `protobuf:"bytes,3,rep,name=details,proto3" json:"details,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Status) Reset() { + *x = Status{} + mi := &file_connectext_grpc_status_v1_status_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Status) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Status) ProtoMessage() {} + +func (x *Status) ProtoReflect() protoreflect.Message { + mi := &file_connectext_grpc_status_v1_status_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Status.ProtoReflect.Descriptor instead. +func (*Status) Descriptor() ([]byte, []int) { + return file_connectext_grpc_status_v1_status_proto_rawDescGZIP(), []int{0} +} + +func (x *Status) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *Status) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *Status) GetDetails() []*anypb.Any { + if x != nil { + return x.Details + } + return nil +} + +var File_connectext_grpc_status_v1_status_proto protoreflect.FileDescriptor + +const file_connectext_grpc_status_v1_status_proto_rawDesc = "" + + "\n" + + "&connectext/grpc/status/v1/status.proto\x12\x0egrpc.status.v1\x1a\x19google/protobuf/any.proto\"f\n" + + "\x06Status\x12\x12\n" + + "\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12.\n" + + "\adetails\x18\x03 \x03(\v2\x14.google.protobuf.AnyR\adetailsB\xc3\x01\n" + + "\x12com.grpc.status.v1B\vStatusProtoP\x01ZFconnectrpc.com/connect/internal/gen/connectext/grpc/status/v1;statusv1\xa2\x02\x03GSX\xaa\x02\x0eGrpc.Status.V1\xca\x02\x0eGrpc\\Status\\V1\xe2\x02\x1aGrpc\\Status\\V1\\GPBMetadata\xea\x02\x10Grpc::Status::V1b\x06proto3" + +var ( + file_connectext_grpc_status_v1_status_proto_rawDescOnce sync.Once + file_connectext_grpc_status_v1_status_proto_rawDescData []byte +) + +func file_connectext_grpc_status_v1_status_proto_rawDescGZIP() []byte { + file_connectext_grpc_status_v1_status_proto_rawDescOnce.Do(func() { + file_connectext_grpc_status_v1_status_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_connectext_grpc_status_v1_status_proto_rawDesc), len(file_connectext_grpc_status_v1_status_proto_rawDesc))) + }) + return file_connectext_grpc_status_v1_status_proto_rawDescData +} + +var file_connectext_grpc_status_v1_status_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_connectext_grpc_status_v1_status_proto_goTypes = []any{ + (*Status)(nil), // 0: grpc.status.v1.Status + (*anypb.Any)(nil), // 1: google.protobuf.Any +} +var file_connectext_grpc_status_v1_status_proto_depIdxs = []int32{ + 1, // 0: grpc.status.v1.Status.details:type_name -> google.protobuf.Any + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_connectext_grpc_status_v1_status_proto_init() } +func file_connectext_grpc_status_v1_status_proto_init() { + if File_connectext_grpc_status_v1_status_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_connectext_grpc_status_v1_status_proto_rawDesc), len(file_connectext_grpc_status_v1_status_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_connectext_grpc_status_v1_status_proto_goTypes, + DependencyIndexes: file_connectext_grpc_status_v1_status_proto_depIdxs, + MessageInfos: file_connectext_grpc_status_v1_status_proto_msgTypes, + }.Build() + File_connectext_grpc_status_v1_status_proto = out.File + file_connectext_grpc_status_v1_status_proto_goTypes = nil + file_connectext_grpc_status_v1_status_proto_depIdxs = nil +} diff --git a/vendor/connectrpc.com/connect/option.go b/vendor/connectrpc.com/connect/option.go new file mode 100644 index 000000000000..fe0a2cd9d21e --- /dev/null +++ b/vendor/connectrpc.com/connect/option.go @@ -0,0 +1,647 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "compress/gzip" + "context" + "io" + "net/http" +) + +// A ClientOption configures a [Client]. +// +// In addition to any options grouped in the documentation below, remember that +// any [Option] is also a valid ClientOption. +type ClientOption interface { + applyToClient(*clientConfig) +} + +// WithAcceptCompression makes a compression algorithm available to a client. +// Clients ask servers to compress responses using any of the registered +// algorithms. The first registered algorithm is treated as the least +// preferred, and the last registered algorithm is the most preferred. +// +// It's safe to use this option liberally: servers will ignore any +// compression algorithms they don't support. To compress requests, pair this +// option with [WithSendCompression]. To remove support for a +// previously-registered compression algorithm, use WithAcceptCompression with +// nil decompressor and compressor constructors. +// +// Clients accept gzipped responses by default, using a compressor backed by the +// standard library's [gzip] package with the default compression level. Use +// [WithSendGzip] to compress requests with gzip. +// +// Calling WithAcceptCompression with an empty name is a no-op. +func WithAcceptCompression( + name string, + newDecompressor func() Decompressor, + newCompressor func() Compressor, +) ClientOption { + return &compressionOption{ + Name: name, + CompressionPool: newCompressionPool(newDecompressor, newCompressor), + } +} + +// WithClientOptions composes multiple ClientOptions into one. +func WithClientOptions(options ...ClientOption) ClientOption { + return &clientOptionsOption{options} +} + +// WithGRPC configures clients to use the HTTP/2 gRPC protocol. +func WithGRPC() ClientOption { + return &grpcOption{web: false} +} + +// WithGRPCWeb configures clients to use the gRPC-Web protocol. +func WithGRPCWeb() ClientOption { + return &grpcOption{web: true} +} + +// WithProtoJSON configures a client to send JSON-encoded data instead of +// binary Protobuf. It uses the standard Protobuf JSON mapping as implemented +// by [google.golang.org/protobuf/encoding/protojson]: fields are named using +// lowerCamelCase, zero values are omitted, missing required fields are errors, +// enums are emitted as strings, etc. +func WithProtoJSON() ClientOption { + return WithCodec(&protoJSONCodec{codecNameJSON}) +} + +// WithSendCompression configures the client to use the specified algorithm to +// compress request messages. If the algorithm has not been registered using +// [WithAcceptCompression], the client will return errors at runtime. +// +// Because some servers don't support compression, clients default to sending +// uncompressed requests. +func WithSendCompression(name string) ClientOption { + return &sendCompressionOption{Name: name} +} + +// WithSendGzip configures the client to gzip requests. Since clients have +// access to a gzip compressor by default, WithSendGzip doesn't require +// [WithSendCompression]. +// +// Some servers don't support gzip, so clients default to sending uncompressed +// requests. +func WithSendGzip() ClientOption { + return WithSendCompression(compressionGzip) +} + +// A HandlerOption configures a [Handler]. +// +// In addition to any options grouped in the documentation below, remember that +// any [Option] is also a HandlerOption. +type HandlerOption interface { + applyToHandler(*handlerConfig) +} + +// WithCompression configures handlers to support a compression algorithm. +// Clients may send messages compressed with that algorithm and/or request +// compressed responses. The [Compressor] and [Decompressor] produced by the +// supplied constructors must use the same algorithm. Internally, Connect pools +// compressors and decompressors. +// +// By default, handlers support gzip using the standard library's +// [compress/gzip] package at the default compression level. To remove support for +// a previously-registered compression algorithm, use WithCompression with nil +// decompressor and compressor constructors. +// +// Calling WithCompression with an empty name is a no-op. +func WithCompression( + name string, + newDecompressor func() Decompressor, + newCompressor func() Compressor, +) HandlerOption { + return &compressionOption{ + Name: name, + CompressionPool: newCompressionPool(newDecompressor, newCompressor), + } +} + +// WithHandlerOptions composes multiple HandlerOptions into one. +func WithHandlerOptions(options ...HandlerOption) HandlerOption { + return &handlerOptionsOption{options} +} + +// WithRecover adds an interceptor that recovers from panics. The supplied +// function receives the context, [Spec], request headers, and the recovered +// value (which may be nil). It must return an error to send back to the +// client. It may also log the panic, emit metrics, or execute other +// error-handling logic. Handler functions must be safe to call concurrently. +// +// To preserve compatibility with [net/http]'s semantics, this interceptor +// doesn't handle panics with [http.ErrAbortHandler]. +// +// By default, handlers don't recover from panics. Because the standard +// library's [http.Server] recovers from panics by default, this option isn't +// usually necessary to prevent crashes. Instead, it helps servers collect +// RPC-specific data during panics and send a more detailed error to +// clients. +func WithRecover(handle func(context.Context, Spec, http.Header, any) error) HandlerOption { + return WithInterceptors(&recoverHandlerInterceptor{handle: handle}) +} + +// WithRequireConnectProtocolHeader configures the Handler to require requests +// using the Connect RPC protocol to include the Connect-Protocol-Version +// header. This ensures that HTTP proxies and net/http middleware can easily +// identify valid Connect requests, even if they use a common Content-Type like +// application/json. However, it makes ad-hoc requests with tools like cURL +// more laborious. Streaming requests are not affected by this option. +// +// This option has no effect if the client uses the gRPC or gRPC-Web protocols. +func WithRequireConnectProtocolHeader() HandlerOption { + return &requireConnectProtocolHeaderOption{} +} + +// WithConditionalHandlerOptions allows procedures in the same service to have +// different configurations: for example, one procedure may need a much larger +// WithReadMaxBytes setting than the others. +// +// WithConditionalHandlerOptions takes a function which may inspect each +// procedure's Spec before deciding which options to apply. Returning a nil +// slice is safe. +func WithConditionalHandlerOptions(conditional func(spec Spec) []HandlerOption) HandlerOption { + return &conditionalHandlerOptions{conditional: conditional} +} + +// Option implements both [ClientOption] and [HandlerOption], so it can be +// applied both client-side and server-side. +type Option interface { + ClientOption + HandlerOption +} + +// WithSchema provides a parsed representation of the schema for an RPC to a +// client or handler. The supplied schema is exposed as [Spec].Schema. This +// option is typically added by generated code. +// +// For services using protobuf schemas, the supplied schema should be a +// [google.golang.org/protobuf/reflect/protoreflect.MethodDescriptor]. +func WithSchema(schema any) Option { + return &schemaOption{Schema: schema} +} + +// WithRequestInitializer provides a function that initializes a new message. +// It may be used to dynamically construct request messages. It is called on +// server receives to construct the message to be unmarshaled into. The message +// will be a non nil pointer to the type created by the handler. Use the Schema +// field of the [Spec] to determine the type of the message. +func WithRequestInitializer(initializer func(spec Spec, message any) error) HandlerOption { + return &initializerOption{Initializer: initializer} +} + +// WithResponseInitializer provides a function that initializes a new message. +// It may be used to dynamically construct response messages. It is called on +// client receives to construct the message to be unmarshaled into. The message +// will be a non nil pointer to the type created by the client. Use the Schema +// field of the [Spec] to determine the type of the message. +func WithResponseInitializer(initializer func(spec Spec, message any) error) ClientOption { + return &initializerOption{Initializer: initializer} +} + +// WithCodec registers a serialization method with a client or handler. +// Handlers may have multiple codecs registered, and use whichever the client +// chooses. Clients may only have a single codec. +// +// By default, handlers and clients support binary Protocol Buffer data using +// [google.golang.org/protobuf/proto]. Handlers also support JSON by default, +// using the standard Protobuf JSON mapping. Users with more specialized needs +// may override the default codecs by registering a new codec under the "proto" +// or "json" names. When supplying a custom "proto" codec, keep in mind that +// some unexported, protocol-specific messages are serialized using Protobuf - +// take care to fall back to the standard Protobuf implementation if +// necessary. +// +// Registering a codec with an empty name is a no-op. +func WithCodec(codec Codec) Option { + return &codecOption{Codec: codec} +} + +// WithCompressMinBytes sets a minimum size threshold for compression: +// regardless of compressor configuration, messages smaller than the configured +// minimum are sent uncompressed. +// +// The default minimum is zero. Setting a minimum compression threshold may +// improve overall performance, because the CPU cost of compressing very small +// messages usually isn't worth the small reduction in network I/O. +func WithCompressMinBytes(minBytes int) Option { + return &compressMinBytesOption{Min: minBytes} +} + +// WithReadMaxBytes limits the performance impact of pathologically large +// messages sent by the other party. For handlers, WithReadMaxBytes limits the size +// of a message that the client can send. For clients, WithReadMaxBytes limits the +// size of a message that the server can respond with. Limits apply to each Protobuf +// message, not to the stream as a whole. +// +// Setting WithReadMaxBytes to zero allows any message size. Both clients and +// handlers default to allowing any request size. +// +// Handlers may also use [http.MaxBytesHandler] to limit the total size of the +// HTTP request stream (rather than the per-message size). Connect handles +// [http.MaxBytesError] specially, so clients still receive errors with the +// appropriate error code and informative messages. +func WithReadMaxBytes(maxBytes int) Option { + return &readMaxBytesOption{Max: maxBytes} +} + +// WithSendMaxBytes prevents sending messages too large for the client/handler +// to handle without significant performance overhead. For handlers, WithSendMaxBytes +// limits the size of a message that the handler can respond with. For clients, +// WithSendMaxBytes limits the size of a message that the client can send. Limits +// apply to each message, not to the stream as a whole. +// +// Setting WithSendMaxBytes to zero allows any message size. Both clients and +// handlers default to allowing any message size. +func WithSendMaxBytes(maxBytes int) Option { + return &sendMaxBytesOption{Max: maxBytes} +} + +// WithIdempotency declares the idempotency of the procedure. This can determine +// whether a procedure call can safely be retried, and may affect which request +// modalities are allowed for a given procedure call. +// +// In most cases, you should not need to manually set this. It is normally set +// by the code generator for your schema. For protobuf schemas, it can be set like this: +// +// rpc Ping(PingRequest) returns (PingResponse) { +// option idempotency_level = NO_SIDE_EFFECTS; +// } +func WithIdempotency(idempotencyLevel IdempotencyLevel) Option { + return &idempotencyOption{idempotencyLevel: idempotencyLevel} +} + +// WithHTTPGet allows Connect-protocol clients to use HTTP GET requests for +// side-effect free unary RPC calls. Typically, the service schema indicates +// which procedures are idempotent (see [WithIdempotency] for an example +// protobuf schema). The gRPC and gRPC-Web protocols are POST-only, so this +// option has no effect when combined with [WithGRPC] or [WithGRPCWeb]. +// +// Using HTTP GET requests makes it easier to take advantage of CDNs, caching +// reverse proxies, and browsers' built-in caching. Note, however, that servers +// don't automatically set any cache headers; you can set cache headers using +// interceptors or by adding headers in individual procedure implementations. +// +// By default, all requests are made as HTTP POSTs. +func WithHTTPGet() ClientOption { + return &enableGet{} +} + +// WithInterceptors configures a client or handler's interceptor stack. Repeated +// WithInterceptors options are applied in order, so +// +// WithInterceptors(A) + WithInterceptors(B, C) == WithInterceptors(A, B, C) +// +// Unary interceptors compose like an onion. The first interceptor provided is +// the outermost layer of the onion: it acts first on the context and request, +// and last on the response and error. +// +// Stream interceptors also behave like an onion: the first interceptor +// provided is the outermost wrapper for the [StreamingClientConn] or +// [StreamingHandlerConn]. It's the first to see sent messages and the last to +// see received messages. +// +// Applied to client and handler, WithInterceptors(A, B, ..., Y, Z) produces: +// +// client.Send() client.Receive() +// | ^ +// v | +// A --- --- A +// B --- --- B +// : ... ... : +// Y --- --- Y +// Z --- --- Z +// | ^ +// v | +// = = = = = = = = = = = = = = = = +// network +// = = = = = = = = = = = = = = = = +// | ^ +// v | +// A --- --- A +// B --- --- B +// : ... ... : +// Y --- --- Y +// Z --- --- Z +// | ^ +// v | +// handler.Receive() handler.Send() +// | ^ +// | | +// '-> handler logic >-' +// +// Note that in clients, Send handles the request message(s) and Receive +// handles the response message(s). For handlers, it's the reverse. Depending +// on your interceptor's logic, you may need to wrap one method in clients and +// the other in handlers. +func WithInterceptors(interceptors ...Interceptor) Option { + return &interceptorsOption{interceptors} +} + +// WithOptions composes multiple Options into one. +func WithOptions(options ...Option) Option { + return &optionsOption{options} +} + +type schemaOption struct { + Schema any +} + +func (o *schemaOption) applyToClient(config *clientConfig) { + config.Schema = o.Schema +} + +func (o *schemaOption) applyToHandler(config *handlerConfig) { + config.Schema = o.Schema +} + +type initializerOption struct { + Initializer func(spec Spec, message any) error +} + +func (o *initializerOption) applyToHandler(config *handlerConfig) { + config.Initializer = maybeInitializer{initializer: o.Initializer} +} + +func (o *initializerOption) applyToClient(config *clientConfig) { + config.Initializer = maybeInitializer{initializer: o.Initializer} +} + +type maybeInitializer struct { + initializer func(spec Spec, message any) error +} + +func (o maybeInitializer) maybe(spec Spec, message any) error { + if o.initializer != nil { + return o.initializer(spec, message) + } + return nil +} + +type clientOptionsOption struct { + options []ClientOption +} + +func (o *clientOptionsOption) applyToClient(config *clientConfig) { + for _, option := range o.options { + option.applyToClient(config) + } +} + +type codecOption struct { + Codec Codec +} + +func (o *codecOption) applyToClient(config *clientConfig) { + if o.Codec == nil || o.Codec.Name() == "" { + return + } + config.Codec = o.Codec +} + +func (o *codecOption) applyToHandler(config *handlerConfig) { + if o.Codec == nil || o.Codec.Name() == "" { + return + } + config.Codecs[o.Codec.Name()] = o.Codec +} + +type compressionOption struct { + Name string + CompressionPool *compressionPool +} + +func (o *compressionOption) applyToClient(config *clientConfig) { + o.apply(&config.CompressionNames, config.CompressionPools) +} + +func (o *compressionOption) applyToHandler(config *handlerConfig) { + o.apply(&config.CompressionNames, config.CompressionPools) +} + +func (o *compressionOption) apply(configuredNames *[]string, configuredPools map[string]*compressionPool) { + if o.Name == "" { + return + } + if o.CompressionPool == nil { + delete(configuredPools, o.Name) + var names []string + for _, name := range *configuredNames { + if name == o.Name { + continue + } + names = append(names, name) + } + *configuredNames = names + return + } + configuredPools[o.Name] = o.CompressionPool + *configuredNames = append(*configuredNames, o.Name) +} + +type compressMinBytesOption struct { + Min int +} + +func (o *compressMinBytesOption) applyToClient(config *clientConfig) { + config.CompressMinBytes = o.Min +} + +func (o *compressMinBytesOption) applyToHandler(config *handlerConfig) { + config.CompressMinBytes = o.Min +} + +type readMaxBytesOption struct { + Max int +} + +func (o *readMaxBytesOption) applyToClient(config *clientConfig) { + config.ReadMaxBytes = o.Max +} + +func (o *readMaxBytesOption) applyToHandler(config *handlerConfig) { + config.ReadMaxBytes = o.Max +} + +type sendMaxBytesOption struct { + Max int +} + +func (o *sendMaxBytesOption) applyToClient(config *clientConfig) { + config.SendMaxBytes = o.Max +} + +func (o *sendMaxBytesOption) applyToHandler(config *handlerConfig) { + config.SendMaxBytes = o.Max +} + +type handlerOptionsOption struct { + options []HandlerOption +} + +func (o *handlerOptionsOption) applyToHandler(config *handlerConfig) { + for _, option := range o.options { + option.applyToHandler(config) + } +} + +type requireConnectProtocolHeaderOption struct{} + +func (o *requireConnectProtocolHeaderOption) applyToHandler(config *handlerConfig) { + config.RequireConnectProtocolHeader = true +} + +type idempotencyOption struct { + idempotencyLevel IdempotencyLevel +} + +func (o *idempotencyOption) applyToClient(config *clientConfig) { + config.IdempotencyLevel = o.idempotencyLevel +} + +func (o *idempotencyOption) applyToHandler(config *handlerConfig) { + config.IdempotencyLevel = o.idempotencyLevel +} + +type grpcOption struct { + web bool +} + +func (o *grpcOption) applyToClient(config *clientConfig) { + config.Protocol = &protocolGRPC{web: o.web} +} + +type enableGet struct{} + +func (o *enableGet) applyToClient(config *clientConfig) { + config.EnableGet = true +} + +// WithHTTPGetMaxURLSize sets the maximum allowable URL length for GET requests +// made using the Connect protocol. It has no effect on gRPC or gRPC-Web +// clients, since those protocols are POST-only. +// +// Limiting the URL size is useful as most user agents, proxies, and servers +// have limits on the allowable length of a URL. For example, Apache and Nginx +// limit the size of a request line to around 8 KiB, meaning that maximum +// length of a URL is a bit smaller than this. If you run into URL size +// limitations imposed by your network infrastructure and don't know the +// maximum allowable size, or if you'd prefer to be cautious from the start, a +// 4096 byte (4 KiB) limit works with most common proxies and CDNs. +// +// If fallback is set to true and the URL would be longer than the configured +// maximum value, the request will be sent as an HTTP POST instead. If fallback +// is set to false, the request will fail with [CodeResourceExhausted]. +// +// By default, Connect-protocol clients with GET requests enabled may send a +// URL of any size. +func WithHTTPGetMaxURLSize(bytes int, fallback bool) ClientOption { + return &getURLMaxBytes{Max: bytes, Fallback: fallback} +} + +type getURLMaxBytes struct { + Max int + Fallback bool +} + +func (o *getURLMaxBytes) applyToClient(config *clientConfig) { + config.GetURLMaxBytes = o.Max + config.GetUseFallback = o.Fallback +} + +type interceptorsOption struct { + Interceptors []Interceptor +} + +func (o *interceptorsOption) applyToClient(config *clientConfig) { + config.Interceptor = o.chainWith(config.Interceptor) +} + +func (o *interceptorsOption) applyToHandler(config *handlerConfig) { + config.Interceptor = o.chainWith(config.Interceptor) +} + +func (o *interceptorsOption) chainWith(current Interceptor) Interceptor { + if len(o.Interceptors) == 0 { + return current + } + if current == nil && len(o.Interceptors) == 1 { + return o.Interceptors[0] + } + if current == nil && len(o.Interceptors) > 1 { + return newChain(o.Interceptors) + } + return newChain(append([]Interceptor{current}, o.Interceptors...)) +} + +type optionsOption struct { + options []Option +} + +func (o *optionsOption) applyToClient(config *clientConfig) { + for _, option := range o.options { + option.applyToClient(config) + } +} + +func (o *optionsOption) applyToHandler(config *handlerConfig) { + for _, option := range o.options { + option.applyToHandler(config) + } +} + +type sendCompressionOption struct { + Name string +} + +func (o *sendCompressionOption) applyToClient(config *clientConfig) { + config.RequestCompressionName = o.Name +} + +func withGzip() Option { + return &compressionOption{ + Name: compressionGzip, + CompressionPool: newCompressionPool( + func() Decompressor { return &gzip.Reader{} }, + func() Compressor { return gzip.NewWriter(io.Discard) }, + ), + } +} + +func withProtoBinaryCodec() Option { + return WithCodec(&protoBinaryCodec{}) +} + +func withProtoJSONCodecs() HandlerOption { + return WithHandlerOptions( + WithCodec(&protoJSONCodec{codecNameJSON}), + WithCodec(&protoJSONCodec{codecNameJSONCharsetUTF8}), + ) +} + +type conditionalHandlerOptions struct { + conditional func(spec Spec) []HandlerOption +} + +func (o *conditionalHandlerOptions) applyToHandler(config *handlerConfig) { + spec := config.newSpec() + if spec.Procedure == "" { + return // ignore empty specs + } + for _, option := range o.conditional(spec) { + option.applyToHandler(config) + } +} diff --git a/vendor/connectrpc.com/connect/protobuf_util.go b/vendor/connectrpc.com/connect/protobuf_util.go new file mode 100644 index 000000000000..b9d0056d7199 --- /dev/null +++ b/vendor/connectrpc.com/connect/protobuf_util.go @@ -0,0 +1,42 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "strings" +) + +// extractProtoPath returns the trailing portion of the URL's path, +// corresponding to the Protobuf package, service, and method. It always starts +// with a slash. Within connect, we use this as (1) Spec.Procedure and (2) the +// path when mounting handlers on muxes. +func extractProtoPath(path string) string { + segments := strings.Split(path, "/") + var pkg, method string + if len(segments) > 0 { + pkg = segments[0] + } + if len(segments) > 1 { + pkg = segments[len(segments)-2] + method = segments[len(segments)-1] + } + if pkg == "" { + return "/" + } + if method == "" { + return "/" + pkg + } + return "/" + pkg + "/" + method +} diff --git a/vendor/connectrpc.com/connect/protocol.go b/vendor/connectrpc.com/connect/protocol.go new file mode 100644 index 000000000000..dc9dc0c8b3cc --- /dev/null +++ b/vendor/connectrpc.com/connect/protocol.go @@ -0,0 +1,424 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "context" + "errors" + "fmt" + "io" + "mime" + "net/http" + "net/url" + "sort" + "strings" +) + +// The names of the Connect, gRPC, and gRPC-Web protocols (as exposed by +// [Peer].Protocol). Additional protocols may be added in the future. +const ( + ProtocolConnect = "connect" + ProtocolGRPC = "grpc" + ProtocolGRPCWeb = "grpcweb" +) + +const ( + headerContentType = "Content-Type" + headerContentEncoding = "Content-Encoding" + headerContentLength = "Content-Length" + headerHost = "Host" + headerUserAgent = "User-Agent" + headerTrailer = "Trailer" + headerDate = "Date" + + discardLimit = 1024 * 1024 * 4 // 4MiB +) + +var errNoTimeout = errors.New("no timeout") + +// A Protocol defines the HTTP semantics to use when sending and receiving +// messages. It ties together codecs, compressors, and net/http to produce +// Senders and Receivers. +// +// For example, connect supports the gRPC protocol using this abstraction. Among +// many other things, the protocol implementation is responsible for +// translating timeouts from Go contexts to HTTP and vice versa. For gRPC, it +// converts timeouts to and from strings (for example, 10*time.Second <-> +// "10S"), and puts those strings into the "Grpc-Timeout" HTTP header. Other +// protocols might encode durations differently, put them into a different HTTP +// header, or ignore them entirely. +// +// We don't have any short-term plans to export this interface; it's just here +// to separate the protocol-specific portions of connect from the +// protocol-agnostic plumbing. +type protocol interface { + NewHandler(*protocolHandlerParams) protocolHandler + NewClient(*protocolClientParams) (protocolClient, error) +} + +// HandlerParams are the arguments provided to a Protocol's NewHandler +// method, bundled into a struct to allow backward-compatible argument +// additions. Protocol implementations should take care to use the supplied +// Spec rather than constructing their own, since new fields may have been +// added. +type protocolHandlerParams struct { + Spec Spec + Codecs readOnlyCodecs + CompressionPools readOnlyCompressionPools + CompressMinBytes int + BufferPool *bufferPool + ReadMaxBytes int + SendMaxBytes int + RequireConnectProtocolHeader bool + IdempotencyLevel IdempotencyLevel +} + +// Handler is the server side of a protocol. HTTP handlers typically support +// multiple protocols, codecs, and compressors. +type protocolHandler interface { + // Methods is the set of HTTP methods the protocol can handle. + Methods() map[string]struct{} + + // ContentTypes is the set of HTTP Content-Types that the protocol can + // handle. + ContentTypes() map[string]struct{} + + // SetTimeout runs before NewStream. Implementations may inspect the HTTP + // request, parse any timeout set by the client, and return a modified + // context and cancellation function. + // + // If the client didn't send a timeout, SetTimeout should return the + // request's context, a nil cancellation function, and a nil error. + SetTimeout(*http.Request) (context.Context, context.CancelFunc, error) + + // CanHandlePayload returns true if the protocol can handle an HTTP request. + // This is called after the request method is validated, so we only need to + // be concerned with the content type/payload specifically. + CanHandlePayload(*http.Request, string) bool + + // NewConn constructs a HandlerConn for the message exchange. + NewConn(http.ResponseWriter, *http.Request) (handlerConnCloser, bool) +} + +// ClientParams are the arguments provided to a Protocol's NewClient method, +// bundled into a struct to allow backward-compatible argument additions. +// Protocol implementations should take care to use the supplied Spec rather +// than constructing their own, since new fields may have been added. +type protocolClientParams struct { + CompressionName string + CompressionPools readOnlyCompressionPools + Codec Codec + CompressMinBytes int + HTTPClient HTTPClient + URL *url.URL + BufferPool *bufferPool + ReadMaxBytes int + SendMaxBytes int + EnableGet bool + GetURLMaxBytes int + GetUseFallback bool + // The gRPC family of protocols always needs access to a Protobuf codec to + // marshal and unmarshal errors. + Protobuf Codec +} + +// Client is the client side of a protocol. HTTP clients typically use a single +// protocol, codec, and compressor to send requests. +type protocolClient interface { + // Peer describes the server for the RPC. + Peer() Peer + + // WriteRequestHeader writes any protocol-specific request headers. + WriteRequestHeader(StreamType, http.Header) + + // NewConn constructs a StreamingClientConn for the message exchange. + // + // Implementations should assume that the supplied HTTP headers have already + // been populated by WriteRequestHeader. When constructing a stream for a + // unary call, implementations may assume that the Sender's Send and Close + // methods return before the Receiver's Receive or Close methods are called. + NewConn(context.Context, Spec, http.Header) streamingClientConn +} + +// streamingClientConn extends StreamingClientConn with a method for registering +// a hook when the HTTP request is actually sent. +type streamingClientConn interface { + StreamingClientConn + + onRequestSend(fn func(*http.Request)) +} + +// errorTranslatingHandlerConnCloser wraps a handlerConnCloser to ensure that +// we always return coded errors to users and write coded errors to the +// network. +// +// It's used in protocol implementations. +type errorTranslatingHandlerConnCloser struct { + handlerConnCloser + + toWire func(error) error + fromWire func(error) error +} + +func (hc *errorTranslatingHandlerConnCloser) Send(msg any) error { + return hc.fromWire(hc.handlerConnCloser.Send(msg)) +} + +func (hc *errorTranslatingHandlerConnCloser) Receive(msg any) error { + return hc.fromWire(hc.handlerConnCloser.Receive(msg)) +} + +func (hc *errorTranslatingHandlerConnCloser) Close(err error) error { + closeErr := hc.handlerConnCloser.Close(hc.toWire(err)) + return hc.fromWire(closeErr) +} + +func (hc *errorTranslatingHandlerConnCloser) getHTTPMethod() string { + if methoder, ok := hc.handlerConnCloser.(interface{ getHTTPMethod() string }); ok { + return methoder.getHTTPMethod() + } + return http.MethodPost +} + +// errorTranslatingClientConn wraps a StreamingClientConn to make sure that we always +// return coded errors from clients. +// +// It's used in protocol implementations. +type errorTranslatingClientConn struct { + streamingClientConn + + fromWire func(error) error +} + +func (cc *errorTranslatingClientConn) Send(msg any) error { + return cc.fromWire(cc.streamingClientConn.Send(msg)) +} + +func (cc *errorTranslatingClientConn) Receive(msg any) error { + return cc.fromWire(cc.streamingClientConn.Receive(msg)) +} + +func (cc *errorTranslatingClientConn) CloseRequest() error { + return cc.fromWire(cc.streamingClientConn.CloseRequest()) +} + +func (cc *errorTranslatingClientConn) CloseResponse() error { + return cc.fromWire(cc.streamingClientConn.CloseResponse()) +} + +func (cc *errorTranslatingClientConn) onRequestSend(fn func(*http.Request)) { + cc.streamingClientConn.onRequestSend(fn) +} + +// wrapHandlerConnWithCodedErrors ensures that we (1) automatically code +// context-related errors correctly when writing them to the network, and (2) +// return *Errors from all exported APIs. +func wrapHandlerConnWithCodedErrors(conn handlerConnCloser) handlerConnCloser { + return &errorTranslatingHandlerConnCloser{ + handlerConnCloser: conn, + toWire: wrapIfContextError, + fromWire: wrapIfUncoded, + } +} + +// wrapClientConnWithCodedErrors ensures that we always return *Errors from +// public APIs. +func wrapClientConnWithCodedErrors(conn streamingClientConn) streamingClientConn { + return &errorTranslatingClientConn{ + streamingClientConn: conn, + fromWire: wrapIfUncoded, + } +} + +func mappedMethodHandlers(handlers []protocolHandler) map[string][]protocolHandler { + methodHandlers := make(map[string][]protocolHandler) + for _, handler := range handlers { + for method := range handler.Methods() { + methodHandlers[method] = append(methodHandlers[method], handler) + } + } + return methodHandlers +} + +func sortedAcceptPostValue(handlers []protocolHandler) string { + contentTypes := make(map[string]struct{}) + for _, handler := range handlers { + for contentType := range handler.ContentTypes() { + contentTypes[contentType] = struct{}{} + } + } + accept := make([]string, 0, len(contentTypes)) + for ct := range contentTypes { + accept = append(accept, ct) + } + sort.Strings(accept) + return strings.Join(accept, ", ") +} + +func sortedAllowMethodValue(handlers []protocolHandler) string { + methods := make(map[string]struct{}) + for _, handler := range handlers { + for method := range handler.Methods() { + methods[method] = struct{}{} + } + } + allow := make([]string, 0, len(methods)) + for ct := range methods { + allow = append(allow, ct) + } + sort.Strings(allow) + return strings.Join(allow, ", ") +} + +func isCommaOrSpace(c rune) bool { + return c == ',' || c == ' ' +} + +func discard(reader io.Reader) (int64, error) { + if lr, ok := reader.(*io.LimitedReader); ok { + return io.Copy(io.Discard, lr) + } + // We don't want to get stuck throwing data away forever, so limit how much + // we're willing to do here. + lr := &io.LimitedReader{R: reader, N: discardLimit} + return io.Copy(io.Discard, lr) +} + +// negotiateCompression determines and validates the request compression and +// response compression using the available compressors and protocol-specific +// Content-Encoding and Accept-Encoding headers. +func negotiateCompression( //nolint:nonamedreturns + availableCompressors readOnlyCompressionPools, + sent, accept string, +) (requestCompression, responseCompression string, clientVisibleErr *Error) { + requestCompression = compressionIdentity + if sent != "" && sent != compressionIdentity { + // We default to identity, so we only care if the client sends something + // other than the empty string or compressIdentity. + if availableCompressors.Contains(sent) { + requestCompression = sent + } else { + // To comply with + // https://github.com/grpc/grpc/blob/master/doc/compression.md and the + // Connect protocol, we should return CodeUnimplemented and specify + // acceptable compression(s) (in addition to setting the a + // protocol-specific accept-encoding header). + return "", "", errorf( + CodeUnimplemented, + "unknown compression %q: supported encodings are %v", + sent, availableCompressors.CommaSeparatedNames(), + ) + } + } + // Support asymmetric compression. This logic follows + // https://github.com/grpc/grpc/blob/master/doc/compression.md and common + // sense. + responseCompression = requestCompression + // If we're not already planning to compress the response, check whether the + // client requested a compression algorithm we support. + if responseCompression == compressionIdentity && accept != "" { + for _, name := range strings.FieldsFunc(accept, isCommaOrSpace) { + if availableCompressors.Contains(name) { + // We found a mutually supported compression algorithm. Unlike standard + // HTTP, there's no preference weighting, so can bail out immediately. + responseCompression = name + break + } + } + } + return requestCompression, responseCompression, nil +} + +// checkServerStreamsCanFlush ensures that bidi and server streaming handlers +// have received an http.ResponseWriter that implements http.Flusher, since +// they must flush data after sending each message. +func checkServerStreamsCanFlush(spec Spec, responseWriter http.ResponseWriter) *Error { + requiresFlusher := (spec.StreamType & StreamTypeServer) == StreamTypeServer + if _, flushable := responseWriter.(http.Flusher); requiresFlusher && !flushable { + return NewError(CodeInternal, fmt.Errorf("%T does not implement http.Flusher", responseWriter)) + } + return nil +} + +func flushResponseWriter(w http.ResponseWriter) { + if f, ok := w.(http.Flusher); ok { + f.Flush() + } +} + +func canonicalizeContentType(contentType string) string { + // Typically, clients send Content-Type in canonical form, without + // parameters. In those cases, we'd like to avoid parsing and + // canonicalization overhead. + // + // See https://www.rfc-editor.org/rfc/rfc2045.html#section-5.1 for a full + // grammar. + var slashes int + for _, r := range contentType { + switch { + case r >= 'a' && r <= 'z': + case r == '.' || r == '+' || r == '-': + case r == '/': + slashes++ + default: + return canonicalizeContentTypeSlow(contentType) + } + } + if slashes == 1 { + return contentType + } + return canonicalizeContentTypeSlow(contentType) +} + +func canonicalizeContentTypeSlow(contentType string) string { + base, params, err := mime.ParseMediaType(contentType) + if err != nil { + return contentType + } + // According to RFC 9110 Section 8.3.2, the charset parameter value should be treated as case-insensitive. + // mime.FormatMediaType canonicalizes parameter names, but not parameter values, + // because the case sensitivity of a parameter value depends on its semantics. + // Therefore, the charset parameter value should be canonicalized here. + // ref.) https://httpwg.org/specs/rfc9110.html#rfc.section.8.3.2 + if charset, ok := params["charset"]; ok { + params["charset"] = strings.ToLower(charset) + } + return mime.FormatMediaType(base, params) +} + +func httpToCode(httpCode int) Code { + // https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md + // Note that this is NOT the inverse of the gRPC-to-HTTP or Connect-to-HTTP + // mappings. + + // Literals are easier to compare to the specification (vs named + // constants). + switch httpCode { + case 400: + return CodeInternal + case 401: + return CodeUnauthenticated + case 403: + return CodePermissionDenied + case 404: + return CodeUnimplemented + case 429: + return CodeUnavailable + case 502, 503, 504: + return CodeUnavailable + default: + return CodeUnknown + } +} diff --git a/vendor/connectrpc.com/connect/protocol_connect.go b/vendor/connectrpc.com/connect/protocol_connect.go new file mode 100644 index 000000000000..371f382e34ab --- /dev/null +++ b/vendor/connectrpc.com/connect/protocol_connect.go @@ -0,0 +1,1449 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "net/http" + "net/url" + "runtime" + "strconv" + "strings" + "time" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" +) + +const ( + connectUnaryHeaderCompression = "Content-Encoding" + connectUnaryHeaderAcceptCompression = "Accept-Encoding" + connectUnaryTrailerPrefix = "Trailer-" + connectStreamingHeaderCompression = "Connect-Content-Encoding" + connectStreamingHeaderAcceptCompression = "Connect-Accept-Encoding" + connectHeaderTimeout = "Connect-Timeout-Ms" + connectHeaderProtocolVersion = "Connect-Protocol-Version" + connectProtocolVersion = "1" + headerVary = "Vary" + + connectFlagEnvelopeEndStream = 0b00000010 + + connectUnaryContentTypePrefix = "application/" + connectUnaryContentTypeJSON = connectUnaryContentTypePrefix + codecNameJSON + connectStreamingContentTypePrefix = "application/connect+" + + connectUnaryEncodingQueryParameter = "encoding" + connectUnaryMessageQueryParameter = "message" + connectUnaryBase64QueryParameter = "base64" + connectUnaryCompressionQueryParameter = "compression" + connectUnaryConnectQueryParameter = "connect" + connectUnaryConnectQueryValue = "v" + connectProtocolVersion +) + +// defaultConnectUserAgent returns a User-Agent string similar to those used in gRPC. +// +//nolint:gochecknoglobals +var defaultConnectUserAgent = fmt.Sprintf("connect-go/%s (%s)", Version, runtime.Version()) + +type protocolConnect struct{} + +// NewHandler implements protocol, so it must return an interface. +func (*protocolConnect) NewHandler(params *protocolHandlerParams) protocolHandler { + methods := make(map[string]struct{}) + methods[http.MethodPost] = struct{}{} + + if params.Spec.StreamType == StreamTypeUnary && params.IdempotencyLevel == IdempotencyNoSideEffects { + methods[http.MethodGet] = struct{}{} + } + + contentTypes := make(map[string]struct{}) + for _, name := range params.Codecs.Names() { + if params.Spec.StreamType == StreamTypeUnary { + contentTypes[canonicalizeContentType(connectUnaryContentTypePrefix+name)] = struct{}{} + continue + } + contentTypes[canonicalizeContentType(connectStreamingContentTypePrefix+name)] = struct{}{} + } + + return &connectHandler{ + protocolHandlerParams: *params, + methods: methods, + accept: contentTypes, + } +} + +// NewClient implements protocol, so it must return an interface. +func (*protocolConnect) NewClient(params *protocolClientParams) (protocolClient, error) { + return &connectClient{ + protocolClientParams: *params, + peer: newPeerForURL(params.URL, ProtocolConnect), + }, nil +} + +type connectHandler struct { + protocolHandlerParams + + methods map[string]struct{} + accept map[string]struct{} +} + +func (h *connectHandler) Methods() map[string]struct{} { + return h.methods +} + +func (h *connectHandler) ContentTypes() map[string]struct{} { + return h.accept +} + +func (*connectHandler) SetTimeout(request *http.Request) (context.Context, context.CancelFunc, error) { + timeout := getHeaderCanonical(request.Header, connectHeaderTimeout) + if timeout == "" { + return request.Context(), nil, nil + } + if len(timeout) > 10 { + return nil, nil, errorf(CodeInvalidArgument, "parse timeout: %q has >10 digits", timeout) + } + millis, err := strconv.ParseInt(timeout, 10 /* base */, 64 /* bitsize */) + if err != nil { + return nil, nil, errorf(CodeInvalidArgument, "parse timeout: %w", err) + } + ctx, cancel := context.WithTimeout( + request.Context(), + time.Duration(millis)*time.Millisecond, + ) + return ctx, cancel, nil +} + +func (h *connectHandler) CanHandlePayload(request *http.Request, contentType string) bool { + if request.Method == http.MethodGet { + query := request.URL.Query() + codecName := query.Get(connectUnaryEncodingQueryParameter) + contentType = connectContentTypeForCodecName( + h.Spec.StreamType, + codecName, + ) + } + _, ok := h.accept[contentType] + return ok +} + +func (h *connectHandler) NewConn( + responseWriter http.ResponseWriter, + request *http.Request, +) (handlerConnCloser, bool) { + ctx := request.Context() + query := request.URL.Query() + // We need to parse metadata before entering the interceptor stack; we'll + // send the error to the client later on. + var contentEncoding, acceptEncoding string + if h.Spec.StreamType == StreamTypeUnary { + if request.Method == http.MethodGet { + contentEncoding = query.Get(connectUnaryCompressionQueryParameter) + } else { + contentEncoding = getHeaderCanonical(request.Header, connectUnaryHeaderCompression) + } + acceptEncoding = getHeaderCanonical(request.Header, connectUnaryHeaderAcceptCompression) + } else { + contentEncoding = getHeaderCanonical(request.Header, connectStreamingHeaderCompression) + acceptEncoding = getHeaderCanonical(request.Header, connectStreamingHeaderAcceptCompression) + } + requestCompression, responseCompression, failed := negotiateCompression( + h.CompressionPools, + contentEncoding, + acceptEncoding, + ) + if failed == nil { + failed = checkServerStreamsCanFlush(h.Spec, responseWriter) + } + if failed == nil { + required := h.RequireConnectProtocolHeader && (h.Spec.StreamType == StreamTypeUnary) + failed = connectCheckProtocolVersion(request, required) + } + + var requestBody io.ReadCloser + var contentType, codecName string + if request.Method == http.MethodGet { + if failed == nil && !query.Has(connectUnaryEncodingQueryParameter) { + failed = errorf(CodeInvalidArgument, "missing %s parameter", connectUnaryEncodingQueryParameter) + } else if failed == nil && !query.Has(connectUnaryMessageQueryParameter) { + failed = errorf(CodeInvalidArgument, "missing %s parameter", connectUnaryMessageQueryParameter) + } + msg := query.Get(connectUnaryMessageQueryParameter) + msgReader := queryValueReader(msg, query.Get(connectUnaryBase64QueryParameter) == "1") + requestBody = io.NopCloser(msgReader) + codecName = query.Get(connectUnaryEncodingQueryParameter) + contentType = connectContentTypeForCodecName( + h.Spec.StreamType, + codecName, + ) + } else { + requestBody = request.Body + contentType = getHeaderCanonical(request.Header, headerContentType) + codecName = connectCodecForContentType( + h.Spec.StreamType, + contentType, + ) + } + + codec := h.Codecs.Get(codecName) + // The codec can be nil in the GET request case; that's okay: when failed + // is non-nil, codec is never used. + if failed == nil && codec == nil { + failed = errorf(CodeInvalidArgument, "invalid message encoding: %q", codecName) + } + + // Write any remaining headers here: + // (1) any writes to the stream will implicitly send the headers, so we + // should get all of gRPC's required response headers ready. + // (2) interceptors should be able to see these headers. + // + // Since we know that these header keys are already in canonical form, we can + // skip the normalization in Header.Set. + header := responseWriter.Header() + header[headerContentType] = []string{contentType} + acceptCompressionHeader := connectUnaryHeaderAcceptCompression + if h.Spec.StreamType != StreamTypeUnary { + acceptCompressionHeader = connectStreamingHeaderAcceptCompression + // We only write the request encoding header here for streaming calls, + // since the streaming envelope lets us choose whether to compress each + // message individually. For unary, we won't know whether we're compressing + // the request until we see how large the payload is. + if responseCompression != compressionIdentity { + header[connectStreamingHeaderCompression] = []string{responseCompression} + } + } + header[acceptCompressionHeader] = []string{h.CompressionPools.CommaSeparatedNames()} + + var conn handlerConnCloser + peer := Peer{ + Addr: request.RemoteAddr, + Protocol: ProtocolConnect, + Query: query, + } + if h.Spec.StreamType == StreamTypeUnary { + conn = &connectUnaryHandlerConn{ + spec: h.Spec, + peer: peer, + request: request, + responseWriter: responseWriter, + marshaler: connectUnaryMarshaler{ + ctx: ctx, + sender: writeSender{writer: responseWriter}, + codec: codec, + compressMinBytes: h.CompressMinBytes, + compressionName: responseCompression, + compressionPool: h.CompressionPools.Get(responseCompression), + bufferPool: h.BufferPool, + header: responseWriter.Header(), + sendMaxBytes: h.SendMaxBytes, + }, + unmarshaler: connectUnaryUnmarshaler{ + ctx: ctx, + reader: requestBody, + codec: codec, + compressionPool: h.CompressionPools.Get(requestCompression), + bufferPool: h.BufferPool, + readMaxBytes: h.ReadMaxBytes, + }, + responseTrailer: make(http.Header), + } + } else { + conn = &connectStreamingHandlerConn{ + spec: h.Spec, + peer: peer, + request: request, + responseWriter: responseWriter, + marshaler: connectStreamingMarshaler{ + envelopeWriter: envelopeWriter{ + ctx: ctx, + sender: writeSender{responseWriter}, + codec: codec, + compressMinBytes: h.CompressMinBytes, + compressionPool: h.CompressionPools.Get(responseCompression), + bufferPool: h.BufferPool, + sendMaxBytes: h.SendMaxBytes, + }, + }, + unmarshaler: connectStreamingUnmarshaler{ + envelopeReader: envelopeReader{ + ctx: ctx, + reader: requestBody, + codec: codec, + compressionPool: h.CompressionPools.Get(requestCompression), + bufferPool: h.BufferPool, + readMaxBytes: h.ReadMaxBytes, + }, + }, + responseTrailer: make(http.Header), + } + } + conn = wrapHandlerConnWithCodedErrors(conn) + + if failed != nil { + // Negotiation failed, so we can't establish a stream. + _ = conn.Close(failed) + return nil, false + } + return conn, true +} + +type connectClient struct { + protocolClientParams + + peer Peer +} + +func (c *connectClient) Peer() Peer { + return c.peer +} + +func (c *connectClient) WriteRequestHeader(streamType StreamType, header http.Header) { + // We know these header keys are in canonical form, so we can bypass all the + // checks in Header.Set. + if getHeaderCanonical(header, headerUserAgent) == "" { + header[headerUserAgent] = []string{defaultConnectUserAgent} + } + header[connectHeaderProtocolVersion] = []string{connectProtocolVersion} + header[headerContentType] = []string{ + connectContentTypeForCodecName(streamType, c.Codec.Name()), + } + acceptCompressionHeader := connectUnaryHeaderAcceptCompression + if streamType != StreamTypeUnary { + // If we don't set Accept-Encoding, by default http.Client will ask the + // server to compress the whole stream. Since we're already compressing + // each message, this is a waste. + header[connectUnaryHeaderAcceptCompression] = []string{compressionIdentity} + acceptCompressionHeader = connectStreamingHeaderAcceptCompression + // We only write the request encoding header here for streaming calls, + // since the streaming envelope lets us choose whether to compress each + // message individually. For unary, we won't know whether we're compressing + // the request until we see how large the payload is. + if c.CompressionName != "" && c.CompressionName != compressionIdentity { + header[connectStreamingHeaderCompression] = []string{c.CompressionName} + } + } + if acceptCompression := c.CompressionPools.CommaSeparatedNames(); acceptCompression != "" { + header[acceptCompressionHeader] = []string{acceptCompression} + } +} + +func (c *connectClient) NewConn( + ctx context.Context, + spec Spec, + header http.Header, +) streamingClientConn { + if deadline, ok := ctx.Deadline(); ok { + millis := int64(time.Until(deadline) / time.Millisecond) + if millis > 0 { + encoded := strconv.FormatInt(millis, 10 /* base */) + if len(encoded) <= 10 { + header[connectHeaderTimeout] = []string{encoded} + } // else effectively unbounded + } + } + duplexCall := newDuplexHTTPCall(ctx, c.HTTPClient, c.URL, spec, header) + var conn streamingClientConn + if spec.StreamType == StreamTypeUnary { + unaryConn := &connectUnaryClientConn{ + spec: spec, + peer: c.Peer(), + duplexCall: duplexCall, + compressionPools: c.CompressionPools, + bufferPool: c.BufferPool, + marshaler: connectUnaryRequestMarshaler{ + connectUnaryMarshaler: connectUnaryMarshaler{ + ctx: ctx, + sender: duplexCall, + codec: c.Codec, + compressMinBytes: c.CompressMinBytes, + compressionName: c.CompressionName, + compressionPool: c.CompressionPools.Get(c.CompressionName), + bufferPool: c.BufferPool, + header: duplexCall.Header(), + sendMaxBytes: c.SendMaxBytes, + }, + }, + unmarshaler: connectUnaryUnmarshaler{ + ctx: ctx, + reader: duplexCall, + codec: c.Codec, + bufferPool: c.BufferPool, + readMaxBytes: c.ReadMaxBytes, + }, + responseHeader: make(http.Header), + responseTrailer: make(http.Header), + } + if spec.IdempotencyLevel == IdempotencyNoSideEffects { + unaryConn.marshaler.enableGet = c.EnableGet + unaryConn.marshaler.getURLMaxBytes = c.GetURLMaxBytes + unaryConn.marshaler.getUseFallback = c.GetUseFallback + unaryConn.marshaler.duplexCall = duplexCall + if stableCodec, ok := c.Codec.(stableCodec); ok { + unaryConn.marshaler.stableCodec = stableCodec + } + } + conn = unaryConn + duplexCall.SetValidateResponse(unaryConn.validateResponse) + } else { + streamingConn := &connectStreamingClientConn{ + spec: spec, + peer: c.Peer(), + duplexCall: duplexCall, + compressionPools: c.CompressionPools, + bufferPool: c.BufferPool, + codec: c.Codec, + marshaler: connectStreamingMarshaler{ + envelopeWriter: envelopeWriter{ + ctx: ctx, + sender: duplexCall, + codec: c.Codec, + compressMinBytes: c.CompressMinBytes, + compressionPool: c.CompressionPools.Get(c.CompressionName), + bufferPool: c.BufferPool, + sendMaxBytes: c.SendMaxBytes, + }, + }, + unmarshaler: connectStreamingUnmarshaler{ + envelopeReader: envelopeReader{ + ctx: ctx, + reader: duplexCall, + codec: c.Codec, + bufferPool: c.BufferPool, + readMaxBytes: c.ReadMaxBytes, + }, + }, + responseHeader: make(http.Header), + responseTrailer: make(http.Header), + } + conn = streamingConn + duplexCall.SetValidateResponse(streamingConn.validateResponse) + } + return wrapClientConnWithCodedErrors(conn) +} + +type connectUnaryClientConn struct { + spec Spec + peer Peer + duplexCall *duplexHTTPCall + compressionPools readOnlyCompressionPools + bufferPool *bufferPool + marshaler connectUnaryRequestMarshaler + unmarshaler connectUnaryUnmarshaler + responseHeader http.Header + responseTrailer http.Header +} + +func (cc *connectUnaryClientConn) Spec() Spec { + return cc.spec +} + +func (cc *connectUnaryClientConn) Peer() Peer { + return cc.peer +} + +func (cc *connectUnaryClientConn) Send(msg any) error { + if err := cc.marshaler.Marshal(msg); err != nil { + return err + } + return nil // must be a literal nil: nil *Error is a non-nil error +} + +func (cc *connectUnaryClientConn) RequestHeader() http.Header { + return cc.duplexCall.Header() +} + +func (cc *connectUnaryClientConn) CloseRequest() error { + return cc.duplexCall.CloseWrite() +} + +func (cc *connectUnaryClientConn) Receive(msg any) error { + if err := cc.duplexCall.BlockUntilResponseReady(); err != nil { + return err + } + if err := cc.unmarshaler.Unmarshal(msg); err != nil { + return err + } + return nil // must be a literal nil: nil *Error is a non-nil error +} + +func (cc *connectUnaryClientConn) ResponseHeader() http.Header { + _ = cc.duplexCall.BlockUntilResponseReady() + return cc.responseHeader +} + +func (cc *connectUnaryClientConn) ResponseTrailer() http.Header { + _ = cc.duplexCall.BlockUntilResponseReady() + return cc.responseTrailer +} + +func (cc *connectUnaryClientConn) CloseResponse() error { + return cc.duplexCall.CloseRead() +} + +func (cc *connectUnaryClientConn) onRequestSend(fn func(*http.Request)) { + cc.duplexCall.onRequestSend = fn +} + +func (cc *connectUnaryClientConn) validateResponse(response *http.Response) *Error { + for k, v := range response.Header { + if !strings.HasPrefix(k, connectUnaryTrailerPrefix) { + cc.responseHeader[k] = v + continue + } + cc.responseTrailer[k[len(connectUnaryTrailerPrefix):]] = v + } + if err := connectValidateUnaryResponseContentType( + cc.marshaler.codec.Name(), + cc.duplexCall.Method(), + response.StatusCode, + response.Status, + getHeaderCanonical(response.Header, headerContentType), + ); err != nil { + if IsNotModifiedError(err) { + // Allow access to response headers for this kind of error. + // RFC 9110 doesn't allow trailers on 304s, so we only need to include headers. + err.meta = cc.responseHeader.Clone() + } + return err + } + compression := getHeaderCanonical(response.Header, connectUnaryHeaderCompression) + if compression != "" && + compression != compressionIdentity && + !cc.compressionPools.Contains(compression) { + return errorf( + CodeInternal, + "unknown encoding %q: accepted encodings are %v", + compression, + cc.compressionPools.CommaSeparatedNames(), + ) + } + cc.unmarshaler.compressionPool = cc.compressionPools.Get(compression) + if response.StatusCode != http.StatusOK { + unmarshaler := connectUnaryUnmarshaler{ + ctx: cc.unmarshaler.ctx, + reader: response.Body, + compressionPool: cc.unmarshaler.compressionPool, + bufferPool: cc.bufferPool, + } + var wireErr connectWireError + if err := unmarshaler.UnmarshalFunc(&wireErr, json.Unmarshal); err != nil { + return NewError( + httpToCode(response.StatusCode), + errors.New(response.Status), + ) + } + if wireErr.Code == 0 { + // code not set? default to one implied by HTTP status + wireErr.Code = httpToCode(response.StatusCode) + } + serverErr := wireErr.asError() + if serverErr == nil { + return nil + } + serverErr.meta = cc.responseHeader.Clone() + mergeHeaders(serverErr.meta, cc.responseTrailer) + return serverErr + } + return nil +} + +type connectStreamingClientConn struct { + spec Spec + peer Peer + duplexCall *duplexHTTPCall + compressionPools readOnlyCompressionPools + bufferPool *bufferPool + codec Codec + marshaler connectStreamingMarshaler + unmarshaler connectStreamingUnmarshaler + responseHeader http.Header + responseTrailer http.Header +} + +func (cc *connectStreamingClientConn) Spec() Spec { + return cc.spec +} + +func (cc *connectStreamingClientConn) Peer() Peer { + return cc.peer +} + +func (cc *connectStreamingClientConn) Send(msg any) error { + if err := cc.marshaler.Marshal(msg); err != nil { + return err + } + return nil // must be a literal nil: nil *Error is a non-nil error +} + +func (cc *connectStreamingClientConn) RequestHeader() http.Header { + return cc.duplexCall.Header() +} + +func (cc *connectStreamingClientConn) CloseRequest() error { + return cc.duplexCall.CloseWrite() +} + +func (cc *connectStreamingClientConn) Receive(msg any) error { + if err := cc.duplexCall.BlockUntilResponseReady(); err != nil { + return err + } + err := cc.unmarshaler.Unmarshal(msg) + if err == nil { + return nil + } + // See if the server sent an explicit error in the end-of-stream message. + mergeHeaders(cc.responseTrailer, cc.unmarshaler.Trailer()) + if serverErr := cc.unmarshaler.EndStreamError(); serverErr != nil { + // This is expected from a protocol perspective, but receiving an + // end-of-stream message means that we're _not_ getting a regular message. + // For users to realize that the stream has ended, Receive must return an + // error. + serverErr.meta = cc.responseHeader.Clone() + mergeHeaders(serverErr.meta, cc.responseTrailer) + _ = cc.duplexCall.CloseWrite() + return serverErr + } + // If the error is EOF but not from a last message, we want to return + // io.ErrUnexpectedEOF instead. + if errors.Is(err, io.EOF) && !errors.Is(err, errSpecialEnvelope) { + err = errorf(CodeInternal, "protocol error: %w", io.ErrUnexpectedEOF) + } + // There's no error in the trailers, so this was probably an error + // converting the bytes to a message, an error reading from the network, or + // just an EOF. We're going to return it to the user, but we also want to + // close the writer so Send errors out. + _ = cc.duplexCall.CloseWrite() + return err +} + +func (cc *connectStreamingClientConn) ResponseHeader() http.Header { + _ = cc.duplexCall.BlockUntilResponseReady() + return cc.responseHeader +} + +func (cc *connectStreamingClientConn) ResponseTrailer() http.Header { + _ = cc.duplexCall.BlockUntilResponseReady() + return cc.responseTrailer +} + +func (cc *connectStreamingClientConn) CloseResponse() error { + return cc.duplexCall.CloseRead() +} + +func (cc *connectStreamingClientConn) onRequestSend(fn func(*http.Request)) { + cc.duplexCall.onRequestSend = fn +} + +func (cc *connectStreamingClientConn) validateResponse(response *http.Response) *Error { + if response.StatusCode != http.StatusOK { + return errorf(httpToCode(response.StatusCode), "HTTP status %v", response.Status) + } + if err := connectValidateStreamResponseContentType( + cc.codec.Name(), + cc.spec.StreamType, + getHeaderCanonical(response.Header, headerContentType), + ); err != nil { + return err + } + compression := getHeaderCanonical(response.Header, connectStreamingHeaderCompression) + if compression != "" && + compression != compressionIdentity && + !cc.compressionPools.Contains(compression) { + return errorf( + CodeInternal, + "unknown encoding %q: accepted encodings are %v", + compression, + cc.compressionPools.CommaSeparatedNames(), + ) + } + cc.unmarshaler.compressionPool = cc.compressionPools.Get(compression) + mergeHeaders(cc.responseHeader, response.Header) + return nil +} + +type connectUnaryHandlerConn struct { + spec Spec + peer Peer + request *http.Request + responseWriter http.ResponseWriter + marshaler connectUnaryMarshaler + unmarshaler connectUnaryUnmarshaler + responseTrailer http.Header +} + +func (hc *connectUnaryHandlerConn) Spec() Spec { + return hc.spec +} + +func (hc *connectUnaryHandlerConn) Peer() Peer { + return hc.peer +} + +func (hc *connectUnaryHandlerConn) Receive(msg any) error { + if err := hc.unmarshaler.Unmarshal(msg); err != nil { + return err + } + return nil // must be a literal nil: nil *Error is a non-nil error +} + +func (hc *connectUnaryHandlerConn) RequestHeader() http.Header { + return hc.request.Header +} + +func (hc *connectUnaryHandlerConn) Send(msg any) error { + hc.mergeResponseHeader(nil /* error */) + if err := hc.marshaler.Marshal(msg); err != nil { + return err + } + return nil // must be a literal nil: nil *Error is a non-nil error +} + +func (hc *connectUnaryHandlerConn) ResponseHeader() http.Header { + return hc.responseWriter.Header() +} + +func (hc *connectUnaryHandlerConn) ResponseTrailer() http.Header { + return hc.responseTrailer +} + +func (hc *connectUnaryHandlerConn) Close(err error) error { + if !hc.marshaler.wroteHeader { + hc.mergeResponseHeader(err) + // If the handler received a GET request and the resource hasn't changed, + // return a 304. + if len(hc.peer.Query) > 0 && IsNotModifiedError(err) { + hc.responseWriter.WriteHeader(http.StatusNotModified) + return hc.request.Body.Close() + } + } + if err == nil || hc.marshaler.wroteHeader { + return hc.request.Body.Close() + } + // In unary Connect, errors always use application/json. + setHeaderCanonical(hc.responseWriter.Header(), headerContentType, connectUnaryContentTypeJSON) + hc.responseWriter.WriteHeader(connectCodeToHTTP(CodeOf(err))) + data, marshalErr := json.Marshal(newConnectWireError(err)) + if marshalErr != nil { + _ = hc.request.Body.Close() + return errorf(CodeInternal, "marshal error: %w", err) + } + if _, writeErr := hc.responseWriter.Write(data); writeErr != nil { + _ = hc.request.Body.Close() + return writeErr + } + return hc.request.Body.Close() +} + +func (hc *connectUnaryHandlerConn) getHTTPMethod() string { + return hc.request.Method +} + +func (hc *connectUnaryHandlerConn) mergeResponseHeader(err error) { + header := hc.responseWriter.Header() + if hc.request.Method == http.MethodGet { + // The response content varies depending on the compression that the client + // requested (if any). GETs are potentially cacheable, so we should ensure + // that the Vary header includes at least Accept-Encoding (and not overwrite any values already set). + header[headerVary] = append(header[headerVary], connectUnaryHeaderAcceptCompression) + } + if err != nil { + if connectErr, ok := asError(err); ok && !connectErr.wireErr { + mergeNonProtocolHeaders(header, connectErr.meta) + } + } + for k, v := range hc.responseTrailer { + header[connectUnaryTrailerPrefix+k] = v + } +} + +type connectStreamingHandlerConn struct { + spec Spec + peer Peer + request *http.Request + responseWriter http.ResponseWriter + marshaler connectStreamingMarshaler + unmarshaler connectStreamingUnmarshaler + responseTrailer http.Header +} + +func (hc *connectStreamingHandlerConn) Spec() Spec { + return hc.spec +} + +func (hc *connectStreamingHandlerConn) Peer() Peer { + return hc.peer +} + +func (hc *connectStreamingHandlerConn) Receive(msg any) error { + if err := hc.unmarshaler.Unmarshal(msg); err != nil { + // Clients may not send end-of-stream metadata, so we don't need to handle + // errSpecialEnvelope. + return err + } + return nil // must be a literal nil: nil *Error is a non-nil error +} + +func (hc *connectStreamingHandlerConn) RequestHeader() http.Header { + return hc.request.Header +} + +func (hc *connectStreamingHandlerConn) Send(msg any) error { + defer flushResponseWriter(hc.responseWriter) + if err := hc.marshaler.Marshal(msg); err != nil { + return err + } + return nil // must be a literal nil: nil *Error is a non-nil error +} + +func (hc *connectStreamingHandlerConn) ResponseHeader() http.Header { + return hc.responseWriter.Header() +} + +func (hc *connectStreamingHandlerConn) ResponseTrailer() http.Header { + return hc.responseTrailer +} + +func (hc *connectStreamingHandlerConn) Close(err error) error { + defer flushResponseWriter(hc.responseWriter) + if err := hc.marshaler.MarshalEndStream(err, hc.responseTrailer); err != nil { + _ = hc.request.Body.Close() + return err + } + // We don't want to copy unread portions of the body to /dev/null here: if + // the client hasn't closed the request body, we'll block until the server + // timeout kicks in. This could happen because the client is malicious, but + // a well-intentioned client may just not expect the server to be returning + // an error for a streaming RPC. Better to accept that we can't always reuse + // TCP connections. + if err := hc.request.Body.Close(); err != nil { + if connectErr, ok := asError(err); ok { + return connectErr + } + return NewError(CodeUnknown, err) + } + return nil // must be a literal nil: nil *Error is a non-nil error +} + +type connectStreamingMarshaler struct { + envelopeWriter +} + +func (m *connectStreamingMarshaler) MarshalEndStream(err error, trailer http.Header) *Error { + end := &connectEndStreamMessage{Trailer: trailer} + if err != nil { + end.Error = newConnectWireError(err) + if connectErr, ok := asError(err); ok && !connectErr.wireErr { + mergeNonProtocolHeaders(end.Trailer, connectErr.meta) + } + } + data, marshalErr := json.Marshal(end) + if marshalErr != nil { + return errorf(CodeInternal, "marshal end stream: %w", marshalErr) + } + raw := bytes.NewBuffer(data) + defer m.envelopeWriter.bufferPool.Put(raw) + return m.Write(&envelope{ + Data: raw, + Flags: connectFlagEnvelopeEndStream, + }) +} + +type connectStreamingUnmarshaler struct { + envelopeReader + + endStreamErr *Error + trailer http.Header +} + +func (u *connectStreamingUnmarshaler) Unmarshal(message any) *Error { + err := u.envelopeReader.Unmarshal(message) + if err == nil { + return nil + } + if !errors.Is(err, errSpecialEnvelope) { + return err + } + env := u.last + data := env.Data + u.last.Data = nil // don't keep a reference to it + defer u.bufferPool.Put(data) + if !env.IsSet(connectFlagEnvelopeEndStream) { + return errorf(CodeInternal, "protocol error: invalid envelope flags %d", env.Flags) + } + var end connectEndStreamMessage + if err := json.Unmarshal(data.Bytes(), &end); err != nil { + return errorf(CodeInternal, "unmarshal end stream message: %w", err) + } + for name, value := range end.Trailer { + canonical := http.CanonicalHeaderKey(name) + if name != canonical { + delHeaderCanonical(end.Trailer, name) + end.Trailer[canonical] = append(end.Trailer[canonical], value...) + } + } + u.trailer = end.Trailer + u.endStreamErr = end.Error.asError() + return errSpecialEnvelope +} + +func (u *connectStreamingUnmarshaler) Trailer() http.Header { + return u.trailer +} + +func (u *connectStreamingUnmarshaler) EndStreamError() *Error { + return u.endStreamErr +} + +type connectUnaryMarshaler struct { + ctx context.Context //nolint:containedctx + sender messageSender + codec Codec + compressMinBytes int + compressionName string + compressionPool *compressionPool + bufferPool *bufferPool + header http.Header + sendMaxBytes int + wroteHeader bool +} + +func (m *connectUnaryMarshaler) Marshal(message any) *Error { + if message == nil { + return m.write(nil) + } + var data []byte + var err error + if appender, ok := m.codec.(marshalAppender); ok { + data, err = appender.MarshalAppend(m.bufferPool.Get().Bytes(), message) + } else { + // Can't avoid allocating the slice, but we'll reuse it. + data, err = m.codec.Marshal(message) + } + if err != nil { + return errorf(CodeInternal, "marshal message: %w", err) + } + uncompressed := bytes.NewBuffer(data) + defer m.bufferPool.Put(uncompressed) + if len(data) < m.compressMinBytes || m.compressionPool == nil { + if m.sendMaxBytes > 0 && len(data) > m.sendMaxBytes { + return NewError(CodeResourceExhausted, fmt.Errorf("message size %d exceeds sendMaxBytes %d", len(data), m.sendMaxBytes)) + } + return m.write(data) + } + compressed := m.bufferPool.Get() + defer m.bufferPool.Put(compressed) + if err := m.compressionPool.Compress(compressed, uncompressed); err != nil { + return err + } + if m.sendMaxBytes > 0 && compressed.Len() > m.sendMaxBytes { + return NewError(CodeResourceExhausted, fmt.Errorf("compressed message size %d exceeds sendMaxBytes %d", compressed.Len(), m.sendMaxBytes)) + } + setHeaderCanonical(m.header, connectUnaryHeaderCompression, m.compressionName) + return m.write(compressed.Bytes()) +} + +func (m *connectUnaryMarshaler) write(data []byte) *Error { + m.wroteHeader = true + payload := bytes.NewReader(data) + if _, err := m.sender.Send(payload); err != nil { + err = wrapIfContextError(err) + if connectErr, ok := asError(err); ok { + return connectErr + } + return errorf(CodeUnknown, "write message: %w", err) + } + return nil +} + +type connectUnaryRequestMarshaler struct { + connectUnaryMarshaler + + enableGet bool + getURLMaxBytes int + getUseFallback bool + stableCodec stableCodec + duplexCall *duplexHTTPCall +} + +func (m *connectUnaryRequestMarshaler) Marshal(message any) *Error { + if m.enableGet { + if m.stableCodec == nil && !m.getUseFallback { + return errorf(CodeInternal, "codec %s doesn't support stable marshal; can't use get", m.codec.Name()) + } + if m.stableCodec != nil { + return m.marshalWithGet(message) + } + } + return m.connectUnaryMarshaler.Marshal(message) +} + +func (m *connectUnaryRequestMarshaler) marshalWithGet(message any) *Error { + // TODO(jchadwick-buf): This function is mostly a superset of + // connectUnaryMarshaler.Marshal. This should be reconciled at some point. + var data []byte + var err error + if message != nil { + data, err = m.stableCodec.MarshalStable(message) + if err != nil { + return errorf(CodeInternal, "marshal message stable: %w", err) + } + } + isTooBig := m.sendMaxBytes > 0 && len(data) > m.sendMaxBytes + if isTooBig && m.compressionPool == nil { + return NewError(CodeResourceExhausted, fmt.Errorf( + "message size %d exceeds sendMaxBytes %d: enabling request compression may help", + len(data), + m.sendMaxBytes, + )) + } + if !isTooBig { + url := m.buildGetURL(data, false /* compressed */) + if m.getURLMaxBytes <= 0 || len(url.String()) < m.getURLMaxBytes { + m.writeWithGet(url) + return nil + } + if m.compressionPool == nil { + if m.getUseFallback { + return m.write(data) + } + return NewError(CodeResourceExhausted, fmt.Errorf( + "url size %d exceeds getURLMaxBytes %d: enabling request compression may help", + len(url.String()), + m.getURLMaxBytes, + )) + } + } + // Compress message to try to make it fit in the URL. + uncompressed := bytes.NewBuffer(data) + defer m.bufferPool.Put(uncompressed) + compressed := m.bufferPool.Get() + defer m.bufferPool.Put(compressed) + if err := m.compressionPool.Compress(compressed, uncompressed); err != nil { + return err + } + if m.sendMaxBytes > 0 && compressed.Len() > m.sendMaxBytes { + return NewError(CodeResourceExhausted, fmt.Errorf("compressed message size %d exceeds sendMaxBytes %d", compressed.Len(), m.sendMaxBytes)) + } + url := m.buildGetURL(compressed.Bytes(), true /* compressed */) + if m.getURLMaxBytes <= 0 || len(url.String()) < m.getURLMaxBytes { + m.writeWithGet(url) + return nil + } + if m.getUseFallback { + setHeaderCanonical(m.header, connectUnaryHeaderCompression, m.compressionName) + return m.write(compressed.Bytes()) + } + return NewError(CodeResourceExhausted, fmt.Errorf("compressed url size %d exceeds getURLMaxBytes %d", len(url.String()), m.getURLMaxBytes)) +} + +func (m *connectUnaryRequestMarshaler) buildGetURL(data []byte, compressed bool) *url.URL { + url := *m.duplexCall.URL() + query := url.Query() + query.Set(connectUnaryConnectQueryParameter, connectUnaryConnectQueryValue) + query.Set(connectUnaryEncodingQueryParameter, m.codec.Name()) + if m.stableCodec.IsBinary() || compressed { + query.Set(connectUnaryMessageQueryParameter, encodeBinaryQueryValue(data)) + query.Set(connectUnaryBase64QueryParameter, "1") + } else { + query.Set(connectUnaryMessageQueryParameter, string(data)) + } + if compressed { + query.Set(connectUnaryCompressionQueryParameter, m.compressionName) + } + url.RawQuery = query.Encode() + return &url +} + +func (m *connectUnaryRequestMarshaler) writeWithGet(url *url.URL) { + delHeaderCanonical(m.header, connectHeaderProtocolVersion) + delHeaderCanonical(m.header, headerContentType) + delHeaderCanonical(m.header, headerContentEncoding) + delHeaderCanonical(m.header, headerContentLength) + m.duplexCall.SetMethod(http.MethodGet) + *m.duplexCall.URL() = *url +} + +type connectUnaryUnmarshaler struct { + ctx context.Context //nolint:containedctx + reader io.Reader + codec Codec + compressionPool *compressionPool + bufferPool *bufferPool + alreadyRead bool + readMaxBytes int +} + +func (u *connectUnaryUnmarshaler) Unmarshal(message any) *Error { + return u.UnmarshalFunc(message, u.codec.Unmarshal) +} + +func (u *connectUnaryUnmarshaler) UnmarshalFunc(message any, unmarshal func([]byte, any) error) *Error { + if u.alreadyRead { + return NewError(CodeInternal, io.EOF) + } + u.alreadyRead = true + data := u.bufferPool.Get() + defer u.bufferPool.Put(data) + reader := u.reader + if u.readMaxBytes > 0 && int64(u.readMaxBytes) < math.MaxInt64 { + reader = io.LimitReader(u.reader, int64(u.readMaxBytes)+1) + } + // ReadFor ignores io.EOF, so any error here is real. + bytesRead, err := data.ReadFrom(reader) + if err != nil { + err = wrapIfMaxBytesError(err, "read first %d bytes of message", bytesRead) + err = wrapIfContextDone(u.ctx, err) + if connectErr, ok := asError(err); ok { + return connectErr + } + return errorf(CodeUnknown, "read message: %w", err) + } + if u.readMaxBytes > 0 && bytesRead > int64(u.readMaxBytes) { + // Attempt to read to end in order to allow connection re-use + discardedBytes, err := io.Copy(io.Discard, u.reader) + if err != nil { + return errorf(CodeResourceExhausted, "message is larger than configured max %d - unable to determine message size: %w", u.readMaxBytes, err) + } + return errorf(CodeResourceExhausted, "message size %d is larger than configured max %d", bytesRead+discardedBytes, u.readMaxBytes) + } + if data.Len() > 0 && u.compressionPool != nil { + decompressed := u.bufferPool.Get() + defer u.bufferPool.Put(decompressed) + if err := u.compressionPool.Decompress(decompressed, data, int64(u.readMaxBytes)); err != nil { + return err + } + data = decompressed + } + if err := unmarshal(data.Bytes(), message); err != nil { + return errorf(CodeInvalidArgument, "unmarshal message: %w", err) + } + return nil +} + +type connectWireDetail ErrorDetail + +func (d *connectWireDetail) MarshalJSON() ([]byte, error) { + if d.wireJSON != "" { + // If we unmarshaled this detail from JSON, return the original data. This + // lets proxies w/o protobuf descriptors preserve human-readable details. + return []byte(d.wireJSON), nil + } + wire := struct { + Type string `json:"type"` + Value string `json:"value"` + Debug json.RawMessage `json:"debug,omitempty"` + }{ + Type: typeNameForURL(d.pbAny.GetTypeUrl()), + Value: base64.RawStdEncoding.EncodeToString(d.pbAny.GetValue()), + } + // Try to produce debug info, but expect failure when we don't have + // descriptors. + msg, err := d.getInner() + if err == nil { + var codec protoJSONCodec + debug, err := codec.Marshal(msg) + if err == nil { + wire.Debug = debug + } + } + return json.Marshal(wire) +} + +func (d *connectWireDetail) UnmarshalJSON(data []byte) error { + var wire struct { + Type string `json:"type"` + Value string `json:"value"` + } + if err := json.Unmarshal(data, &wire); err != nil { + return err + } + if !strings.Contains(wire.Type, "/") { + wire.Type = defaultAnyResolverPrefix + wire.Type + } + decoded, err := DecodeBinaryHeader(wire.Value) + if err != nil { + return fmt.Errorf("decode base64: %w", err) + } + *d = connectWireDetail{ + pbAny: &anypb.Any{ + TypeUrl: wire.Type, + Value: decoded, + }, + wireJSON: string(data), + } + return nil +} + +func (d *connectWireDetail) getInner() (proto.Message, error) { + if d.pbInner != nil { + return d.pbInner, nil + } + return d.pbAny.UnmarshalNew() +} + +type connectWireError struct { + Code Code `json:"code"` + Message string `json:"message,omitempty"` + Details []*connectWireDetail `json:"details,omitempty"` +} + +func newConnectWireError(err error) *connectWireError { + wire := &connectWireError{ + Code: CodeUnknown, + Message: err.Error(), + } + if connectErr, ok := asError(err); ok { + wire.Code = connectErr.Code() + wire.Message = connectErr.Message() + if len(connectErr.details) > 0 { + wire.Details = make([]*connectWireDetail, len(connectErr.details)) + for i, detail := range connectErr.details { + wire.Details[i] = (*connectWireDetail)(detail) + } + } + } + return wire +} + +func (e *connectWireError) asError() *Error { + if e == nil { + return nil + } + if e.Code < minCode || e.Code > maxCode { + e.Code = CodeUnknown + } + err := NewWireError(e.Code, errors.New(e.Message)) + if len(e.Details) > 0 { + err.details = make([]*ErrorDetail, len(e.Details)) + for i, detail := range e.Details { + err.details[i] = (*ErrorDetail)(detail) + } + } + return err +} + +func (e *connectWireError) UnmarshalJSON(data []byte) error { + // We want to be lenient if the JSON has an unrecognized or invalid code. + // So if that occurs, we leave the code unset but can still de-serialize + // the other fields from the input JSON. + var wireError struct { + Code string `json:"code"` + Message string `json:"message"` + Details []*connectWireDetail `json:"details"` + } + err := json.Unmarshal(data, &wireError) + if err != nil { + return err + } + e.Message = wireError.Message + e.Details = wireError.Details + // This will leave e.Code unset if we can't unmarshal the given string. + _ = e.Code.UnmarshalText([]byte(wireError.Code)) + return nil +} + +type connectEndStreamMessage struct { + Error *connectWireError `json:"error,omitempty"` + Trailer http.Header `json:"metadata,omitempty"` +} + +func connectCodeToHTTP(code Code) int { + // Return literals rather than named constants from the HTTP package to make + // it easier to compare this function to the Connect specification. + switch code { + case CodeCanceled: + return 499 + case CodeUnknown: + return 500 + case CodeInvalidArgument: + return 400 + case CodeDeadlineExceeded: + return 504 + case CodeNotFound: + return 404 + case CodeAlreadyExists: + return 409 + case CodePermissionDenied: + return 403 + case CodeResourceExhausted: + return 429 + case CodeFailedPrecondition: + return 400 + case CodeAborted: + return 409 + case CodeOutOfRange: + return 400 + case CodeUnimplemented: + return 501 + case CodeInternal: + return 500 + case CodeUnavailable: + return 503 + case CodeDataLoss: + return 500 + case CodeUnauthenticated: + return 401 + default: + return 500 // same as CodeUnknown + } +} + +func connectCodecForContentType(streamType StreamType, contentType string) string { + if streamType == StreamTypeUnary { + return strings.TrimPrefix(contentType, connectUnaryContentTypePrefix) + } + return strings.TrimPrefix(contentType, connectStreamingContentTypePrefix) +} + +func connectContentTypeForCodecName(streamType StreamType, name string) string { + if streamType == StreamTypeUnary { + return connectUnaryContentTypePrefix + name + } + return connectStreamingContentTypePrefix + name +} + +// encodeBinaryQueryValue URL-safe base64-encodes data, without padding. +func encodeBinaryQueryValue(data []byte) string { + return base64.RawURLEncoding.EncodeToString(data) +} + +// binaryQueryValueReader creates a reader that can read either padded or +// unpadded URL-safe base64 from a string. +func binaryQueryValueReader(data string) io.Reader { + stringReader := strings.NewReader(data) + if len(data)%4 != 0 { + // Data definitely isn't padded. + return base64.NewDecoder(base64.RawURLEncoding, stringReader) + } + // Data is padded, or no padding was necessary. + return base64.NewDecoder(base64.URLEncoding, stringReader) +} + +// queryValueReader creates a reader for a string that may be URL-safe base64 +// encoded. +func queryValueReader(data string, base64Encoded bool) io.Reader { + if base64Encoded { + return binaryQueryValueReader(data) + } + return strings.NewReader(data) +} + +func connectValidateUnaryResponseContentType( + requestCodecName string, + httpMethod string, + statusCode int, + statusMsg string, + responseContentType string, +) *Error { + if statusCode != http.StatusOK { + if statusCode == http.StatusNotModified && httpMethod == http.MethodGet { + return NewWireError(CodeUnknown, errNotModifiedClient) + } + // Error responses must be JSON-encoded. + if responseContentType == connectUnaryContentTypePrefix+codecNameJSON || + responseContentType == connectUnaryContentTypePrefix+codecNameJSONCharsetUTF8 { + return nil + } + return NewError( + httpToCode(statusCode), + errors.New(statusMsg), + ) + } + // Normal responses must have valid content-type that indicates same codec as the request. + if !strings.HasPrefix(responseContentType, connectUnaryContentTypePrefix) { + // Doesn't even look like a Connect response? Use code "unknown". + return errorf( + CodeUnknown, + "invalid content-type: %q; expecting %q", + responseContentType, + connectUnaryContentTypePrefix+requestCodecName, + ) + } + responseCodecName := connectCodecForContentType( + StreamTypeUnary, + responseContentType, + ) + if responseCodecName == requestCodecName { + return nil + } + // HACK: We likely want a better way to handle the optional "charset" parameter + // for application/json, instead of hard-coding. But this suffices for now. + if (responseCodecName == codecNameJSON && requestCodecName == codecNameJSONCharsetUTF8) || + (responseCodecName == codecNameJSONCharsetUTF8 && requestCodecName == codecNameJSON) { + // Both are JSON + return nil + } + return errorf( + CodeInternal, + "invalid content-type: %q; expecting %q", + responseContentType, + connectUnaryContentTypePrefix+requestCodecName, + ) +} + +func connectValidateStreamResponseContentType(requestCodecName string, streamType StreamType, responseContentType string) *Error { + // Responses must have valid content-type that indicates same codec as the request. + if !strings.HasPrefix(responseContentType, connectStreamingContentTypePrefix) { + // Doesn't even look like a Connect response? Use code "unknown". + return errorf( + CodeUnknown, + "invalid content-type: %q; expecting %q", + responseContentType, + connectStreamingContentTypePrefix+requestCodecName, + ) + } + responseCodecName := connectCodecForContentType( + streamType, + responseContentType, + ) + if responseCodecName != requestCodecName { + return errorf( + CodeInternal, + "invalid content-type: %q; expecting %q", + responseContentType, + connectStreamingContentTypePrefix+requestCodecName, + ) + } + return nil +} + +func connectCheckProtocolVersion(request *http.Request, required bool) *Error { + switch request.Method { + case http.MethodGet: + version := request.URL.Query().Get(connectUnaryConnectQueryParameter) + if version == "" && required { + return errorf(CodeInvalidArgument, "missing required query parameter: set %s to %q", connectUnaryConnectQueryParameter, connectUnaryConnectQueryValue) + } else if version != "" && version != connectUnaryConnectQueryValue { + return errorf(CodeInvalidArgument, "%s must be %q: got %q", connectUnaryConnectQueryParameter, connectUnaryConnectQueryValue, version) + } + case http.MethodPost: + version := getHeaderCanonical(request.Header, connectHeaderProtocolVersion) + if version == "" && required { + return errorf(CodeInvalidArgument, "missing required header: set %s to %q", connectHeaderProtocolVersion, connectProtocolVersion) + } else if version != "" && version != connectProtocolVersion { + return errorf(CodeInvalidArgument, "%s must be %q: got %q", connectHeaderProtocolVersion, connectProtocolVersion, version) + } + default: + return errorf(CodeInvalidArgument, "unsupported method: %q", request.Method) + } + return nil +} diff --git a/vendor/connectrpc.com/connect/protocol_grpc.go b/vendor/connectrpc.com/connect/protocol_grpc.go new file mode 100644 index 000000000000..4c0e8125e73f --- /dev/null +++ b/vendor/connectrpc.com/connect/protocol_grpc.go @@ -0,0 +1,1010 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "math" + "net/http" + "net/textproto" + "runtime" + "strconv" + "strings" + "time" + + statusv1 "connectrpc.com/connect/internal/gen/connectext/grpc/status/v1" +) + +const ( + grpcHeaderCompression = "Grpc-Encoding" + grpcHeaderAcceptCompression = "Grpc-Accept-Encoding" + grpcHeaderTimeout = "Grpc-Timeout" + grpcHeaderStatus = "Grpc-Status" + grpcHeaderMessage = "Grpc-Message" + grpcHeaderDetails = "Grpc-Status-Details-Bin" + + grpcFlagEnvelopeTrailer = 0b10000000 + + grpcContentTypeDefault = "application/grpc" + grpcWebContentTypeDefault = "application/grpc-web" + grpcContentTypePrefix = grpcContentTypeDefault + "+" + grpcWebContentTypePrefix = grpcWebContentTypeDefault + "+" + + headerXUserAgent = "X-User-Agent" + + upperhex = "0123456789ABCDEF" +) + +var ( + errTrailersWithoutGRPCStatus = fmt.Errorf("protocol error: no %s trailer: %w", grpcHeaderStatus, io.ErrUnexpectedEOF) + + // defaultGrpcUserAgent follows + // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#user-agents: + // + // While the protocol does not require a user-agent to function it is recommended + // that clients provide a structured user-agent string that provides a basic + // description of the calling library, version & platform to facilitate issue diagnosis + // in heterogeneous environments. The following structure is recommended to library developers: + // + // User-Agent → "grpc-" Language ?("-" Variant) "/" Version ?( " (" *(AdditionalProperty ";") ")" ) + // + //nolint:gochecknoglobals + defaultGrpcUserAgent = fmt.Sprintf("grpc-go-connect/%s (%s)", Version, runtime.Version()) + //nolint:gochecknoglobals + grpcAllowedMethods = map[string]struct{}{ + http.MethodPost: {}, + } +) + +type protocolGRPC struct { + web bool +} + +// NewHandler implements protocol, so it must return an interface. +func (g *protocolGRPC) NewHandler(params *protocolHandlerParams) protocolHandler { + bare, prefix := grpcContentTypeDefault, grpcContentTypePrefix + if g.web { + bare, prefix = grpcWebContentTypeDefault, grpcWebContentTypePrefix + } + contentTypes := make(map[string]struct{}) + for _, name := range params.Codecs.Names() { + contentTypes[canonicalizeContentType(prefix+name)] = struct{}{} + } + if params.Codecs.Get(codecNameProto) != nil { + contentTypes[bare] = struct{}{} + } + return &grpcHandler{ + protocolHandlerParams: *params, + web: g.web, + accept: contentTypes, + } +} + +// NewClient implements protocol, so it must return an interface. +func (g *protocolGRPC) NewClient(params *protocolClientParams) (protocolClient, error) { + peer := newPeerForURL(params.URL, ProtocolGRPC) + if g.web { + peer = newPeerForURL(params.URL, ProtocolGRPCWeb) + } + return &grpcClient{ + protocolClientParams: *params, + web: g.web, + peer: peer, + }, nil +} + +type grpcHandler struct { + protocolHandlerParams + + web bool + accept map[string]struct{} +} + +func (g *grpcHandler) Methods() map[string]struct{} { + return grpcAllowedMethods +} + +func (g *grpcHandler) ContentTypes() map[string]struct{} { + return g.accept +} + +func (*grpcHandler) SetTimeout(request *http.Request) (context.Context, context.CancelFunc, error) { + timeout, err := grpcParseTimeout(getHeaderCanonical(request.Header, grpcHeaderTimeout)) + if err != nil && !errors.Is(err, errNoTimeout) { + // Errors here indicate that the client sent an invalid timeout header, so + // the error text is safe to send back. + return nil, nil, NewError(CodeInvalidArgument, err) + } else if err != nil { + // err wraps errNoTimeout, nothing to do. + return request.Context(), nil, nil //nolint:nilerr + } + ctx, cancel := context.WithTimeout(request.Context(), timeout) + return ctx, cancel, nil +} + +func (g *grpcHandler) CanHandlePayload(_ *http.Request, contentType string) bool { + _, ok := g.accept[contentType] + return ok +} + +func (g *grpcHandler) NewConn( + responseWriter http.ResponseWriter, + request *http.Request, +) (handlerConnCloser, bool) { + ctx := request.Context() + // We need to parse metadata before entering the interceptor stack; we'll + // send the error to the client later on. + requestCompression, responseCompression, failed := negotiateCompression( + g.CompressionPools, + getHeaderCanonical(request.Header, grpcHeaderCompression), + getHeaderCanonical(request.Header, grpcHeaderAcceptCompression), + ) + if failed == nil { + failed = checkServerStreamsCanFlush(g.Spec, responseWriter) + } + + // Write any remaining headers here: + // (1) any writes to the stream will implicitly send the headers, so we + // should get all of gRPC's required response headers ready. + // (2) interceptors should be able to see these headers. + // + // Since we know that these header keys are already in canonical form, we can + // skip the normalization in Header.Set. + header := responseWriter.Header() + header[headerContentType] = []string{getHeaderCanonical(request.Header, headerContentType)} + header[grpcHeaderAcceptCompression] = []string{g.CompressionPools.CommaSeparatedNames()} + if responseCompression != compressionIdentity { + header[grpcHeaderCompression] = []string{responseCompression} + } + + codecName := grpcCodecForContentType(g.web, getHeaderCanonical(request.Header, headerContentType)) + codec := g.Codecs.Get(codecName) // handler.go guarantees this is not nil + protocolName := ProtocolGRPC + if g.web { + protocolName = ProtocolGRPCWeb + } + conn := wrapHandlerConnWithCodedErrors(&grpcHandlerConn{ + spec: g.Spec, + peer: Peer{ + Addr: request.RemoteAddr, + Protocol: protocolName, + }, + web: g.web, + bufferPool: g.BufferPool, + protobuf: g.Codecs.Protobuf(), // for errors + marshaler: grpcMarshaler{ + envelopeWriter: envelopeWriter{ + ctx: ctx, + sender: writeSender{writer: responseWriter}, + compressionPool: g.CompressionPools.Get(responseCompression), + codec: codec, + compressMinBytes: g.CompressMinBytes, + bufferPool: g.BufferPool, + sendMaxBytes: g.SendMaxBytes, + }, + }, + responseWriter: responseWriter, + responseHeader: make(http.Header), + responseTrailer: make(http.Header), + request: request, + unmarshaler: grpcUnmarshaler{ + envelopeReader: envelopeReader{ + ctx: ctx, + reader: request.Body, + codec: codec, + compressionPool: g.CompressionPools.Get(requestCompression), + bufferPool: g.BufferPool, + readMaxBytes: g.ReadMaxBytes, + }, + web: g.web, + }, + }) + if failed != nil { + // Negotiation failed, so we can't establish a stream. + _ = conn.Close(failed) + return nil, false + } + return conn, true +} + +type grpcClient struct { + protocolClientParams + + web bool + peer Peer +} + +func (g *grpcClient) Peer() Peer { + return g.peer +} + +func (g *grpcClient) WriteRequestHeader(_ StreamType, header http.Header) { + // We know these header keys are in canonical form, so we can bypass all the + // checks in Header.Set. + if getHeaderCanonical(header, headerUserAgent) == "" { + header[headerUserAgent] = []string{defaultGrpcUserAgent} + } + if g.web && getHeaderCanonical(header, headerXUserAgent) == "" { + // The gRPC-Web pseudo-specification seems to require X-User-Agent rather + // than User-Agent for all clients, even if they're not browser-based. This + // is very odd for a backend client, so we'll split the difference and set + // both. + header[headerXUserAgent] = []string{defaultGrpcUserAgent} + } + header[headerContentType] = []string{grpcContentTypeForCodecName(g.web, g.Codec.Name())} + // gRPC handles compression on a per-message basis, so we don't want to + // compress the whole stream. By default, http.Client will ask the server + // to gzip the stream if we don't set Accept-Encoding. + header["Accept-Encoding"] = []string{compressionIdentity} + if g.CompressionName != "" && g.CompressionName != compressionIdentity { + header[grpcHeaderCompression] = []string{g.CompressionName} + } + if acceptCompression := g.CompressionPools.CommaSeparatedNames(); acceptCompression != "" { + header[grpcHeaderAcceptCompression] = []string{acceptCompression} + } + if !g.web { + // The gRPC-HTTP2 specification requires this - it flushes out proxies that + // don't support HTTP trailers. + header["Te"] = []string{"trailers"} + } +} + +func (g *grpcClient) NewConn( + ctx context.Context, + spec Spec, + header http.Header, +) streamingClientConn { + if deadline, ok := ctx.Deadline(); ok { + encodedDeadline := grpcEncodeTimeout(time.Until(deadline)) + header[grpcHeaderTimeout] = []string{encodedDeadline} + } + duplexCall := newDuplexHTTPCall( + ctx, + g.HTTPClient, + g.URL, + spec, + header, + ) + conn := &grpcClientConn{ + spec: spec, + peer: g.Peer(), + duplexCall: duplexCall, + compressionPools: g.CompressionPools, + bufferPool: g.BufferPool, + protobuf: g.Protobuf, + marshaler: grpcMarshaler{ + envelopeWriter: envelopeWriter{ + ctx: ctx, + sender: duplexCall, + compressionPool: g.CompressionPools.Get(g.CompressionName), + codec: g.Codec, + compressMinBytes: g.CompressMinBytes, + bufferPool: g.BufferPool, + sendMaxBytes: g.SendMaxBytes, + }, + }, + unmarshaler: grpcUnmarshaler{ + envelopeReader: envelopeReader{ + ctx: ctx, + reader: duplexCall, + codec: g.Codec, + bufferPool: g.BufferPool, + readMaxBytes: g.ReadMaxBytes, + }, + }, + responseHeader: make(http.Header), + responseTrailer: make(http.Header), + } + duplexCall.SetValidateResponse(conn.validateResponse) + if g.web { + conn.unmarshaler.web = true + conn.readTrailers = func(unmarshaler *grpcUnmarshaler, _ *duplexHTTPCall) http.Header { + return unmarshaler.WebTrailer() + } + } else { + conn.readTrailers = func(_ *grpcUnmarshaler, call *duplexHTTPCall) http.Header { + // To access HTTP trailers, we need to read the body to EOF. + _, _ = discard(call) + return call.ResponseTrailer() + } + } + return wrapClientConnWithCodedErrors(conn) +} + +// grpcClientConn works for both gRPC and gRPC-Web. +type grpcClientConn struct { + spec Spec + peer Peer + duplexCall *duplexHTTPCall + compressionPools readOnlyCompressionPools + bufferPool *bufferPool + protobuf Codec // for errors + marshaler grpcMarshaler + unmarshaler grpcUnmarshaler + responseHeader http.Header + responseTrailer http.Header + readTrailers func(*grpcUnmarshaler, *duplexHTTPCall) http.Header +} + +func (cc *grpcClientConn) Spec() Spec { + return cc.spec +} + +func (cc *grpcClientConn) Peer() Peer { + return cc.peer +} + +func (cc *grpcClientConn) Send(msg any) error { + if err := cc.marshaler.Marshal(msg); err != nil { + return err + } + return nil // must be a literal nil: nil *Error is a non-nil error +} + +func (cc *grpcClientConn) RequestHeader() http.Header { + return cc.duplexCall.Header() +} + +func (cc *grpcClientConn) CloseRequest() error { + return cc.duplexCall.CloseWrite() +} + +func (cc *grpcClientConn) Receive(msg any) error { + if err := cc.duplexCall.BlockUntilResponseReady(); err != nil { + return err + } + err := cc.unmarshaler.Unmarshal(msg) + if err == nil { + return nil + } + mergeHeaders( + cc.responseTrailer, + cc.readTrailers(&cc.unmarshaler, cc.duplexCall), + ) + if errors.Is(err, io.EOF) && cc.unmarshaler.bytesRead == 0 && len(cc.responseTrailer) == 0 { + // No body and no trailers means a trailers-only response. + // Note: per the specification, only the HTTP status code and Content-Type + // should be treated as headers. The rest should be treated as trailing + // metadata. But it would be unsafe to mutate cc.responseHeader at this + // point. So we'll leave cc.responseHeader alone but copy the relevant + // metadata into cc.responseTrailer. + mergeHeaders(cc.responseTrailer, cc.responseHeader) + delHeaderCanonical(cc.responseTrailer, headerContentType) + + // Try to read the status out of the headers. + serverErr := grpcErrorForTrailer(cc.protobuf, cc.responseHeader) + if serverErr == nil { + // Status says "OK". So return original error (io.EOF). + return err + } + serverErr.meta = cc.responseHeader.Clone() + return serverErr + } + + // See if the server sent an explicit error in the HTTP or gRPC-Web trailers. + serverErr := grpcErrorForTrailer(cc.protobuf, cc.responseTrailer) + if serverErr != nil && (errors.Is(err, io.EOF) || !errors.Is(serverErr, errTrailersWithoutGRPCStatus)) { + // We've either: + // - Cleanly read until the end of the response body and *not* received + // gRPC status trailers, which is a protocol error, or + // - Received an explicit error from the server. + // + // This is expected from a protocol perspective, but receiving trailers + // means that we're _not_ getting a message. For users to realize that + // the stream has ended, Receive must return an error. + serverErr.meta = cc.responseHeader.Clone() + mergeHeaders(serverErr.meta, cc.responseTrailer) + _ = cc.duplexCall.CloseWrite() + return serverErr + } + // This was probably an error converting the bytes to a message or an error + // reading from the network. We're going to return it to the + // user, but we also want to close writes so Send errors out. + _ = cc.duplexCall.CloseWrite() + return err +} + +func (cc *grpcClientConn) ResponseHeader() http.Header { + _ = cc.duplexCall.BlockUntilResponseReady() + return cc.responseHeader +} + +func (cc *grpcClientConn) ResponseTrailer() http.Header { + _ = cc.duplexCall.BlockUntilResponseReady() + return cc.responseTrailer +} + +func (cc *grpcClientConn) CloseResponse() error { + return cc.duplexCall.CloseRead() +} + +func (cc *grpcClientConn) onRequestSend(fn func(*http.Request)) { + cc.duplexCall.onRequestSend = fn +} + +func (cc *grpcClientConn) validateResponse(response *http.Response) *Error { + if err := grpcValidateResponse( + response, + cc.responseHeader, + cc.compressionPools, + cc.unmarshaler.web, + cc.marshaler.codec.Name(), + ); err != nil { + return err + } + compression := getHeaderCanonical(response.Header, grpcHeaderCompression) + cc.unmarshaler.compressionPool = cc.compressionPools.Get(compression) + return nil +} + +type grpcHandlerConn struct { + spec Spec + peer Peer + web bool + bufferPool *bufferPool + protobuf Codec // for errors + marshaler grpcMarshaler + responseWriter http.ResponseWriter + responseHeader http.Header + responseTrailer http.Header + wroteToBody bool + request *http.Request + unmarshaler grpcUnmarshaler +} + +func (hc *grpcHandlerConn) Spec() Spec { + return hc.spec +} + +func (hc *grpcHandlerConn) Peer() Peer { + return hc.peer +} + +func (hc *grpcHandlerConn) Receive(msg any) error { + if err := hc.unmarshaler.Unmarshal(msg); err != nil { + return err // already coded + } + return nil // must be a literal nil: nil *Error is a non-nil error +} + +func (hc *grpcHandlerConn) RequestHeader() http.Header { + return hc.request.Header +} + +func (hc *grpcHandlerConn) Send(msg any) error { + defer flushResponseWriter(hc.responseWriter) + if !hc.wroteToBody { + mergeHeaders(hc.responseWriter.Header(), hc.responseHeader) + hc.wroteToBody = true + } + if err := hc.marshaler.Marshal(msg); err != nil { + return err + } + return nil // must be a literal nil: nil *Error is a non-nil error +} + +func (hc *grpcHandlerConn) ResponseHeader() http.Header { + return hc.responseHeader +} + +func (hc *grpcHandlerConn) ResponseTrailer() http.Header { + return hc.responseTrailer +} + +func (hc *grpcHandlerConn) Close(err error) (retErr error) { + defer func() { + // We don't want to copy unread portions of the body to /dev/null here: if + // the client hasn't closed the request body, we'll block until the server + // timeout kicks in. This could happen because the client is malicious, but + // a well-intentioned client may just not expect the server to be returning + // an error for a streaming RPC. Better to accept that we can't always reuse + // TCP connections. + closeErr := hc.request.Body.Close() + if retErr == nil { + retErr = closeErr + } + }() + defer flushResponseWriter(hc.responseWriter) + // If we haven't written the headers yet, do so. + if !hc.wroteToBody { + mergeHeaders(hc.responseWriter.Header(), hc.responseHeader) + } + // gRPC always sends the error's code, message, details, and metadata as + // trailing metadata. The Connect protocol doesn't do this, so we don't want + // to mutate the trailers map that the user sees. + mergedTrailers := make( + http.Header, + len(hc.responseTrailer)+2, // always make space for status & message + ) + mergeHeaders(mergedTrailers, hc.responseTrailer) + grpcErrorToTrailer(mergedTrailers, hc.protobuf, err) + if hc.web && !hc.wroteToBody && len(hc.responseHeader) == 0 { + // We're using gRPC-Web, we haven't yet written to the body, and there are no + // custom headers. That means we can send a "trailers-only" response and send + // trailing metadata as HTTP headers (instead of as trailers). + mergeHeaders(hc.responseWriter.Header(), mergedTrailers) + return nil + } + if hc.web { + // We're using gRPC-Web and we've already sent the headers, so we write + // trailing metadata to the HTTP body. + if err := hc.marshaler.MarshalWebTrailers(mergedTrailers); err != nil { + return err + } + return nil // must be a literal nil: nil *Error is a non-nil error + } + // We're using standard gRPC. Even if we haven't written to the body and + // we're sending a "trailers-only" response, we must send trailing metadata + // as HTTP trailers. (If we had frame-level control of the HTTP/2 layer, we + // could send trailers-only responses as a single HEADER frame and no DATA + // frames, but net/http doesn't expose APIs that low-level.) + // + // In net/http's ResponseWriter API, we send HTTP trailers by writing to the + // headers map with a special prefix. This prefixing is an implementation + // detail, so we should hide it and _not_ mutate the user-visible headers. + // + // Note that this is _very_ finicky and difficult to test with net/http, + // since correctness depends on low-level framing details. Breaking this + // logic breaks Envoy's gRPC-Web translation. + for key, values := range mergedTrailers { + for _, value := range values { + // These are potentially user-supplied, so we can't assume they're in + // canonical form. + hc.responseWriter.Header().Add(http.TrailerPrefix+key, value) + } + } + return nil +} + +type grpcMarshaler struct { + envelopeWriter +} + +func (m *grpcMarshaler) MarshalWebTrailers(trailer http.Header) *Error { + raw := m.envelopeWriter.bufferPool.Get() + defer m.envelopeWriter.bufferPool.Put(raw) + for key, values := range trailer { + // Per the Go specification, keys inserted during iteration may be produced + // later in the iteration or may be skipped. For safety, avoid mutating the + // map if the key is already lower-cased. + lower := strings.ToLower(key) + if key == lower { + continue + } + delete(trailer, key) + trailer[lower] = values + } + if err := trailer.Write(raw); err != nil { + return errorf(CodeInternal, "format trailers: %w", err) + } + return m.Write(&envelope{ + Data: raw, + Flags: grpcFlagEnvelopeTrailer, + }) +} + +type grpcUnmarshaler struct { + envelopeReader + + web bool + webTrailer http.Header +} + +func (u *grpcUnmarshaler) Unmarshal(message any) *Error { + err := u.envelopeReader.Unmarshal(message) + if err == nil { + return nil + } + if !errors.Is(err, errSpecialEnvelope) { + return err + } + env := u.last + data := env.Data + u.last.Data = nil // don't keep a reference to it + defer u.bufferPool.Put(data) + if !u.web || !env.IsSet(grpcFlagEnvelopeTrailer) { + return errorf(CodeInternal, "protocol error: invalid envelope flags %d", env.Flags) + } + + // Per the gRPC-Web specification, trailers should be encoded as an HTTP/1 + // headers block _without_ the terminating newline. To make the headers + // parseable by net/textproto, we need to add the newline. + if err := data.WriteByte('\n'); err != nil { + return errorf(CodeInternal, "unmarshal web trailers: %w", err) + } + bufferedReader := bufio.NewReader(data) + mimeReader := textproto.NewReader(bufferedReader) + mimeHeader, mimeErr := mimeReader.ReadMIMEHeader() + if mimeErr != nil { + return errorf( + CodeInternal, + "gRPC-Web protocol error: trailers invalid: %w", + mimeErr, + ) + } + u.webTrailer = http.Header(mimeHeader) + return errSpecialEnvelope +} + +func (u *grpcUnmarshaler) WebTrailer() http.Header { + return u.webTrailer +} + +func grpcValidateResponse( + response *http.Response, + header http.Header, + availableCompressors readOnlyCompressionPools, + web bool, + codecName string, +) *Error { + if response.StatusCode != http.StatusOK { + return errorf(httpToCode(response.StatusCode), "HTTP status %v", response.Status) + } + if err := grpcValidateResponseContentType( + web, + codecName, + getHeaderCanonical(response.Header, headerContentType), + ); err != nil { + return err + } + if compression := getHeaderCanonical(response.Header, grpcHeaderCompression); compression != "" && + compression != compressionIdentity && + !availableCompressors.Contains(compression) { + // Per https://github.com/grpc/grpc/blob/master/doc/compression.md, we + // should return CodeInternal and specify acceptable compression(s) (in + // addition to setting the Grpc-Accept-Encoding header). + return errorf( + CodeInternal, + "unknown encoding %q: accepted encodings are %v", + compression, + availableCompressors.CommaSeparatedNames(), + ) + } + // The response is valid, so we should expose the headers. + mergeHeaders(header, response.Header) + return nil +} + +// The gRPC wire protocol specifies that errors should be serialized using the +// binary Protobuf format, even if the messages in the request/response stream +// use a different codec. Consequently, this function needs a Protobuf codec to +// unmarshal error information in the headers. +// +// A nil error is only returned when a grpc-status key IS present, but it +// indicates a code of zero (no error). If no grpc-status key is present, this +// returns a non-nil *Error that wraps errTrailersWithoutGRPCStatus. +func grpcErrorForTrailer(protobuf Codec, trailer http.Header) *Error { + codeHeader := getHeaderCanonical(trailer, grpcHeaderStatus) + if codeHeader == "" { + // If there are no trailers at all, that's an internal error. + // But if it's an error determining the status code from the + // trailers, it's unknown. + code := CodeUnknown + if len(trailer) == 0 { + code = CodeInternal + } + return NewError(code, errTrailersWithoutGRPCStatus) + } + if codeHeader == "0" { + return nil + } + + code, err := strconv.ParseUint(codeHeader, 10 /* base */, 32 /* bitsize */) + if err != nil { + return errorf(CodeUnknown, "protocol error: invalid error code %q", codeHeader) + } + message, err := grpcPercentDecode(getHeaderCanonical(trailer, grpcHeaderMessage)) + if err != nil { + return errorf(CodeInternal, "protocol error: invalid error message %q", message) + } + retErr := NewWireError(Code(code), errors.New(message)) + + detailsBinaryEncoded := getHeaderCanonical(trailer, grpcHeaderDetails) + if len(detailsBinaryEncoded) > 0 { + detailsBinary, err := DecodeBinaryHeader(detailsBinaryEncoded) + if err != nil { + return errorf(CodeInternal, "server returned invalid grpc-status-details-bin trailer: %w", err) + } + var status statusv1.Status + if err := protobuf.Unmarshal(detailsBinary, &status); err != nil { + return errorf(CodeInternal, "server returned invalid protobuf for error details: %w", err) + } + for _, d := range status.GetDetails() { + retErr.details = append(retErr.details, &ErrorDetail{pbAny: d}) + } + // Prefer the Protobuf-encoded data to the headers (grpc-go does this too). + retErr.code = Code(status.GetCode()) //nolint:gosec // No information loss + retErr.err = errors.New(status.GetMessage()) + } + + return retErr +} + +func grpcParseTimeout(timeout string) (time.Duration, error) { + if timeout == "" { + return 0, errNoTimeout + } + unit, err := grpcTimeoutUnitLookup(timeout[len(timeout)-1]) + if err != nil { + return 0, err + } + num, err := strconv.ParseInt(timeout[:len(timeout)-1], 10 /* base */, 64 /* bitsize */) + if err != nil || num < 0 { + return 0, fmt.Errorf("protocol error: invalid timeout %q", timeout) + } + if num > 99999999 { // timeout must be ASCII string of at most 8 digits + return 0, fmt.Errorf("protocol error: timeout %q is too long", timeout) + } + const grpcTimeoutMaxHours = math.MaxInt64 / int64(time.Hour) // how many hours fit into a time.Duration? + if unit == time.Hour && num > grpcTimeoutMaxHours { + // Timeout is effectively unbounded, so ignore it. The grpc-go + // implementation does the same thing. + return 0, errNoTimeout + } + return time.Duration(num) * unit, nil +} + +func grpcEncodeTimeout(timeout time.Duration) string { + if timeout <= 0 { + return "0n" + } + // The gRPC protocol limits timeouts to 8 characters (not counting the unit), + // so timeouts must be strictly less than 1e8 of the appropriate unit. + const grpcTimeoutMaxValue = 1e8 + var ( + size time.Duration + unit byte + ) + switch { + case timeout < time.Nanosecond*grpcTimeoutMaxValue: + size, unit = time.Nanosecond, 'n' + case timeout < time.Microsecond*grpcTimeoutMaxValue: + size, unit = time.Microsecond, 'u' + case timeout < time.Millisecond*grpcTimeoutMaxValue: + size, unit = time.Millisecond, 'm' + case timeout < time.Second*grpcTimeoutMaxValue: + size, unit = time.Second, 'S' + case timeout < time.Minute*grpcTimeoutMaxValue: + size, unit = time.Minute, 'M' + default: + // time.Duration is an int64 number of nanoseconds, so the largest + // expressible duration is less than 1e8 hours. + size, unit = time.Hour, 'H' + } + buf := make([]byte, 0, 9) + buf = strconv.AppendInt(buf, int64(timeout/size), 10 /* base */) + buf = append(buf, unit) + return string(buf) +} + +func grpcTimeoutUnitLookup(unit byte) (time.Duration, error) { + switch unit { + case 'n': + return time.Nanosecond, nil + case 'u': + return time.Microsecond, nil + case 'm': + return time.Millisecond, nil + case 'S': + return time.Second, nil + case 'M': + return time.Minute, nil + case 'H': + return time.Hour, nil + default: + return 0, fmt.Errorf("protocol error: timeout has invalid unit %q", unit) + } +} + +func grpcCodecForContentType(web bool, contentType string) string { + if (!web && contentType == grpcContentTypeDefault) || (web && contentType == grpcWebContentTypeDefault) { + // implicitly protobuf + return codecNameProto + } + prefix := grpcContentTypePrefix + if web { + prefix = grpcWebContentTypePrefix + } + return strings.TrimPrefix(contentType, prefix) +} + +func grpcContentTypeForCodecName(web bool, name string) string { + if web { + return grpcWebContentTypePrefix + name + } + if name == codecNameProto { + // For compatibility with Google Cloud Platform's frontends, prefer an + // implicit default codec. See + // https://github.com/connectrpc/connect-go/pull/655#issuecomment-1915754523 + // for details. + return grpcContentTypeDefault + } + return grpcContentTypePrefix + name +} + +func grpcErrorToTrailer(trailer http.Header, protobuf Codec, err error) { + if err == nil { + setHeaderCanonical(trailer, grpcHeaderStatus, "0") // zero is the gRPC OK status + return + } + if connectErr, ok := asError(err); ok && !connectErr.wireErr { + mergeNonProtocolHeaders(trailer, connectErr.meta) + } + var ( + status = grpcStatusForError(err) + code = status.GetCode() + message = status.GetMessage() + bin []byte + ) + if len(status.Details) > 0 { + var binErr error + bin, binErr = protobuf.Marshal(status) + if binErr != nil { + code = int32(CodeInternal) + message = fmt.Sprintf("marshal protobuf status: %v", binErr) + } + } + setHeaderCanonical(trailer, grpcHeaderStatus, strconv.Itoa(int(code))) + setHeaderCanonical(trailer, grpcHeaderMessage, grpcPercentEncode(message)) + if len(bin) > 0 { + setHeaderCanonical(trailer, grpcHeaderDetails, EncodeBinaryHeader(bin)) + } +} + +func grpcStatusForError(err error) *statusv1.Status { + status := &statusv1.Status{ + Code: int32(CodeUnknown), + Message: err.Error(), + } + if connectErr, ok := asError(err); ok { + status.Code = int32(connectErr.Code()) //nolint:gosec // No information loss + status.Message = connectErr.Message() + status.Details = connectErr.detailsAsAny() + } + return status +} + +// grpcPercentEncode follows RFC 3986 Section 2.1 and the gRPC HTTP/2 spec. +// It's a variant of URL-encoding with fewer reserved characters. It's intended +// to take UTF-8 encoded text and escape non-ASCII bytes so that they're valid +// HTTP/1 headers, while still maximizing readability of the data on the wire. +// +// The grpc-message trailer (used for human-readable error messages) should be +// percent-encoded. +// +// References: +// +// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses +// https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 +func grpcPercentEncode(msg string) string { + var hexCount int + for i := range len(msg) { + if grpcShouldEscape(msg[i]) { + hexCount++ + } + } + if hexCount == 0 { + return msg + } + // We need to escape some characters, so we'll need to allocate a new string. + var out strings.Builder + out.Grow(len(msg) + 2*hexCount) + for i := range len(msg) { + switch char := msg[i]; { + case grpcShouldEscape(char): + out.WriteByte('%') + out.WriteByte(upperhex[char>>4]) + out.WriteByte(upperhex[char&15]) + default: + out.WriteByte(char) + } + } + return out.String() +} + +func grpcPercentDecode(input string) (string, error) { + percentCount := 0 + for i := 0; i < len(input); { + switch input[i] { + case '%': + percentCount++ + if err := validateHex(input[i:]); err != nil { + return "", err + } + i += 3 + default: + i++ + } + } + if percentCount == 0 { + return input, nil + } + // We need to unescape some characters, so we'll need to allocate a new string. + var out strings.Builder + out.Grow(len(input) - 2*percentCount) + for i := 0; i < len(input); i++ { + switch input[i] { + case '%': + out.WriteByte(unhex(input[i+1])<<4 | unhex(input[i+2])) + i += 2 + default: + out.WriteByte(input[i]) + } + } + return out.String(), nil +} + +// Characters that need to be escaped are defined in gRPC's HTTP/2 spec. +// They're different from the generic set defined in RFC 3986. +func grpcShouldEscape(char byte) bool { + return char < ' ' || char > '~' || char == '%' +} + +func unhex(char byte) byte { + switch { + case '0' <= char && char <= '9': + return char - '0' + case 'a' <= char && char <= 'f': + return char - 'a' + 10 + case 'A' <= char && char <= 'F': + return char - 'A' + 10 + } + return 0 +} + +func isHex(char byte) bool { + return ('0' <= char && char <= '9') || ('a' <= char && char <= 'f') || ('A' <= char && char <= 'F') +} + +func validateHex(input string) error { + if len(input) < 3 || input[0] != '%' || !isHex(input[1]) || !isHex(input[2]) { + if len(input) > 3 { + input = input[:3] + } + return fmt.Errorf("invalid percent-encoded string %q", input) + } + return nil +} + +func grpcValidateResponseContentType(web bool, requestCodecName string, responseContentType string) *Error { + // Responses must have valid content-type that indicates same codec as the request. + bare, prefix := grpcContentTypeDefault, grpcContentTypePrefix + if web { + bare, prefix = grpcWebContentTypeDefault, grpcWebContentTypePrefix + } + if responseContentType == prefix+requestCodecName || + (requestCodecName == codecNameProto && responseContentType == bare) { + return nil + } + expectedContentType := bare + if requestCodecName != codecNameProto { + expectedContentType = prefix + requestCodecName + } + code := CodeInternal + if responseContentType != bare && !strings.HasPrefix(responseContentType, prefix) { + // Doesn't even look like a gRPC response? Use code "unknown". + code = CodeUnknown + } + return errorf( + code, + "invalid content-type: %q; expecting %q", + responseContentType, + expectedContentType, + ) +} diff --git a/vendor/connectrpc.com/connect/recover.go b/vendor/connectrpc.com/connect/recover.go new file mode 100644 index 000000000000..2fee6ab274dc --- /dev/null +++ b/vendor/connectrpc.com/connect/recover.go @@ -0,0 +1,64 @@ +// Copyright 2021-2025 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "context" + "net/http" +) + +// recoverHandlerInterceptor lets handlers trap panics, perform side effects +// (like emitting logs or metrics), and present a friendlier error message to +// clients. +type recoverHandlerInterceptor struct { + Interceptor + + handle func(context.Context, Spec, http.Header, any) error +} + +func (i *recoverHandlerInterceptor) WrapUnary(next UnaryFunc) UnaryFunc { + return func(ctx context.Context, req AnyRequest) (_ AnyResponse, retErr error) { + if req.Spec().IsClient { + return next(ctx, req) + } + defer func() { + if r := recover(); r != nil { + // net/http checks for ErrAbortHandler with ==, so we should too. + if r == http.ErrAbortHandler { //nolint:errorlint,goerr113 + panic(r) //nolint:forbidigo + } + retErr = i.handle(ctx, req.Spec(), req.Header(), r) + } + }() + res, err := next(ctx, req) + return res, err + } +} + +func (i *recoverHandlerInterceptor) WrapStreamingHandler(next StreamingHandlerFunc) StreamingHandlerFunc { + return func(ctx context.Context, conn StreamingHandlerConn) (retErr error) { + defer func() { + if r := recover(); r != nil { + // net/http checks for ErrAbortHandler with ==, so we should too. + if r == http.ErrAbortHandler { //nolint:errorlint,goerr113 + panic(r) //nolint:forbidigo + } + retErr = i.handle(ctx, conn.Spec(), conn.RequestHeader(), r) + } + }() + err := next(ctx, conn) + return err + } +} diff --git a/vendor/github.com/docker/secrets-engine/client/.gomodguard.yaml b/vendor/github.com/docker/secrets-engine/client/.gomodguard.yaml new file mode 100644 index 000000000000..9cbeb987e84d --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/client/.gomodguard.yaml @@ -0,0 +1,13 @@ +--- +blocked: + modules: + - github.com/docker/secrets-engine/runtime: + reason: "Forbidden dependency" + - github.com/docker/secrets-engine/injector: + reason: "Forbidden dependency" + - github.com/docker/secrets-engine/plugin: + reason: "Forbidden dependency" + - github.com/docker/secrets-engine/plugins/pass: + reason: "Forbidden dependency" + - github.com/docker/secrets-engine/store: + reason: "Forbidden dependency" \ No newline at end of file diff --git a/vendor/github.com/docker/secrets-engine/client/LICENSE b/vendor/github.com/docker/secrets-engine/client/LICENSE new file mode 100644 index 000000000000..3c6f5a21a864 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/client/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/github.com/docker/secrets-engine/client/client.go b/vendor/github.com/docker/secrets-engine/client/client.go new file mode 100644 index 000000000000..83bff96a42ad --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/client/client.go @@ -0,0 +1,355 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package client + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "time" + + "connectrpc.com/connect" + + "github.com/docker/secrets-engine/client/dockerhub" + "github.com/docker/secrets-engine/x/api" + healthv1 "github.com/docker/secrets-engine/x/api/health/v1" + "github.com/docker/secrets-engine/x/api/health/v1/healthv1connect" + pluginsv1 "github.com/docker/secrets-engine/x/api/plugins/v1" + "github.com/docker/secrets-engine/x/api/plugins/v1/pluginsv1connect" + "github.com/docker/secrets-engine/x/api/resolver" + "github.com/docker/secrets-engine/x/secrets" +) + +type ( + Envelope = secrets.Envelope + ID = secrets.ID + Pattern = secrets.Pattern + AuthorizeResponse = secrets.AuthorizeResponse + DaemonVersion = api.DaemonVersion +) + +var ( + ParseID = secrets.ParseID + MustParseID = secrets.MustParseID + + ParsePattern = secrets.ParsePattern + MustParsePattern = secrets.MustParsePattern + + ErrSecretNotFound = secrets.ErrNotFound + ErrAccessDenied = secrets.ErrAccessDenied +) + +var _ secrets.Resolver = &client{} + +type Option func(c *config) error + +func WithSocketPath(path string) Option { + return func(s *config) error { + if path == "" { + return errors.New("no path provided") + } + if s.dialContext != nil { + return errors.New("cannot set socket path and dial") + } + s.socketPath = path + s.dialContext = dialFromPath(path) + return nil + } +} + +func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) Option { + return func(s *config) error { + if s.dialContext != nil { + return errors.New("cannot set socket path and dial") + } + s.dialContext = dialContext + return nil + } +} + +// WithTimeout overrides the request timeout of the client. +// +// It is useful to set if there are hard-limits to when the client must wait +// for the server to accept the request. +// +// A timeout of 0 means no request timeout will be applied. +// Negative durations are not allowed and will result in an error. +func WithTimeout(timeout time.Duration) Option { + return func(s *config) error { + if timeout < 0 { + return errors.New("request timeout duration cannot be negative") + } + s.requestTimeout = timeout + return nil + } +} + +// WithResponseTimeout overrides the response header timeout of the client. +// By default no timeout is applied, because a request may block on user +// interaction. 0 means no timeout; negative durations return an error. +func WithResponseTimeout(responseTimeout time.Duration) Option { + return func(s *config) error { + if responseTimeout < 0 { + return errors.New("response timeout duration cannot be negative") + } + s.responseTimeout = responseTimeout + return nil + } +} + +type dial func(ctx context.Context, network, addr string) (net.Conn, error) + +type config struct { + socketPath string + dialContext dial + requestTimeout time.Duration + responseTimeout time.Duration +} + +var ( + _ Client = &client{} + _ PluginManagement = &client{} +) + +type client struct { + socketPath string + resolverClient secrets.Resolver + engineClient pluginsv1connect.PluginManagementServiceClient + versionClient healthv1connect.VersionServiceClient + authorizerClient secrets.Authorizer +} + +// Authorize decides access to the patterns. The decision holds until the +// response expiry. A zero expiry means the decision never expires. +func (c client) Authorize(ctx context.Context, patterns ...secrets.Pattern) (secrets.AuthorizeResponse, error) { + resp, err := c.authorizerClient.Authorize(ctx, patterns...) + if isDialError(err) { + return secrets.AuthorizeResponse{}, c.connectError(err) + } + if err != nil { + return secrets.AuthorizeResponse{}, err + } + return resp, nil +} + +func (c client) GetSecrets(ctx context.Context, pattern secrets.Pattern) ([]secrets.Envelope, error) { + envelopes, err := c.resolverClient.GetSecrets(ctx, pattern) + if isDialError(err) { + return nil, c.connectError(err) + } + if err != nil { + return nil, err + } + return envelopes, nil +} + +func (c client) HubAuth(opts ...dockerhub.Option) dockerhub.ClientAuth { + return dockerhub.New(c, opts...) +} + +func (c client) Version(ctx context.Context) (DaemonVersion, error) { + resp, err := c.versionClient.GetVersion(ctx, connect.NewRequest(healthv1.GetVersionRequest_builder{}.Build())) + if isDialError(err) { + return DaemonVersion{}, c.connectError(err) + } + if err != nil { + return DaemonVersion{}, err + } + ver, err := api.NewVersion(resp.Msg.GetVersion()) + if err != nil { + return DaemonVersion{}, fmt.Errorf("parsing daemon version %q: %w", resp.Msg.GetVersion(), err) + } + return DaemonVersion{Version: ver, Date: resp.Msg.GetDate(), CommitHash: resp.Msg.GetCommitHash()}, nil +} + +// Client is the interface for interacting with the secrets engine daemon. +type Client interface { + secrets.Resolver + secrets.Authorizer + + // Version returns the name and version reported by the daemon. + Version(ctx context.Context) (DaemonVersion, error) + + // HubAuth returns a Docker Hub authentication accessor backed by this client. + HubAuth(opts ...dockerhub.Option) dockerhub.ClientAuth +} + +type PluginManagement interface { + ListPlugins(ctx context.Context) ([]PluginInfo, error) + EnablePlugin(ctx context.Context, name string) error + DisablePlugin(ctx context.Context, name string) error +} + +func PluginManagementFromClient(c Client) (PluginManagement, error) { + m, ok := c.(PluginManagement) + if !ok { + return nil, errors.New("client does not implement PluginManagement") + } + return m, nil +} + +func isDialError(err error) bool { + if err == nil { + return false + } + var oe *net.OpError + if errors.As(err, &oe) && (oe.Op == "dial" || oe.Op == "connect") { + return true + } + return false +} + +func (c client) connectError(err error) error { + if errors.Is(err, context.Canceled) { + return err + } + return &ConnectError{Reason: connectReason(err), SocketPath: c.socketPath, Err: err} +} + +// New creates a client that connects to [api.StandaloneSocketPath] by default. +// To connect to Docker Desktop, use WithSocketPath(api.DesktopSocketPath()). +func New(options ...Option) (Client, error) { + cfg := &config{ + requestTimeout: api.DefaultClientRequestTimeout, + responseTimeout: api.DefaultClientResponseHeaderTimeout, + } + for _, opt := range options { + if err := opt(cfg); err != nil { + return nil, err + } + } + if cfg.dialContext == nil { + cfg.socketPath = api.StandaloneSocketPath() + cfg.dialContext = dialFromPath(cfg.socketPath) + } + c := &http.Client{ + Transport: &http.Transport{ + // re-use the same connection to the runtime, this speeds up subsequent + // calls. + MaxConnsPerHost: api.DefaultClientMaxConnsPerHost, + MaxIdleConnsPerHost: api.DefaultClientMaxIdleConnsPerHost, + // keep the connection alive (good for long-lived clients) + IdleConnTimeout: api.DefaultClientIdleConnTimeout, + // no timeout by default; override with [WithResponseTimeout] + ResponseHeaderTimeout: cfg.responseTimeout, + TLSHandshakeTimeout: api.DefaultClientTLSHandshakeTimeout, + + DialContext: cfg.dialContext, + DisableKeepAlives: false, + DisableCompression: false, + ForceAttemptHTTP2: true, + }, + // by default Timeout will be 0 (meaning no timeout) + // it can be overwritten with [WithTimeout] + Timeout: cfg.requestTimeout, + } + return &client{ + socketPath: cfg.socketPath, + resolverClient: resolver.NewResolverClient(c), + engineClient: pluginsv1connect.NewPluginManagementServiceClient(c, "http://unix"), + versionClient: healthv1connect.NewVersionServiceClient(c, "http://unix"), + authorizerClient: resolver.NewAuthorizerClient(c), + }, nil +} + +func (c client) ListPlugins(ctx context.Context) ([]PluginInfo, error) { + req := connect.NewRequest(pluginsv1.ListPluginsRequest_builder{}.Build()) + resp, err := c.engineClient.ListPlugins(ctx, req) + if isDialError(err) { + return nil, c.connectError(err) + } + if err != nil { + return nil, err + } + var result []PluginInfo + for _, item := range resp.Msg.GetPlugins() { + name, err := api.NewName(item.GetName()) + if err != nil { + continue + } + version, err := api.NewVersion(item.GetVersion()) + if err != nil { + continue + } + info := PluginInfo{ + Name: name, + Version: version, + Disabled: item.GetDisabled(), + External: item.GetExternal(), + Configurable: item.GetConfigurable(), + RunStatus: item.GetRunStatus(), + StatusMessage: item.GetStatusMessage(), + } + switch item.WhichMetadata() { + case pluginsv1.Plugin_SecretsProvider_case: + pattern, err := secrets.ParsePattern(item.GetSecretsProvider().GetPattern()) + if err != nil { + continue + } + info.SecretsProvider = &SecretsProviderMetadata{Pattern: pattern} + case pluginsv1.Plugin_AccessControlModule_case: + info.AccessControlModule = &AccessControlModuleMetadata{} + } + result = append(result, info) + } + return result, nil +} + +func (c client) EnablePlugin(ctx context.Context, name string) error { + r := pluginsv1.EnablePluginRequest_builder{}.Build() + r.SetName(name) + _, err := c.engineClient.EnablePlugin(ctx, connect.NewRequest(r)) + if isDialError(err) { + return c.connectError(err) + } + return err +} + +func (c client) DisablePlugin(ctx context.Context, name string) error { + r := pluginsv1.DisablePluginRequest_builder{}.Build() + r.SetName(name) + _, err := c.engineClient.DisablePlugin(ctx, connect.NewRequest(r)) + if isDialError(err) { + return c.connectError(err) + } + return err +} + +type PluginInfo struct { + Name api.Name + Version api.Version + Disabled bool + External bool + Configurable bool + RunStatus pluginsv1.RunStatus + StatusMessage string + SecretsProvider *SecretsProviderMetadata + AccessControlModule *AccessControlModuleMetadata +} + +type AccessControlModuleMetadata struct{} + +type SecretsProviderMetadata struct { + Pattern secrets.Pattern +} + +func dialFromPath(path string) dial { + return func(ctx context.Context, _, _ string) (net.Conn, error) { + d := &net.Dialer{} + return d.DialContext(ctx, "unix", path) + } +} diff --git a/vendor/github.com/docker/secrets-engine/client/dockerhub/dockerhub.go b/vendor/github.com/docker/secrets-engine/client/dockerhub/dockerhub.go new file mode 100644 index 000000000000..cd85d8eb252c --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/client/dockerhub/dockerhub.go @@ -0,0 +1,415 @@ +// Copyright 2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package dockerhub reads Docker Hub access tokens and account profiles from +// the secrets engine. Obtain a [ClientAuth] from the client's HubAuth method, +// or from any [secrets.Resolver] via [New]. +package dockerhub + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "math" + "strconv" + "strings" + "time" + + "github.com/docker/secrets-engine/client/realms" + "github.com/docker/secrets-engine/x/secrets" +) + +var ( + // ErrNoSession means no Docker Hub credential is stored for the account. + ErrNoSession = errors.New("user is not authenticated for this application") + // ErrNoDefaultProfile means no account is set as the default. It wraps + // [ErrNoSession]. + ErrNoDefaultProfile = fmt.Errorf("no default account profile set: %w", ErrNoSession) + // ErrMissingAccessToken means the stored credential has no access token. + // It wraps [ErrNoSession]. + ErrMissingAccessToken error = &noSessionError{"no access token in payload"} + // ErrInvalidAccessToken means the stored access token is not a JWT. It + // wraps [ErrNoSession]. + ErrInvalidAccessToken error = &noSessionError{"access token is not a JWT"} +) + +type noSessionError struct{ msg string } + +func (e *noSessionError) Error() string { return e.msg } + +func (*noSessionError) Unwrap() error { return ErrNoSession } + +var ( + defaultProfileKey = secrets.MustParsePattern("default") + singleEntryKey = secrets.MustParsePattern("*") +) + +// UserSession is a stored Docker Hub credential. +type UserSession struct { + // AccessToken is a Docker Hub issued JWT. + AccessToken string `json:"access_token"` + // Claims are zero when the payload carries none. + Claims Claims `json:"claims"` +} + +// Expired reports whether the access token has expired. It is +// ExpiresWithin(0). +func (s UserSession) Expired() bool { + return s.ExpiresWithin(0) +} + +// ExpiresWithin reports whether the access token will expire within the next given duration. +// Sessions without [Claims.ExpiresAt] report as expired by default. +func (s UserSession) ExpiresWithin(leeway time.Duration) bool { + exp := s.Claims.ExpiresAt + if exp == nil || exp.IsZero() { + return true + } + return !time.Now().Add(leeway).Before(exp.Time) +} + +// Claims are the claims of a Docker Hub access token. +type Claims struct { + Issuer string `json:"iss,omitempty"` + Subject string `json:"sub,omitempty"` + Audience Audience `json:"aud,omitempty"` + ExpiresAt *NumericDate `json:"exp,omitempty"` + NotBefore *NumericDate `json:"nbf,omitempty"` + IssuedAt *NumericDate `json:"iat,omitempty"` + ID string `json:"jti,omitempty"` + + // Scope is a space-delimited list of granted scopes. + Scope string `json:"scope,omitempty"` + // AppName is the Docker client application the token was issued to. + AppName string `json:"app_name"` + UUID string `json:"uuid"` + // Source is formatted as `docker_{type}|{id}`. + Source string `json:"source"` + SessionID string `json:"session_id"` + ClientID string `json:"client_id,omitempty"` + ClientName string `json:"client_name,omitempty"` + Email string `json:"email"` + Username string `json:"username"` +} + +// NumericDate is an RFC 7519 numeric date: UNIX epoch seconds. It marshals +// truncated to whole seconds. +type NumericDate struct { + time.Time +} + +func (d NumericDate) MarshalJSON() ([]byte, error) { + return []byte(strconv.FormatInt(d.Unix(), 10)), nil +} + +func (d *NumericDate) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + return nil + } + var epoch float64 + if err := json.Unmarshal(data, &epoch); err != nil { + return fmt.Errorf("parse numeric date: %w", err) + } + if math.Abs(epoch) > 1e15 { + return fmt.Errorf("parse numeric date: %v is out of range", epoch) + } + seconds, fraction := math.Modf(epoch) + d.Time = time.Unix(int64(seconds), int64(fraction*float64(time.Second))) + return nil +} + +// Audience is the "aud" claim: a single string or an array of strings. +type Audience []string + +func (a *Audience) UnmarshalJSON(data []byte) error { + var value any + if err := json.Unmarshal(data, &value); err != nil { + return fmt.Errorf("parse audience: %w", err) + } + switch v := value.(type) { + case nil: + case string: + *a = Audience{v} + case []any: + audience := make(Audience, 0, len(v)) + for _, item := range v { + s, ok := item.(string) + if !ok { + return fmt.Errorf("parse audience: unexpected element type %T", item) + } + audience = append(audience, s) + } + *a = audience + default: + return fmt.Errorf("parse audience: unexpected type %T", value) + } + return nil +} + +// Profile describes a signed-in Docker Hub account. +type Profile struct { + // UserID is the secret ID where the account's credential is stored. + UserID string `json:"user_id"` + // OriginalSignInApp is the Docker client application the user signed in from. + OriginalSignInApp string `json:"original_sign_in_app"` + Username string `json:"username"` + Email string `json:"email"` + SignInDate time.Time `json:"sign_in_date"` +} + +// parseUserSession decodes a docker/auth/hub/** envelope into a [UserSession]. +func parseUserSession(envelope secrets.Envelope) (UserSession, error) { + var session UserSession + if err := json.Unmarshal(envelope.Value, &session); err != nil { + return UserSession{}, fmt.Errorf("decode user session: %w", err) + } + if session.AccessToken == "" { + return UserSession{}, fmt.Errorf("decode user session: %w", ErrMissingAccessToken) + } + if err := checkJWT(session.AccessToken); err != nil { + return UserSession{}, fmt.Errorf("decode user session: %w: %w", ErrInvalidAccessToken, err) + } + return session, nil +} + +func checkJWT(token string) error { + segments := strings.Split(token, ".") + if len(segments) != 3 { + return fmt.Errorf("expected 3 dot-separated segments, got %d", len(segments)) + } + var header struct { + Alg string `json:"alg"` + } + if err := decodeSegment(segments[0], &header); err != nil { + return fmt.Errorf("header: %w", err) + } + if header.Alg == "" { + return errors.New("header: missing alg") + } + var payload map[string]json.RawMessage + if err := decodeSegment(segments[1], &payload); err != nil { + return fmt.Errorf("payload: %w", err) + } + if payload == nil { + return errors.New("payload: not a JSON object") + } + if segments[2] == "" { + return errors.New("missing signature") + } + if _, err := base64.RawURLEncoding.DecodeString(segments[2]); err != nil { + return fmt.Errorf("signature: %w", err) + } + return nil +} + +func decodeSegment(segment string, v any) error { + data, err := base64.RawURLEncoding.DecodeString(segment) + if err != nil { + return err + } + return json.Unmarshal(data, v) +} + +// parseProfile decodes a docker/auth/metadata/hub/** envelope into a [Profile]. +func parseProfile(envelope secrets.Envelope) (Profile, error) { + var profile Profile + if err := json.Unmarshal(envelope.Value, &profile); err != nil { + return Profile{}, fmt.Errorf("decode profile metadata: %w", err) + } + if profile.UserID == "" { + return Profile{}, errors.New("decode profile metadata: no user ID in payload") + } + return profile, nil +} + +// ClientAuth reads Docker Hub authentication state from the secrets engine. +type ClientAuth interface { + // ListProfiles returns the profiles of all signed-in accounts. + ListProfiles(ctx context.Context) ([]Profile, error) + // GetDefaultProfile returns the default account's profile, or + // [ErrNoDefaultProfile] when no default is set. + GetDefaultProfile(ctx context.Context) (Profile, error) + // GetDefaultSession returns the default account's session: + // [ErrNoDefaultProfile] when no default is set, [ErrNoSession] when its + // credential is missing. + GetDefaultSession(ctx context.Context) (UserSession, error) + // GetSession returns the session for username, or [ErrNoSession]. + GetSession(ctx context.Context, username string) (UserSession, error) +} + +// Option configures a [ClientAuth]. +type Option func(*config) + +type config struct { + accounts secrets.Pattern + profiles secrets.Pattern + defaultEntry secrets.Pattern + accountEntry secrets.Pattern +} + +// Staging switches the lookup to the Docker Hub staging realms. +func Staging() Option { + return func(c *config) { + c.accounts = realms.DockerHubStagingAuthentication + c.profiles = realms.DockerHubStagingAuthenticationMetadata + } +} + +func newConfig(opts []Option) config { + cfg := config{ + accounts: realms.DockerHubAuthentication, + profiles: realms.DockerHubAuthenticationMetadata, + } + for _, opt := range opts { + opt(&cfg) + } + cfg.defaultEntry = mustExpand(cfg.profiles, defaultProfileKey) + cfg.accountEntry = mustExpand(cfg.accounts, singleEntryKey) + return cfg +} + +func mustExpand(realm, key secrets.Pattern) secrets.Pattern { + expanded, err := realm.ExpandPattern(key) + if err != nil { + panic(fmt.Sprintf("dockerhub: expand %s in %s: %v", key, realm, err)) + } + return expanded +} + +var _ ClientAuth = clientAuth{} + +type clientAuth struct { + engine secrets.Resolver + cfg config +} + +// New returns a [ClientAuth] backed by engine. It panics on a nil engine. +func New(engine secrets.Resolver, opts ...Option) ClientAuth { + if engine == nil { + panic("dockerhub: secrets engine client is required") + } + return clientAuth{engine: engine, cfg: newConfig(opts)} +} + +func (c clientAuth) ListProfiles(ctx context.Context) ([]Profile, error) { + envelopes, err := c.engine.GetSecrets(ctx, c.cfg.profiles) + if errors.Is(err, secrets.ErrNotFound) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("list account profiles: %w", err) + } + var profiles []Profile + var errs []error + seen := make(map[string]bool, len(envelopes)) + for _, envelope := range envelopes { + // The default entry duplicates an account's profile. + if envelope.ID != nil && c.cfg.defaultEntry.Match(envelope.ID) { + continue + } + profile, err := parseProfile(envelope) + if err != nil { + errs = append(errs, err) + continue + } + if seen[profile.UserID] { + continue + } + seen[profile.UserID] = true + profiles = append(profiles, profile) + } + if len(profiles) == 0 && len(errs) > 0 { + return nil, errors.Join(errs...) + } + return profiles, nil +} + +func (c clientAuth) GetDefaultProfile(ctx context.Context) (Profile, error) { + envelopes, err := c.engine.GetSecrets(ctx, c.cfg.defaultEntry) + if errors.Is(err, secrets.ErrNotFound) { + return Profile{}, ErrNoDefaultProfile + } + if err != nil { + return Profile{}, fmt.Errorf("retrieve default account metadata: %w", err) + } + return parseFirst(envelopes, parseProfile, ErrNoDefaultProfile) +} + +func (c clientAuth) GetDefaultSession(ctx context.Context) (UserSession, error) { + profile, err := c.GetDefaultProfile(ctx) + if err != nil { + return UserSession{}, err + } + // Require a wildcard-free ID naming one account entry in the accounts + // realm, so a tampered profile cannot address an arbitrary secret. + id, err := secrets.ParseID(profile.UserID) + if err != nil { + return UserSession{}, fmt.Errorf("default profile user id: %w", err) + } + if !c.cfg.accountEntry.Match(id) { + return UserSession{}, fmt.Errorf("default profile user id %q is not an account entry in the %s realm", profile.UserID, c.cfg.accounts) + } + return c.getSession(ctx, exactPattern(id)) +} + +func (c clientAuth) GetSession(ctx context.Context, username string) (UserSession, error) { + if strings.Contains(username, "/") { + return UserSession{}, fmt.Errorf("invalid username %q: must not contain '/'", username) + } + user, err := secrets.ParseID(username) + if err != nil { + return UserSession{}, fmt.Errorf("invalid username %q: %w", username, err) + } + id, err := c.cfg.accounts.ExpandID(user) + if err != nil { + return UserSession{}, err + } + return c.getSession(ctx, exactPattern(id)) +} + +func (c clientAuth) getSession(ctx context.Context, pattern secrets.Pattern) (UserSession, error) { + envelopes, err := c.engine.GetSecrets(ctx, pattern) + if errors.Is(err, secrets.ErrNotFound) { + return UserSession{}, ErrNoSession + } + if err != nil { + return UserSession{}, fmt.Errorf("retrieve user access token: %w", err) + } + return parseFirst(envelopes, parseUserSession, ErrNoSession) +} + +func exactPattern(id secrets.ID) secrets.Pattern { + return secrets.MustParsePattern(id.String()) +} + +// parseFirst returns the first envelope that parses. It returns notFound when +// there are no envelopes, and the joined decode errors when none parses. +func parseFirst[T any](envelopes []secrets.Envelope, parse func(secrets.Envelope) (T, error), notFound error) (T, error) { + var zero T + var errs []error + for _, envelope := range envelopes { + v, err := parse(envelope) + if err != nil { + errs = append(errs, err) + continue + } + return v, nil + } + if len(errs) == 0 { + return zero, notFound + } + return zero, errors.Join(errs...) +} diff --git a/vendor/github.com/docker/secrets-engine/client/errno_unix.go b/vendor/github.com/docker/secrets-engine/client/errno_unix.go new file mode 100644 index 000000000000..512f6aa7afce --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/client/errno_unix.go @@ -0,0 +1,22 @@ +// Copyright 2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows + +package client + +import "syscall" + +// errConnRefused is returned when dialing a socket nothing listens on. +const errConnRefused = syscall.ECONNREFUSED diff --git a/vendor/github.com/docker/secrets-engine/client/errno_windows.go b/vendor/github.com/docker/secrets-engine/client/errno_windows.go new file mode 100644 index 000000000000..f305cf75248a --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/client/errno_windows.go @@ -0,0 +1,20 @@ +// Copyright 2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package client + +import "syscall" + +// errConnRefused is WSAECONNREFUSED; Winsock never returns syscall.ECONNREFUSED. +const errConnRefused = syscall.Errno(10061) diff --git a/vendor/github.com/docker/secrets-engine/client/errors.go b/vendor/github.com/docker/secrets-engine/client/errors.go new file mode 100644 index 000000000000..3adf788fa401 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/client/errors.go @@ -0,0 +1,173 @@ +// Copyright 2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package client + +import ( + "context" + "errors" + "fmt" + "io/fs" + "net" + "path/filepath" + + "github.com/docker/secrets-engine/x/api" +) + +var ( + ErrSecretsEngineNotRunning = errors.New("secrets engine is not running") + ErrSecretsEnginePermissionDenied = errors.New("permission denied connecting to the secrets engine") + ErrSecretsEngineTimeout = errors.New("secrets engine did not respond in time") +) + +type ConnectReason int + +const ( + ReasonUnknown ConnectReason = iota + ReasonNotRunning + ReasonPermissionDenied + ReasonTimeout +) + +func (r ConnectReason) String() string { + switch r { + case ReasonNotRunning: + return "not running" + case ReasonPermissionDenied: + return "permission denied" + case ReasonTimeout: + return "timeout" + default: + return "unknown" + } +} + +func (r ConnectReason) sentinel() error { + switch r { + case ReasonNotRunning: + return ErrSecretsEngineNotRunning + case ReasonPermissionDenied: + return ErrSecretsEnginePermissionDenied + case ReasonTimeout: + return ErrSecretsEngineTimeout + default: + return nil + } +} + +type ConnectError struct { + Reason ConnectReason + SocketPath string + Err error +} + +func (e *ConnectError) Error() string { + msg := "cannot connect to the secrets engine" + if s := e.Reason.sentinel(); s != nil { + msg = s.Error() + } + if e.SocketPath != "" { + msg += " at " + e.SocketPath + } + if e.Err != nil { + msg += ": " + e.Err.Error() + } + return msg +} + +func (e *ConnectError) Unwrap() error { return e.Err } + +func (e *ConnectError) Is(target error) bool { + s := e.Reason.sentinel() + return s != nil && target == s +} + +const ( + desktopNotRunningHint = "Start Docker Desktop and retry: the secrets engine runs as part of it." + desktopTimeoutHint = "The secrets engine did not respond. Restart Docker Desktop and retry." + standaloneNotRunningHint = "Start the standalone secrets engine and retry: nothing is listening on %s." + standaloneTimeoutHint = "The standalone secrets engine on %s did not respond. Restart it and retry." + customNotRunningHint = "Start the secrets engine and retry: nothing is listening on %s." + customTimeoutHint = "The secrets engine on %s did not respond. Restart it and retry." + permissionDeniedHint = "Your user lacks permission to connect to %s. Grant it read and write access to the socket and retry." +) + +// Hint returns a user friendly message to help recover from the error. +func (e *ConnectError) Hint() string { + socket := "the secrets engine socket" + if e.SocketPath != "" { + socket = fmt.Sprintf("%q", e.SocketPath) + } + kind := classifySocket(e.SocketPath) + switch e.Reason { + case ReasonNotRunning: + switch kind { + case engineDesktop: + return desktopNotRunningHint + case engineStandalone: + return fmt.Sprintf(standaloneNotRunningHint, socket) + default: + return fmt.Sprintf(customNotRunningHint, socket) + } + case ReasonTimeout: + switch kind { + case engineDesktop: + return desktopTimeoutHint + case engineStandalone: + return fmt.Sprintf(standaloneTimeoutHint, socket) + default: + return fmt.Sprintf(customTimeoutHint, socket) + } + case ReasonPermissionDenied: + return fmt.Sprintf(permissionDeniedHint, socket) + default: + return "" + } +} + +type engineKind int + +const ( + engineCustom engineKind = iota + engineDesktop + engineStandalone +) + +func classifySocket(path string) engineKind { + if path == "" { + return engineCustom + } + switch filepath.Clean(path) { + case filepath.Clean(api.DesktopSocketPath()): + return engineDesktop + case filepath.Clean(api.StandaloneSocketPath()): + return engineStandalone + default: + return engineCustom + } +} + +func connectReason(err error) ConnectReason { + var ne net.Error + switch { + case errors.Is(err, fs.ErrNotExist), errors.Is(err, errConnRefused): + return ReasonNotRunning + case errors.Is(err, fs.ErrPermission): + return ReasonPermissionDenied + case errors.Is(err, context.DeadlineExceeded), errors.As(err, &ne) && ne.Timeout(): + return ReasonTimeout + default: + return ReasonUnknown + } +} diff --git a/vendor/github.com/docker/secrets-engine/client/realms/docker.go b/vendor/github.com/docker/secrets-engine/client/realms/docker.go new file mode 100644 index 000000000000..d730fca3ad81 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/client/realms/docker.go @@ -0,0 +1,86 @@ +// Copyright 2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package realms exposes the canonical set of Docker realm patterns that +// clients use to scope secret queries. +// +// A realm is a path-pattern contract - not a permission boundary. Clients use +// realms to declare which set of secrets they care about (e.g. Hub auth, +// registry auth, MCP OAuth). Once a realm is established its pattern MUST NOT +// change; clients may pin to a realm and treat it as a stable interface. +// +// The Docker realm hierarchy is: +// +// docker/ – all Docker secrets +// docker/auth/hub/** – Docker Hub authentication (OAuth login) +// docker/auth/hub-staging/** – Docker Hub staging authentication +// docker/auth/registry/docker/** – Docker Registry authentication +// docker/auth/metadata/hub/** – Hub account profile metadata +// docker/mcp/** – MCP-related secrets +// docker/mcp/oauth/** – MCP OAuth credentials +// docker/mcp/oauth-dcr/** – MCP Dynamic Client Registration configs +// docker/sandbox/** – Sandbox-related secrets +// docker/sandbox/oauth/** – Sandbox third-party OAuth tokens +// +// All variables in this package are re-exported from +// github.com/docker/secrets-engine/x/realms and are provided here as a +// stable, versioned surface for external consumers. +package realms + +import xrealms "github.com/docker/secrets-engine/x/realms" + +// Docker realms all start with `docker/` as the prefix. +// +// Authentication flows done by the Docker CLI, Docker Desktop and Docker related +// products must go through `docker/auth`. +// +// Docker Hub authentication (browser based OAuth login) will be prefixed +// with `docker/auth/hub/`. +// +// Docker Registry authentication will be prefixed with +// `docker/auth/registry/docker/`. +var ( + DockerHubAuthentication = xrealms.DockerHubAuthentication + DockerHubStagingAuthentication = xrealms.DockerHubStagingAuthentication + DockerRegistryAuthentication = xrealms.DockerRegistryAuthentication + DockerRegistryStagingAuthentication = xrealms.DockerRegistryStagingAuthentication +) + +var ( + // DockerHubAuthenticationMetadata holds one profile entry per signed-in + // Docker Hub account, plus a default entry duplicating the default + // account's profile. + DockerHubAuthenticationMetadata = xrealms.DockerHubAuthenticationMetadata + // DockerHubStagingAuthenticationMetadata is the staging variant of + // [DockerHubAuthenticationMetadata]. + DockerHubStagingAuthenticationMetadata = xrealms.DockerHubStagingAuthenticationMetadata +) + +var ( + // DockerMCPDefault is the default realm used for MCP related secrets + DockerMCPDefault = xrealms.DockerMCPDefault + // DockerMCPOAuth is the realm used for all MCP OAuth credentials retrieved by Docker. + DockerMCPOAuth = xrealms.DockerMCPOAuth + // DockerMCPOAuthDCR is the realm used to hold Dynamic Client Registered (DCR) + // OAuth configurations for supported MCP servers. + DockerMCPOAuthDCR = xrealms.DockerMCPOAuthDCR +) + +var ( + // DockerSandbox is the default realm used for Sandbox related secrets + DockerSandbox = xrealms.DockerSandbox + // DockerSandboxOAuth is the realm used for all Sandbox OAuth credentials + // such as third-party tokens - this does not store the Docker Auth tokens. + DockerSandboxOAuth = xrealms.DockerSandboxOAuth +) diff --git a/vendor/github.com/docker/secrets-engine/x/LICENSE b/vendor/github.com/docker/secrets-engine/x/LICENSE new file mode 100644 index 000000000000..3c6f5a21a864 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/github.com/docker/secrets-engine/x/api/daemon.go b/vendor/github.com/docker/secrets-engine/x/api/daemon.go new file mode 100644 index 000000000000..cc35cc2e7cdf --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/daemon.go @@ -0,0 +1,22 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +// DaemonVersion holds version information reported by the daemon. +type DaemonVersion struct { + Version Version + Date string + CommitHash string +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/defaults.go b/vendor/github.com/docker/secrets-engine/x/api/defaults.go new file mode 100644 index 000000000000..4edee29139de --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/defaults.go @@ -0,0 +1,81 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "os" + "path/filepath" + "time" +) + +const ( + // PluginLaunchedByEngineVar is used to inform engine-launched plugins about their name. + PluginLaunchedByEngineVar = "DOCKER_SECRETS_ENGINE_PLUGIN_LAUNCH_CFG" + // DefaultPluginRegistrationTimeout is the default timeout for plugin registration. + DefaultPluginRegistrationTimeout = 5 * time.Second + // DefaultClientRequestTimeout is the default timeout for clients to handle a request. + DefaultClientRequestTimeout = time.Duration(0) // 0 means no limit + // DefaultClientResponseHeaderTimeout is the default timeout for response + // headers, excluding the body. No limit by default: a request may block + // on user interaction daemon-side. Use [client.WithResponseTimeout] or a + // context deadline for a hard bound. + DefaultClientResponseHeaderTimeout = time.Duration(0) // 0 means no limit + // DefaultClientTLSHandshakeTimeout is the default timeout for clients to handle + // tls handshakes. It should usually be short. + DefaultClientTLSHandshakeTimeout = time.Second + // DefaultClientIdleConnTimeout is the time a connection may stay alive for. + // Clients that are long-lived take advantage of re-using the same connection + // when making subsequent requests. This reduces latency. + DefaultClientIdleConnTimeout = 90 * time.Second + // DefaultClientMaxConnsPerHost is the maximum number of open connections + // to the same host. + DefaultClientMaxConnsPerHost = 100 + // DefaultClientMaxIdleConnsPerHost is the maximum number of idle connections + // to the same host. Long-lived clients can re-use a connection from the + // connection pool. + DefaultClientMaxIdleConnsPerHost = 10 +) + +// DesktopSocketPath returns the Secrets Engine socket path used by Docker Desktop. +// +// - Linux: $XDG_CACHE_HOME/docker-secrets-engine/engine.sock, or +// $HOME/.cache/docker-secrets-engine/engine.sock when XDG_CACHE_HOME is unset. +// - macOS: $HOME/Library/Caches/docker-secrets-engine/engine.sock. +// - Windows: %LOCALAPPDATA%\docker-secrets-engine\engine.sock. +// +// If the user cache directory cannot be determined, it returns +// /docker-secrets-engine/engine.sock, where is the directory returned +// by [os.TempDir]. +func DesktopSocketPath() string { + if dir, err := os.UserCacheDir(); err == nil { + return filepath.Join(dir, "docker-secrets-engine", "engine.sock") + } + return filepath.Join(os.TempDir(), "docker-secrets-engine", "engine.sock") +} + +// DefaultSocketPath returns the Secrets Engine socket path used by Docker Desktop. +// +// Deprecated: Use [DesktopSocketPath] instead. +func DefaultSocketPath() string { + return DesktopSocketPath() +} + +func DefaultSecretsEngineDirectory() (string, error) { + dir, err := os.UserConfigDir() + if err != nil { + return "", err + } + return filepath.Join(dir, "docker-secrets-engine"), nil +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/defaults_darwin.go b/vendor/github.com/docker/secrets-engine/x/api/defaults_darwin.go new file mode 100644 index 000000000000..5500cdecd225 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/defaults_darwin.go @@ -0,0 +1,40 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build darwin + +package api + +import ( + "os" + "path/filepath" +) + +// StandaloneSocketPath returns the standalone Secrets Engine's listening socket +// path, $HOME/Library/Caches/docker-secrets-engine/daemon.sock. +// If HOME is unset, it uses $TMPDIR/docker-secrets-engine/daemon.sock, or +// /tmp/docker-secrets-engine/daemon.sock when TMPDIR is also unset. +func StandaloneSocketPath() string { + if dir, err := os.UserCacheDir(); err == nil { + return filepath.Join(dir, "docker-secrets-engine", "daemon.sock") + } + return filepath.Join(os.TempDir(), "docker-secrets-engine", "daemon.sock") +} + +// DaemonSocketPath returns the standalone Secrets Engine's listening socket path. +// +// Deprecated: Use [StandaloneSocketPath] instead. +func DaemonSocketPath() string { + return StandaloneSocketPath() +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/defaults_linux.go b/vendor/github.com/docker/secrets-engine/x/api/defaults_linux.go new file mode 100644 index 000000000000..802b4abaf866 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/defaults_linux.go @@ -0,0 +1,45 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux + +package api + +import ( + "fmt" + "os" +) + +// StandaloneSocketPath returns the address of the standalone Secrets Engine's +// listening socket, @docker-secrets-engine//daemon.sock. +// For example, UID 1000 uses @docker-secrets-engine/1000/daemon.sock. +// +// On Linux it is an abstract Unix domain socket: the address has a leading +// "@", which Go's net package maps to a NUL byte, placing the socket in the +// abstract namespace instead of on the filesystem. +// +// The address is namespaced by the user's UID so daemons run by different +// users on the same host do not collide (the abstract namespace is shared per +// network namespace, not per user as a filesystem path would be). +func StandaloneSocketPath() string { + return fmt.Sprintf("@docker-secrets-engine/%d/daemon.sock", os.Getuid()) +} + +// DaemonSocketPath returns the address of the standalone Secrets Engine's +// listening socket. +// +// Deprecated: Use [StandaloneSocketPath] instead. +func DaemonSocketPath() string { + return StandaloneSocketPath() +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/defaults_windows.go b/vendor/github.com/docker/secrets-engine/x/api/defaults_windows.go new file mode 100644 index 000000000000..5e6ebb1c2035 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/defaults_windows.go @@ -0,0 +1,41 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows + +package api + +import ( + "os" + "path/filepath" +) + +// StandaloneSocketPath returns the standalone Secrets Engine's listening socket +// path, %LOCALAPPDATA%\DockerSecretsEngine\service\daemon.sock. +// If LOCALAPPDATA is unset, it uses +// %USERPROFILE%\AppData\Local\DockerSecretsEngine\service\daemon.sock. +func StandaloneSocketPath() string { + base := os.Getenv("LOCALAPPDATA") + if base == "" { + base = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local") + } + return filepath.Join(base, "DockerSecretsEngine", "service", "daemon.sock") +} + +// DaemonSocketPath returns the standalone Secrets Engine's listening socket path. +// +// Deprecated: Use [StandaloneSocketPath] instead. +func DaemonSocketPath() string { + return StandaloneSocketPath() +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/health/v1/api.pb.go b/vendor/github.com/docker/secrets-engine/x/api/health/v1/api.pb.go new file mode 100644 index 000000000000..ec9782710f5e --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/health/v1/api.pb.go @@ -0,0 +1,264 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc (unknown) +// source: health/v1/api.proto + +package healthv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetVersionRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetVersionRequest) Reset() { + *x = GetVersionRequest{} + mi := &file_health_v1_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetVersionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetVersionRequest) ProtoMessage() {} + +func (x *GetVersionRequest) ProtoReflect() protoreflect.Message { + mi := &file_health_v1_api_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +type GetVersionRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + +} + +func (b0 GetVersionRequest_builder) Build() *GetVersionRequest { + m0 := &GetVersionRequest{} + b, x := &b0, m0 + _, _ = b, x + return m0 +} + +type GetVersionResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Version *string `protobuf:"bytes,1,opt,name=version"` + xxx_hidden_Date *string `protobuf:"bytes,2,opt,name=date"` + xxx_hidden_CommitHash *string `protobuf:"bytes,3,opt,name=commit_hash,json=commitHash"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetVersionResponse) Reset() { + *x = GetVersionResponse{} + mi := &file_health_v1_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetVersionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetVersionResponse) ProtoMessage() {} + +func (x *GetVersionResponse) ProtoReflect() protoreflect.Message { + mi := &file_health_v1_api_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *GetVersionResponse) GetVersion() string { + if x != nil { + if x.xxx_hidden_Version != nil { + return *x.xxx_hidden_Version + } + return "" + } + return "" +} + +func (x *GetVersionResponse) GetDate() string { + if x != nil { + if x.xxx_hidden_Date != nil { + return *x.xxx_hidden_Date + } + return "" + } + return "" +} + +func (x *GetVersionResponse) GetCommitHash() string { + if x != nil { + if x.xxx_hidden_CommitHash != nil { + return *x.xxx_hidden_CommitHash + } + return "" + } + return "" +} + +func (x *GetVersionResponse) SetVersion(v string) { + x.xxx_hidden_Version = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 3) +} + +func (x *GetVersionResponse) SetDate(v string) { + x.xxx_hidden_Date = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 3) +} + +func (x *GetVersionResponse) SetCommitHash(v string) { + x.xxx_hidden_CommitHash = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 3) +} + +func (x *GetVersionResponse) HasVersion() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *GetVersionResponse) HasDate() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *GetVersionResponse) HasCommitHash() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 2) +} + +func (x *GetVersionResponse) ClearVersion() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Version = nil +} + +func (x *GetVersionResponse) ClearDate() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_Date = nil +} + +func (x *GetVersionResponse) ClearCommitHash() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 2) + x.xxx_hidden_CommitHash = nil +} + +type GetVersionResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Version *string + Date *string + CommitHash *string +} + +func (b0 GetVersionResponse_builder) Build() *GetVersionResponse { + m0 := &GetVersionResponse{} + b, x := &b0, m0 + _, _ = b, x + if b.Version != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 3) + x.xxx_hidden_Version = b.Version + } + if b.Date != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 3) + x.xxx_hidden_Date = b.Date + } + if b.CommitHash != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 3) + x.xxx_hidden_CommitHash = b.CommitHash + } + return m0 +} + +var File_health_v1_api_proto protoreflect.FileDescriptor + +const file_health_v1_api_proto_rawDesc = "" + + "\n" + + "\x13health/v1/api.proto\x12\thealth.v1\"\x13\n" + + "\x11GetVersionRequest\"c\n" + + "\x12GetVersionResponse\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\x12\n" + + "\x04date\x18\x02 \x01(\tR\x04date\x12\x1f\n" + + "\vcommit_hash\x18\x03 \x01(\tR\n" + + "commitHash2[\n" + + "\x0eVersionService\x12I\n" + + "\n" + + "GetVersion\x12\x1c.health.v1.GetVersionRequest\x1a\x1d.health.v1.GetVersionResponseB\x99\x01\n" + + "\rcom.health.v1B\bApiProtoP\x01Z9github.com/docker/secrets-engine/x/api/health/v1;healthv1\xa2\x02\x03HXX\xaa\x02\tHealth.V1\xca\x02\tHealth\\V1\xe2\x02\x15Health\\V1\\GPBMetadata\xea\x02\n" + + "Health::V1b\beditionsp\xe8\a" + +var file_health_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_health_v1_api_proto_goTypes = []any{ + (*GetVersionRequest)(nil), // 0: health.v1.GetVersionRequest + (*GetVersionResponse)(nil), // 1: health.v1.GetVersionResponse +} +var file_health_v1_api_proto_depIdxs = []int32{ + 0, // 0: health.v1.VersionService.GetVersion:input_type -> health.v1.GetVersionRequest + 1, // 1: health.v1.VersionService.GetVersion:output_type -> health.v1.GetVersionResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_health_v1_api_proto_init() } +func file_health_v1_api_proto_init() { + if File_health_v1_api_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_health_v1_api_proto_rawDesc), len(file_health_v1_api_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_health_v1_api_proto_goTypes, + DependencyIndexes: file_health_v1_api_proto_depIdxs, + MessageInfos: file_health_v1_api_proto_msgTypes, + }.Build() + File_health_v1_api_proto = out.File + file_health_v1_api_proto_goTypes = nil + file_health_v1_api_proto_depIdxs = nil +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/health/v1/api.proto b/vendor/github.com/docker/secrets-engine/x/api/health/v1/api.proto new file mode 100644 index 000000000000..a3a14cf9e2c5 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/health/v1/api.proto @@ -0,0 +1,32 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +edition = "2023"; + +package health.v1; + +option go_package = "github.com/docker/secrets-engine/x/api/health/v1;healthv1"; + +service VersionService { + // GetVersion returns the version of the daemon. + rpc GetVersion(GetVersionRequest) returns (GetVersionResponse); +} + +message GetVersionRequest {} + +message GetVersionResponse { + string version = 1; + string date = 2; + string commit_hash = 3; +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/health/v1/healthv1connect/api.connect.go b/vendor/github.com/docker/secrets-engine/x/api/health/v1/healthv1connect/api.connect.go new file mode 100644 index 000000000000..752ed828ef1a --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/health/v1/healthv1connect/api.connect.go @@ -0,0 +1,111 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: health/v1/api.proto + +package healthv1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/docker/secrets-engine/x/api/health/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // VersionServiceName is the fully-qualified name of the VersionService service. + VersionServiceName = "health.v1.VersionService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // VersionServiceGetVersionProcedure is the fully-qualified name of the VersionService's GetVersion + // RPC. + VersionServiceGetVersionProcedure = "/health.v1.VersionService/GetVersion" +) + +// VersionServiceClient is a client for the health.v1.VersionService service. +type VersionServiceClient interface { + // GetVersion returns the version of the daemon. + GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error) +} + +// NewVersionServiceClient constructs a client for the health.v1.VersionService service. By default, +// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and +// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() +// or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewVersionServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) VersionServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + versionServiceMethods := v1.File_health_v1_api_proto.Services().ByName("VersionService").Methods() + return &versionServiceClient{ + getVersion: connect.NewClient[v1.GetVersionRequest, v1.GetVersionResponse]( + httpClient, + baseURL+VersionServiceGetVersionProcedure, + connect.WithSchema(versionServiceMethods.ByName("GetVersion")), + connect.WithClientOptions(opts...), + ), + } +} + +// versionServiceClient implements VersionServiceClient. +type versionServiceClient struct { + getVersion *connect.Client[v1.GetVersionRequest, v1.GetVersionResponse] +} + +// GetVersion calls health.v1.VersionService.GetVersion. +func (c *versionServiceClient) GetVersion(ctx context.Context, req *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error) { + return c.getVersion.CallUnary(ctx, req) +} + +// VersionServiceHandler is an implementation of the health.v1.VersionService service. +type VersionServiceHandler interface { + // GetVersion returns the version of the daemon. + GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error) +} + +// NewVersionServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewVersionServiceHandler(svc VersionServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + versionServiceMethods := v1.File_health_v1_api_proto.Services().ByName("VersionService").Methods() + versionServiceGetVersionHandler := connect.NewUnaryHandler( + VersionServiceGetVersionProcedure, + svc.GetVersion, + connect.WithSchema(versionServiceMethods.ByName("GetVersion")), + connect.WithHandlerOptions(opts...), + ) + return "/health.v1.VersionService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case VersionServiceGetVersionProcedure: + versionServiceGetVersionHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedVersionServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedVersionServiceHandler struct{} + +func (UnimplementedVersionServiceHandler) GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("health.v1.VersionService.GetVersion is not implemented")) +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/name.go b/vendor/github.com/docker/secrets-engine/x/api/name.go new file mode 100644 index 000000000000..713b7e1f5769 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/name.go @@ -0,0 +1,62 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "errors" + "fmt" +) + +var ErrEmptyName = errors.New("empty name") + +type Name interface { + String() string +} + +type name struct { + value string +} + +func (n *name) String() string { + return n.value +} + +// NewName creates a new [Name] from a string +// If a validation error occurs, it returns nil and the error. +func NewName(s string) (Name, error) { + if err := validName(s); err != nil { + return nil, fmt.Errorf("parsing name: %w", err) + } + return &name{ + value: s, + }, nil +} + +// MustNewName creates a new Name as [NewName] does, but panics when a validation +// error occurs. +func MustNewName(s string) Name { + v, err := NewName(s) + if err != nil { + panic(err) + } + return v +} + +func validName(s string) error { + if s == "" { + return ErrEmptyName + } + return nil +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/plugins/v1/api.pb.go b/vendor/github.com/docker/secrets-engine/x/api/plugins/v1/api.pb.go new file mode 100644 index 000000000000..a1e5c707f757 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/plugins/v1/api.pb.go @@ -0,0 +1,1551 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc (unknown) +// source: plugins/v1/api.proto + +package pluginsv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + reflect "reflect" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type RunStatus int32 + +const ( + RunStatus_RUN_STATUS_UNSPECIFIED RunStatus = 0 + RunStatus_RUN_STATUS_RUNNING RunStatus = 1 + RunStatus_RUN_STATUS_STOPPED RunStatus = 2 + RunStatus_RUN_STATUS_CRASHED RunStatus = 3 + RunStatus_RUN_STATUS_RETRIES_EXCEEDED RunStatus = 4 +) + +// Enum value maps for RunStatus. +var ( + RunStatus_name = map[int32]string{ + 0: "RUN_STATUS_UNSPECIFIED", + 1: "RUN_STATUS_RUNNING", + 2: "RUN_STATUS_STOPPED", + 3: "RUN_STATUS_CRASHED", + 4: "RUN_STATUS_RETRIES_EXCEEDED", + } + RunStatus_value = map[string]int32{ + "RUN_STATUS_UNSPECIFIED": 0, + "RUN_STATUS_RUNNING": 1, + "RUN_STATUS_STOPPED": 2, + "RUN_STATUS_CRASHED": 3, + "RUN_STATUS_RETRIES_EXCEEDED": 4, + } +) + +func (x RunStatus) Enum() *RunStatus { + p := new(RunStatus) + *p = x + return p +} + +func (x RunStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RunStatus) Descriptor() protoreflect.EnumDescriptor { + return file_plugins_v1_api_proto_enumTypes[0].Descriptor() +} + +func (RunStatus) Type() protoreflect.EnumType { + return &file_plugins_v1_api_proto_enumTypes[0] +} + +func (x RunStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +type RegisterPluginRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Name *string `protobuf:"bytes,1,opt,name=name"` + xxx_hidden_Version *string `protobuf:"bytes,2,opt,name=version"` + xxx_hidden_Metadata isRegisterPluginRequest_Metadata `protobuf_oneof:"metadata"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterPluginRequest) Reset() { + *x = RegisterPluginRequest{} + mi := &file_plugins_v1_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterPluginRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterPluginRequest) ProtoMessage() {} + +func (x *RegisterPluginRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *RegisterPluginRequest) GetName() string { + if x != nil { + if x.xxx_hidden_Name != nil { + return *x.xxx_hidden_Name + } + return "" + } + return "" +} + +func (x *RegisterPluginRequest) GetVersion() string { + if x != nil { + if x.xxx_hidden_Version != nil { + return *x.xxx_hidden_Version + } + return "" + } + return "" +} + +func (x *RegisterPluginRequest) GetSecretsProvider() *SecretsProvider { + if x != nil { + if x, ok := x.xxx_hidden_Metadata.(*registerPluginRequest_SecretsProvider); ok { + return x.SecretsProvider + } + } + return nil +} + +func (x *RegisterPluginRequest) GetAccessControlModule() *AccessControlModule { + if x != nil { + if x, ok := x.xxx_hidden_Metadata.(*registerPluginRequest_AccessControlModule); ok { + return x.AccessControlModule + } + } + return nil +} + +func (x *RegisterPluginRequest) SetName(v string) { + x.xxx_hidden_Name = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 3) +} + +func (x *RegisterPluginRequest) SetVersion(v string) { + x.xxx_hidden_Version = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 3) +} + +func (x *RegisterPluginRequest) SetSecretsProvider(v *SecretsProvider) { + if v == nil { + x.xxx_hidden_Metadata = nil + return + } + x.xxx_hidden_Metadata = ®isterPluginRequest_SecretsProvider{v} +} + +func (x *RegisterPluginRequest) SetAccessControlModule(v *AccessControlModule) { + if v == nil { + x.xxx_hidden_Metadata = nil + return + } + x.xxx_hidden_Metadata = ®isterPluginRequest_AccessControlModule{v} +} + +func (x *RegisterPluginRequest) HasName() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *RegisterPluginRequest) HasVersion() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *RegisterPluginRequest) HasMetadata() bool { + if x == nil { + return false + } + return x.xxx_hidden_Metadata != nil +} + +func (x *RegisterPluginRequest) HasSecretsProvider() bool { + if x == nil { + return false + } + _, ok := x.xxx_hidden_Metadata.(*registerPluginRequest_SecretsProvider) + return ok +} + +func (x *RegisterPluginRequest) HasAccessControlModule() bool { + if x == nil { + return false + } + _, ok := x.xxx_hidden_Metadata.(*registerPluginRequest_AccessControlModule) + return ok +} + +func (x *RegisterPluginRequest) ClearName() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Name = nil +} + +func (x *RegisterPluginRequest) ClearVersion() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_Version = nil +} + +func (x *RegisterPluginRequest) ClearMetadata() { + x.xxx_hidden_Metadata = nil +} + +func (x *RegisterPluginRequest) ClearSecretsProvider() { + if _, ok := x.xxx_hidden_Metadata.(*registerPluginRequest_SecretsProvider); ok { + x.xxx_hidden_Metadata = nil + } +} + +func (x *RegisterPluginRequest) ClearAccessControlModule() { + if _, ok := x.xxx_hidden_Metadata.(*registerPluginRequest_AccessControlModule); ok { + x.xxx_hidden_Metadata = nil + } +} + +const RegisterPluginRequest_Metadata_not_set_case case_RegisterPluginRequest_Metadata = 0 +const RegisterPluginRequest_SecretsProvider_case case_RegisterPluginRequest_Metadata = 3 +const RegisterPluginRequest_AccessControlModule_case case_RegisterPluginRequest_Metadata = 4 + +func (x *RegisterPluginRequest) WhichMetadata() case_RegisterPluginRequest_Metadata { + if x == nil { + return RegisterPluginRequest_Metadata_not_set_case + } + switch x.xxx_hidden_Metadata.(type) { + case *registerPluginRequest_SecretsProvider: + return RegisterPluginRequest_SecretsProvider_case + case *registerPluginRequest_AccessControlModule: + return RegisterPluginRequest_AccessControlModule_case + default: + return RegisterPluginRequest_Metadata_not_set_case + } +} + +type RegisterPluginRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // Name of the plugin to register. + Name *string + // Version of the plugin. + Version *string + // Type-specific fields + + // Fields of oneof xxx_hidden_Metadata: + SecretsProvider *SecretsProvider + AccessControlModule *AccessControlModule + // -- end of xxx_hidden_Metadata +} + +func (b0 RegisterPluginRequest_builder) Build() *RegisterPluginRequest { + m0 := &RegisterPluginRequest{} + b, x := &b0, m0 + _, _ = b, x + if b.Name != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 3) + x.xxx_hidden_Name = b.Name + } + if b.Version != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 3) + x.xxx_hidden_Version = b.Version + } + if b.SecretsProvider != nil { + x.xxx_hidden_Metadata = ®isterPluginRequest_SecretsProvider{b.SecretsProvider} + } + if b.AccessControlModule != nil { + x.xxx_hidden_Metadata = ®isterPluginRequest_AccessControlModule{b.AccessControlModule} + } + return m0 +} + +type case_RegisterPluginRequest_Metadata protoreflect.FieldNumber + +func (x case_RegisterPluginRequest_Metadata) String() string { + md := file_plugins_v1_api_proto_msgTypes[0].Descriptor() + if x == 0 { + return "not set" + } + return protoimpl.X.MessageFieldStringOf(md, protoreflect.FieldNumber(x)) +} + +type isRegisterPluginRequest_Metadata interface { + isRegisterPluginRequest_Metadata() +} + +type registerPluginRequest_SecretsProvider struct { + SecretsProvider *SecretsProvider `protobuf:"bytes,3,opt,name=secrets_provider,json=secretsProvider,oneof"` +} + +type registerPluginRequest_AccessControlModule struct { + AccessControlModule *AccessControlModule `protobuf:"bytes,4,opt,name=access_control_module,json=accessControlModule,oneof"` +} + +func (*registerPluginRequest_SecretsProvider) isRegisterPluginRequest_Metadata() {} + +func (*registerPluginRequest_AccessControlModule) isRegisterPluginRequest_Metadata() {} + +type RegisterPluginResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_EngineName *string `protobuf:"bytes,1,opt,name=engine_name,json=engineName"` + xxx_hidden_EngineVersion *string `protobuf:"bytes,2,opt,name=engine_version,json=engineVersion"` + xxx_hidden_RequestTimeout *durationpb.Duration `protobuf:"bytes,3,opt,name=request_timeout,json=requestTimeout"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterPluginResponse) Reset() { + *x = RegisterPluginResponse{} + mi := &file_plugins_v1_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterPluginResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterPluginResponse) ProtoMessage() {} + +func (x *RegisterPluginResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *RegisterPluginResponse) GetEngineName() string { + if x != nil { + if x.xxx_hidden_EngineName != nil { + return *x.xxx_hidden_EngineName + } + return "" + } + return "" +} + +func (x *RegisterPluginResponse) GetEngineVersion() string { + if x != nil { + if x.xxx_hidden_EngineVersion != nil { + return *x.xxx_hidden_EngineVersion + } + return "" + } + return "" +} + +func (x *RegisterPluginResponse) GetRequestTimeout() *durationpb.Duration { + if x != nil { + return x.xxx_hidden_RequestTimeout + } + return nil +} + +func (x *RegisterPluginResponse) SetEngineName(v string) { + x.xxx_hidden_EngineName = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 3) +} + +func (x *RegisterPluginResponse) SetEngineVersion(v string) { + x.xxx_hidden_EngineVersion = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 3) +} + +func (x *RegisterPluginResponse) SetRequestTimeout(v *durationpb.Duration) { + x.xxx_hidden_RequestTimeout = v +} + +func (x *RegisterPluginResponse) HasEngineName() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *RegisterPluginResponse) HasEngineVersion() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *RegisterPluginResponse) HasRequestTimeout() bool { + if x == nil { + return false + } + return x.xxx_hidden_RequestTimeout != nil +} + +func (x *RegisterPluginResponse) ClearEngineName() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_EngineName = nil +} + +func (x *RegisterPluginResponse) ClearEngineVersion() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_EngineVersion = nil +} + +func (x *RegisterPluginResponse) ClearRequestTimeout() { + x.xxx_hidden_RequestTimeout = nil +} + +type RegisterPluginResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // Name of the secrets engine is running in. + EngineName *string + // Version of the runtime engine is running in. + EngineVersion *string + // Configured request processing timeout in milliseconds. + RequestTimeout *durationpb.Duration +} + +func (b0 RegisterPluginResponse_builder) Build() *RegisterPluginResponse { + m0 := &RegisterPluginResponse{} + b, x := &b0, m0 + _, _ = b, x + if b.EngineName != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 3) + x.xxx_hidden_EngineName = b.EngineName + } + if b.EngineVersion != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 3) + x.xxx_hidden_EngineVersion = b.EngineVersion + } + x.xxx_hidden_RequestTimeout = b.RequestTimeout + return m0 +} + +type ShutdownRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ShutdownRequest) Reset() { + *x = ShutdownRequest{} + mi := &file_plugins_v1_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ShutdownRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ShutdownRequest) ProtoMessage() {} + +func (x *ShutdownRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +type ShutdownRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + +} + +func (b0 ShutdownRequest_builder) Build() *ShutdownRequest { + m0 := &ShutdownRequest{} + b, x := &b0, m0 + _, _ = b, x + return m0 +} + +type ShutdownResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ShutdownResponse) Reset() { + *x = ShutdownResponse{} + mi := &file_plugins_v1_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ShutdownResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ShutdownResponse) ProtoMessage() {} + +func (x *ShutdownResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +type ShutdownResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + +} + +func (b0 ShutdownResponse_builder) Build() *ShutdownResponse { + m0 := &ShutdownResponse{} + b, x := &b0, m0 + _, _ = b, x + return m0 +} + +type Plugin struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Name *string `protobuf:"bytes,1,opt,name=name"` + xxx_hidden_Version *string `protobuf:"bytes,2,opt,name=version"` + xxx_hidden_Disabled bool `protobuf:"varint,3,opt,name=disabled"` + xxx_hidden_External bool `protobuf:"varint,4,opt,name=external"` + xxx_hidden_Configurable bool `protobuf:"varint,5,opt,name=configurable"` + xxx_hidden_RunStatus RunStatus `protobuf:"varint,6,opt,name=run_status,json=runStatus,enum=plugins.v1.RunStatus"` + xxx_hidden_StatusMessage *string `protobuf:"bytes,7,opt,name=status_message,json=statusMessage"` + xxx_hidden_Metadata isPlugin_Metadata `protobuf_oneof:"metadata"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Plugin) Reset() { + *x = Plugin{} + mi := &file_plugins_v1_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Plugin) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Plugin) ProtoMessage() {} + +func (x *Plugin) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *Plugin) GetName() string { + if x != nil { + if x.xxx_hidden_Name != nil { + return *x.xxx_hidden_Name + } + return "" + } + return "" +} + +func (x *Plugin) GetVersion() string { + if x != nil { + if x.xxx_hidden_Version != nil { + return *x.xxx_hidden_Version + } + return "" + } + return "" +} + +func (x *Plugin) GetDisabled() bool { + if x != nil { + return x.xxx_hidden_Disabled + } + return false +} + +func (x *Plugin) GetExternal() bool { + if x != nil { + return x.xxx_hidden_External + } + return false +} + +func (x *Plugin) GetConfigurable() bool { + if x != nil { + return x.xxx_hidden_Configurable + } + return false +} + +func (x *Plugin) GetRunStatus() RunStatus { + if x != nil { + if protoimpl.X.Present(&(x.XXX_presence[0]), 5) { + return x.xxx_hidden_RunStatus + } + } + return RunStatus_RUN_STATUS_UNSPECIFIED +} + +func (x *Plugin) GetStatusMessage() string { + if x != nil { + if x.xxx_hidden_StatusMessage != nil { + return *x.xxx_hidden_StatusMessage + } + return "" + } + return "" +} + +func (x *Plugin) GetSecretsProvider() *SecretsProvider { + if x != nil { + if x, ok := x.xxx_hidden_Metadata.(*plugin_SecretsProvider); ok { + return x.SecretsProvider + } + } + return nil +} + +func (x *Plugin) GetAccessControlModule() *AccessControlModule { + if x != nil { + if x, ok := x.xxx_hidden_Metadata.(*plugin_AccessControlModule); ok { + return x.AccessControlModule + } + } + return nil +} + +func (x *Plugin) SetName(v string) { + x.xxx_hidden_Name = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 8) +} + +func (x *Plugin) SetVersion(v string) { + x.xxx_hidden_Version = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 8) +} + +func (x *Plugin) SetDisabled(v bool) { + x.xxx_hidden_Disabled = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 8) +} + +func (x *Plugin) SetExternal(v bool) { + x.xxx_hidden_External = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 3, 8) +} + +func (x *Plugin) SetConfigurable(v bool) { + x.xxx_hidden_Configurable = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 4, 8) +} + +func (x *Plugin) SetRunStatus(v RunStatus) { + x.xxx_hidden_RunStatus = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 5, 8) +} + +func (x *Plugin) SetStatusMessage(v string) { + x.xxx_hidden_StatusMessage = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 6, 8) +} + +func (x *Plugin) SetSecretsProvider(v *SecretsProvider) { + if v == nil { + x.xxx_hidden_Metadata = nil + return + } + x.xxx_hidden_Metadata = &plugin_SecretsProvider{v} +} + +func (x *Plugin) SetAccessControlModule(v *AccessControlModule) { + if v == nil { + x.xxx_hidden_Metadata = nil + return + } + x.xxx_hidden_Metadata = &plugin_AccessControlModule{v} +} + +func (x *Plugin) HasName() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *Plugin) HasVersion() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *Plugin) HasDisabled() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 2) +} + +func (x *Plugin) HasExternal() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 3) +} + +func (x *Plugin) HasConfigurable() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 4) +} + +func (x *Plugin) HasRunStatus() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 5) +} + +func (x *Plugin) HasStatusMessage() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 6) +} + +func (x *Plugin) HasMetadata() bool { + if x == nil { + return false + } + return x.xxx_hidden_Metadata != nil +} + +func (x *Plugin) HasSecretsProvider() bool { + if x == nil { + return false + } + _, ok := x.xxx_hidden_Metadata.(*plugin_SecretsProvider) + return ok +} + +func (x *Plugin) HasAccessControlModule() bool { + if x == nil { + return false + } + _, ok := x.xxx_hidden_Metadata.(*plugin_AccessControlModule) + return ok +} + +func (x *Plugin) ClearName() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Name = nil +} + +func (x *Plugin) ClearVersion() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_Version = nil +} + +func (x *Plugin) ClearDisabled() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 2) + x.xxx_hidden_Disabled = false +} + +func (x *Plugin) ClearExternal() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 3) + x.xxx_hidden_External = false +} + +func (x *Plugin) ClearConfigurable() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 4) + x.xxx_hidden_Configurable = false +} + +func (x *Plugin) ClearRunStatus() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 5) + x.xxx_hidden_RunStatus = RunStatus_RUN_STATUS_UNSPECIFIED +} + +func (x *Plugin) ClearStatusMessage() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 6) + x.xxx_hidden_StatusMessage = nil +} + +func (x *Plugin) ClearMetadata() { + x.xxx_hidden_Metadata = nil +} + +func (x *Plugin) ClearSecretsProvider() { + if _, ok := x.xxx_hidden_Metadata.(*plugin_SecretsProvider); ok { + x.xxx_hidden_Metadata = nil + } +} + +func (x *Plugin) ClearAccessControlModule() { + if _, ok := x.xxx_hidden_Metadata.(*plugin_AccessControlModule); ok { + x.xxx_hidden_Metadata = nil + } +} + +const Plugin_Metadata_not_set_case case_Plugin_Metadata = 0 +const Plugin_SecretsProvider_case case_Plugin_Metadata = 8 +const Plugin_AccessControlModule_case case_Plugin_Metadata = 9 + +func (x *Plugin) WhichMetadata() case_Plugin_Metadata { + if x == nil { + return Plugin_Metadata_not_set_case + } + switch x.xxx_hidden_Metadata.(type) { + case *plugin_SecretsProvider: + return Plugin_SecretsProvider_case + case *plugin_AccessControlModule: + return Plugin_AccessControlModule_case + default: + return Plugin_Metadata_not_set_case + } +} + +type Plugin_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // Name + Name *string + // Version + Version *string + // Enabled vs disabled + Disabled *bool + // External vs builtin + External *bool + // Configurable + Configurable *bool + // Run status + RunStatus *RunStatus + // Human-readable detail for the current run_status. Populated for + // terminal/error statuses (CRASHED, RETRIES_EXCEEDED); typically empty + // otherwise. Not machine-parseable — intended for display only. + StatusMessage *string + // Type-specific fields + + // Fields of oneof xxx_hidden_Metadata: + SecretsProvider *SecretsProvider + AccessControlModule *AccessControlModule + // -- end of xxx_hidden_Metadata +} + +func (b0 Plugin_builder) Build() *Plugin { + m0 := &Plugin{} + b, x := &b0, m0 + _, _ = b, x + if b.Name != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 8) + x.xxx_hidden_Name = b.Name + } + if b.Version != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 8) + x.xxx_hidden_Version = b.Version + } + if b.Disabled != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 8) + x.xxx_hidden_Disabled = *b.Disabled + } + if b.External != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 3, 8) + x.xxx_hidden_External = *b.External + } + if b.Configurable != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 4, 8) + x.xxx_hidden_Configurable = *b.Configurable + } + if b.RunStatus != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 5, 8) + x.xxx_hidden_RunStatus = *b.RunStatus + } + if b.StatusMessage != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 6, 8) + x.xxx_hidden_StatusMessage = b.StatusMessage + } + if b.SecretsProvider != nil { + x.xxx_hidden_Metadata = &plugin_SecretsProvider{b.SecretsProvider} + } + if b.AccessControlModule != nil { + x.xxx_hidden_Metadata = &plugin_AccessControlModule{b.AccessControlModule} + } + return m0 +} + +type case_Plugin_Metadata protoreflect.FieldNumber + +func (x case_Plugin_Metadata) String() string { + md := file_plugins_v1_api_proto_msgTypes[4].Descriptor() + if x == 0 { + return "not set" + } + return protoimpl.X.MessageFieldStringOf(md, protoreflect.FieldNumber(x)) +} + +type isPlugin_Metadata interface { + isPlugin_Metadata() +} + +type plugin_SecretsProvider struct { + SecretsProvider *SecretsProvider `protobuf:"bytes,8,opt,name=secrets_provider,json=secretsProvider,oneof"` +} + +type plugin_AccessControlModule struct { + AccessControlModule *AccessControlModule `protobuf:"bytes,9,opt,name=access_control_module,json=accessControlModule,oneof"` +} + +func (*plugin_SecretsProvider) isPlugin_Metadata() {} + +func (*plugin_AccessControlModule) isPlugin_Metadata() {} + +type SecretsProvider struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Pattern *string `protobuf:"bytes,1,opt,name=pattern"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecretsProvider) Reset() { + *x = SecretsProvider{} + mi := &file_plugins_v1_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecretsProvider) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretsProvider) ProtoMessage() {} + +func (x *SecretsProvider) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *SecretsProvider) GetPattern() string { + if x != nil { + if x.xxx_hidden_Pattern != nil { + return *x.xxx_hidden_Pattern + } + return "" + } + return "" +} + +func (x *SecretsProvider) SetPattern(v string) { + x.xxx_hidden_Pattern = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 1) +} + +func (x *SecretsProvider) HasPattern() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *SecretsProvider) ClearPattern() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Pattern = nil +} + +type SecretsProvider_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // Pattern + Pattern *string +} + +func (b0 SecretsProvider_builder) Build() *SecretsProvider { + m0 := &SecretsProvider{} + b, x := &b0, m0 + _, _ = b, x + if b.Pattern != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 1) + x.xxx_hidden_Pattern = b.Pattern + } + return m0 +} + +type AccessControlModule struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AccessControlModule) Reset() { + *x = AccessControlModule{} + mi := &file_plugins_v1_api_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AccessControlModule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccessControlModule) ProtoMessage() {} + +func (x *AccessControlModule) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +type AccessControlModule_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + +} + +func (b0 AccessControlModule_builder) Build() *AccessControlModule { + m0 := &AccessControlModule{} + b, x := &b0, m0 + _, _ = b, x + return m0 +} + +type ListPluginsRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPluginsRequest) Reset() { + *x = ListPluginsRequest{} + mi := &file_plugins_v1_api_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPluginsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPluginsRequest) ProtoMessage() {} + +func (x *ListPluginsRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +type ListPluginsRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + +} + +func (b0 ListPluginsRequest_builder) Build() *ListPluginsRequest { + m0 := &ListPluginsRequest{} + b, x := &b0, m0 + _, _ = b, x + return m0 +} + +type ListPluginsResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Plugins *[]*Plugin `protobuf:"bytes,1,rep,name=plugins"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPluginsResponse) Reset() { + *x = ListPluginsResponse{} + mi := &file_plugins_v1_api_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPluginsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPluginsResponse) ProtoMessage() {} + +func (x *ListPluginsResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *ListPluginsResponse) GetPlugins() []*Plugin { + if x != nil { + if x.xxx_hidden_Plugins != nil { + return *x.xxx_hidden_Plugins + } + } + return nil +} + +func (x *ListPluginsResponse) SetPlugins(v []*Plugin) { + x.xxx_hidden_Plugins = &v +} + +type ListPluginsResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Plugins []*Plugin +} + +func (b0 ListPluginsResponse_builder) Build() *ListPluginsResponse { + m0 := &ListPluginsResponse{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_Plugins = &b.Plugins + return m0 +} + +type EnablePluginRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Name *string `protobuf:"bytes,1,opt,name=name"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnablePluginRequest) Reset() { + *x = EnablePluginRequest{} + mi := &file_plugins_v1_api_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnablePluginRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnablePluginRequest) ProtoMessage() {} + +func (x *EnablePluginRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *EnablePluginRequest) GetName() string { + if x != nil { + if x.xxx_hidden_Name != nil { + return *x.xxx_hidden_Name + } + return "" + } + return "" +} + +func (x *EnablePluginRequest) SetName(v string) { + x.xxx_hidden_Name = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 1) +} + +func (x *EnablePluginRequest) HasName() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *EnablePluginRequest) ClearName() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Name = nil +} + +type EnablePluginRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Name *string +} + +func (b0 EnablePluginRequest_builder) Build() *EnablePluginRequest { + m0 := &EnablePluginRequest{} + b, x := &b0, m0 + _, _ = b, x + if b.Name != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 1) + x.xxx_hidden_Name = b.Name + } + return m0 +} + +type EnablePluginResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnablePluginResponse) Reset() { + *x = EnablePluginResponse{} + mi := &file_plugins_v1_api_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnablePluginResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnablePluginResponse) ProtoMessage() {} + +func (x *EnablePluginResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +type EnablePluginResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + +} + +func (b0 EnablePluginResponse_builder) Build() *EnablePluginResponse { + m0 := &EnablePluginResponse{} + b, x := &b0, m0 + _, _ = b, x + return m0 +} + +type DisablePluginRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Name *string `protobuf:"bytes,1,opt,name=name"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisablePluginRequest) Reset() { + *x = DisablePluginRequest{} + mi := &file_plugins_v1_api_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisablePluginRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisablePluginRequest) ProtoMessage() {} + +func (x *DisablePluginRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *DisablePluginRequest) GetName() string { + if x != nil { + if x.xxx_hidden_Name != nil { + return *x.xxx_hidden_Name + } + return "" + } + return "" +} + +func (x *DisablePluginRequest) SetName(v string) { + x.xxx_hidden_Name = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 1) +} + +func (x *DisablePluginRequest) HasName() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *DisablePluginRequest) ClearName() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Name = nil +} + +type DisablePluginRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Name *string +} + +func (b0 DisablePluginRequest_builder) Build() *DisablePluginRequest { + m0 := &DisablePluginRequest{} + b, x := &b0, m0 + _, _ = b, x + if b.Name != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 1) + x.xxx_hidden_Name = b.Name + } + return m0 +} + +type DisablePluginResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisablePluginResponse) Reset() { + *x = DisablePluginResponse{} + mi := &file_plugins_v1_api_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisablePluginResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisablePluginResponse) ProtoMessage() {} + +func (x *DisablePluginResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugins_v1_api_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +type DisablePluginResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + +} + +func (b0 DisablePluginResponse_builder) Build() *DisablePluginResponse { + m0 := &DisablePluginResponse{} + b, x := &b0, m0 + _, _ = b, x + return m0 +} + +var File_plugins_v1_api_proto protoreflect.FileDescriptor + +const file_plugins_v1_api_proto_rawDesc = "" + + "\n" + + "\x14plugins/v1/api.proto\x12\n" + + "plugins.v1\x1a\x1egoogle/protobuf/duration.proto\"\xf2\x01\n" + + "\x15RegisterPluginRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\x12H\n" + + "\x10secrets_provider\x18\x03 \x01(\v2\x1b.plugins.v1.SecretsProviderH\x00R\x0fsecretsProvider\x12U\n" + + "\x15access_control_module\x18\x04 \x01(\v2\x1f.plugins.v1.AccessControlModuleH\x00R\x13accessControlModuleB\n" + + "\n" + + "\bmetadata\"\xa4\x01\n" + + "\x16RegisterPluginResponse\x12\x1f\n" + + "\vengine_name\x18\x01 \x01(\tR\n" + + "engineName\x12%\n" + + "\x0eengine_version\x18\x02 \x01(\tR\rengineVersion\x12B\n" + + "\x0frequest_timeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\x0erequestTimeout\"\x11\n" + + "\x0fShutdownRequest\"\x12\n" + + "\x10ShutdownResponse\"\x9c\x03\n" + + "\x06Plugin\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\x12\x1a\n" + + "\bdisabled\x18\x03 \x01(\bR\bdisabled\x12\x1a\n" + + "\bexternal\x18\x04 \x01(\bR\bexternal\x12\"\n" + + "\fconfigurable\x18\x05 \x01(\bR\fconfigurable\x124\n" + + "\n" + + "run_status\x18\x06 \x01(\x0e2\x15.plugins.v1.RunStatusR\trunStatus\x12%\n" + + "\x0estatus_message\x18\a \x01(\tR\rstatusMessage\x12H\n" + + "\x10secrets_provider\x18\b \x01(\v2\x1b.plugins.v1.SecretsProviderH\x00R\x0fsecretsProvider\x12U\n" + + "\x15access_control_module\x18\t \x01(\v2\x1f.plugins.v1.AccessControlModuleH\x00R\x13accessControlModuleB\n" + + "\n" + + "\bmetadata\"+\n" + + "\x0fSecretsProvider\x12\x18\n" + + "\apattern\x18\x01 \x01(\tR\apattern\"\x15\n" + + "\x13AccessControlModule\"\x14\n" + + "\x12ListPluginsRequest\"C\n" + + "\x13ListPluginsResponse\x12,\n" + + "\aplugins\x18\x01 \x03(\v2\x12.plugins.v1.PluginR\aplugins\")\n" + + "\x13EnablePluginRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x16\n" + + "\x14EnablePluginResponse\"*\n" + + "\x14DisablePluginRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x17\n" + + "\x15DisablePluginResponse*\x90\x01\n" + + "\tRunStatus\x12\x1a\n" + + "\x16RUN_STATUS_UNSPECIFIED\x10\x00\x12\x16\n" + + "\x12RUN_STATUS_RUNNING\x10\x01\x12\x16\n" + + "\x12RUN_STATUS_STOPPED\x10\x02\x12\x16\n" + + "\x12RUN_STATUS_CRASHED\x10\x03\x12\x1f\n" + + "\x1bRUN_STATUS_RETRIES_EXCEEDED\x10\x042j\n" + + "\x0fRegisterService\x12W\n" + + "\x0eRegisterPlugin\x12!.plugins.v1.RegisterPluginRequest\x1a\".plugins.v1.RegisterPluginResponse2V\n" + + "\rPluginService\x12E\n" + + "\bShutdown\x12\x1b.plugins.v1.ShutdownRequest\x1a\x1c.plugins.v1.ShutdownResponse2\x92\x02\n" + + "\x17PluginManagementService\x12N\n" + + "\vListPlugins\x12\x1e.plugins.v1.ListPluginsRequest\x1a\x1f.plugins.v1.ListPluginsResponse\x12Q\n" + + "\fEnablePlugin\x12\x1f.plugins.v1.EnablePluginRequest\x1a .plugins.v1.EnablePluginResponse\x12T\n" + + "\rDisablePlugin\x12 .plugins.v1.DisablePluginRequest\x1a!.plugins.v1.DisablePluginResponseB\xa0\x01\n" + + "\x0ecom.plugins.v1B\bApiProtoP\x01Z;github.com/docker/secrets-engine/x/api/plugins/v1;pluginsv1\xa2\x02\x03PXX\xaa\x02\n" + + "Plugins.V1\xca\x02\n" + + "Plugins\\V1\xe2\x02\x16Plugins\\V1\\GPBMetadata\xea\x02\vPlugins::V1b\beditionsp\xe8\a" + +var file_plugins_v1_api_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_plugins_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_plugins_v1_api_proto_goTypes = []any{ + (RunStatus)(0), // 0: plugins.v1.RunStatus + (*RegisterPluginRequest)(nil), // 1: plugins.v1.RegisterPluginRequest + (*RegisterPluginResponse)(nil), // 2: plugins.v1.RegisterPluginResponse + (*ShutdownRequest)(nil), // 3: plugins.v1.ShutdownRequest + (*ShutdownResponse)(nil), // 4: plugins.v1.ShutdownResponse + (*Plugin)(nil), // 5: plugins.v1.Plugin + (*SecretsProvider)(nil), // 6: plugins.v1.SecretsProvider + (*AccessControlModule)(nil), // 7: plugins.v1.AccessControlModule + (*ListPluginsRequest)(nil), // 8: plugins.v1.ListPluginsRequest + (*ListPluginsResponse)(nil), // 9: plugins.v1.ListPluginsResponse + (*EnablePluginRequest)(nil), // 10: plugins.v1.EnablePluginRequest + (*EnablePluginResponse)(nil), // 11: plugins.v1.EnablePluginResponse + (*DisablePluginRequest)(nil), // 12: plugins.v1.DisablePluginRequest + (*DisablePluginResponse)(nil), // 13: plugins.v1.DisablePluginResponse + (*durationpb.Duration)(nil), // 14: google.protobuf.Duration +} +var file_plugins_v1_api_proto_depIdxs = []int32{ + 6, // 0: plugins.v1.RegisterPluginRequest.secrets_provider:type_name -> plugins.v1.SecretsProvider + 7, // 1: plugins.v1.RegisterPluginRequest.access_control_module:type_name -> plugins.v1.AccessControlModule + 14, // 2: plugins.v1.RegisterPluginResponse.request_timeout:type_name -> google.protobuf.Duration + 0, // 3: plugins.v1.Plugin.run_status:type_name -> plugins.v1.RunStatus + 6, // 4: plugins.v1.Plugin.secrets_provider:type_name -> plugins.v1.SecretsProvider + 7, // 5: plugins.v1.Plugin.access_control_module:type_name -> plugins.v1.AccessControlModule + 5, // 6: plugins.v1.ListPluginsResponse.plugins:type_name -> plugins.v1.Plugin + 1, // 7: plugins.v1.RegisterService.RegisterPlugin:input_type -> plugins.v1.RegisterPluginRequest + 3, // 8: plugins.v1.PluginService.Shutdown:input_type -> plugins.v1.ShutdownRequest + 8, // 9: plugins.v1.PluginManagementService.ListPlugins:input_type -> plugins.v1.ListPluginsRequest + 10, // 10: plugins.v1.PluginManagementService.EnablePlugin:input_type -> plugins.v1.EnablePluginRequest + 12, // 11: plugins.v1.PluginManagementService.DisablePlugin:input_type -> plugins.v1.DisablePluginRequest + 2, // 12: plugins.v1.RegisterService.RegisterPlugin:output_type -> plugins.v1.RegisterPluginResponse + 4, // 13: plugins.v1.PluginService.Shutdown:output_type -> plugins.v1.ShutdownResponse + 9, // 14: plugins.v1.PluginManagementService.ListPlugins:output_type -> plugins.v1.ListPluginsResponse + 11, // 15: plugins.v1.PluginManagementService.EnablePlugin:output_type -> plugins.v1.EnablePluginResponse + 13, // 16: plugins.v1.PluginManagementService.DisablePlugin:output_type -> plugins.v1.DisablePluginResponse + 12, // [12:17] is the sub-list for method output_type + 7, // [7:12] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_plugins_v1_api_proto_init() } +func file_plugins_v1_api_proto_init() { + if File_plugins_v1_api_proto != nil { + return + } + file_plugins_v1_api_proto_msgTypes[0].OneofWrappers = []any{ + (*registerPluginRequest_SecretsProvider)(nil), + (*registerPluginRequest_AccessControlModule)(nil), + } + file_plugins_v1_api_proto_msgTypes[4].OneofWrappers = []any{ + (*plugin_SecretsProvider)(nil), + (*plugin_AccessControlModule)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_plugins_v1_api_proto_rawDesc), len(file_plugins_v1_api_proto_rawDesc)), + NumEnums: 1, + NumMessages: 13, + NumExtensions: 0, + NumServices: 3, + }, + GoTypes: file_plugins_v1_api_proto_goTypes, + DependencyIndexes: file_plugins_v1_api_proto_depIdxs, + EnumInfos: file_plugins_v1_api_proto_enumTypes, + MessageInfos: file_plugins_v1_api_proto_msgTypes, + }.Build() + File_plugins_v1_api_proto = out.File + file_plugins_v1_api_proto_goTypes = nil + file_plugins_v1_api_proto_depIdxs = nil +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/plugins/v1/api.proto b/vendor/github.com/docker/secrets-engine/x/api/plugins/v1/api.proto new file mode 100644 index 000000000000..684f672f31c0 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/plugins/v1/api.proto @@ -0,0 +1,122 @@ +// Copyright 2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +edition = "2023"; + +package plugins.v1; + +import "google/protobuf/duration.proto"; + +option go_package = "github.com/docker/secrets-engine/x/api/plugins/v1;pluginsv1"; + +service RegisterService { + // RegisterPlugin registers the plugin with the engine. + rpc RegisterPlugin(RegisterPluginRequest) returns (RegisterPluginResponse); +} + +message RegisterPluginRequest { + // Name of the plugin to register. + string name = 1; + // Version of the plugin. + string version = 2; + // Type-specific fields + oneof metadata { + SecretsProvider secrets_provider = 3; + AccessControlModule access_control_module = 4; + } +} + +message RegisterPluginResponse { + // Name of the secrets engine is running in. + string engine_name = 1; + // Version of the runtime engine is running in. + string engine_version = 2; + // Configured request processing timeout in milliseconds. + google.protobuf.Duration request_timeout = 3; +} + +service PluginService { + // Shutdown a plugin (let it know the runtime is going down). + rpc Shutdown(ShutdownRequest) returns (ShutdownResponse); +} + +message ShutdownRequest {} + +message ShutdownResponse {} + +service PluginManagementService { + // ListPlugins returns all plugins registered with the engine. + rpc ListPlugins(ListPluginsRequest) returns (ListPluginsResponse); + // EnablePlugin enables a plugin by name. + rpc EnablePlugin(EnablePluginRequest) returns (EnablePluginResponse); + // DisablePlugin disables a plugin by name. + rpc DisablePlugin(DisablePluginRequest) returns (DisablePluginResponse); +} + +enum RunStatus { + RUN_STATUS_UNSPECIFIED = 0; + RUN_STATUS_RUNNING = 1; + RUN_STATUS_STOPPED = 2; + RUN_STATUS_CRASHED = 3; + RUN_STATUS_RETRIES_EXCEEDED = 4; +} + +message Plugin { + // Name + string name = 1; + // Version + string version = 2; + // Enabled vs disabled + bool disabled = 3; + // External vs builtin + bool external = 4; + // Configurable + bool configurable = 5; + // Run status + RunStatus run_status = 6; + // Human-readable detail for the current run_status. Populated for + // terminal/error statuses (CRASHED, RETRIES_EXCEEDED); typically empty + // otherwise. Not machine-parseable — intended for display only. + string status_message = 7; + // Type-specific fields + oneof metadata { + SecretsProvider secrets_provider = 8; + AccessControlModule access_control_module = 9; + } +} + +message SecretsProvider { + // Pattern + string pattern = 1; +} + +message AccessControlModule {} + +message ListPluginsRequest {} + +message ListPluginsResponse { + repeated Plugin plugins = 1; +} + +message EnablePluginRequest { + string name = 1; +} + +message EnablePluginResponse {} + +message DisablePluginRequest { + string name = 1; +} + +message DisablePluginResponse {} diff --git a/vendor/github.com/docker/secrets-engine/x/api/plugins/v1/pluginsv1connect/api.connect.go b/vendor/github.com/docker/secrets-engine/x/api/plugins/v1/pluginsv1connect/api.connect.go new file mode 100644 index 000000000000..5dc08ba93d0b --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/plugins/v1/pluginsv1connect/api.connect.go @@ -0,0 +1,327 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: plugins/v1/api.proto + +package pluginsv1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/docker/secrets-engine/x/api/plugins/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // RegisterServiceName is the fully-qualified name of the RegisterService service. + RegisterServiceName = "plugins.v1.RegisterService" + // PluginServiceName is the fully-qualified name of the PluginService service. + PluginServiceName = "plugins.v1.PluginService" + // PluginManagementServiceName is the fully-qualified name of the PluginManagementService service. + PluginManagementServiceName = "plugins.v1.PluginManagementService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // RegisterServiceRegisterPluginProcedure is the fully-qualified name of the RegisterService's + // RegisterPlugin RPC. + RegisterServiceRegisterPluginProcedure = "/plugins.v1.RegisterService/RegisterPlugin" + // PluginServiceShutdownProcedure is the fully-qualified name of the PluginService's Shutdown RPC. + PluginServiceShutdownProcedure = "/plugins.v1.PluginService/Shutdown" + // PluginManagementServiceListPluginsProcedure is the fully-qualified name of the + // PluginManagementService's ListPlugins RPC. + PluginManagementServiceListPluginsProcedure = "/plugins.v1.PluginManagementService/ListPlugins" + // PluginManagementServiceEnablePluginProcedure is the fully-qualified name of the + // PluginManagementService's EnablePlugin RPC. + PluginManagementServiceEnablePluginProcedure = "/plugins.v1.PluginManagementService/EnablePlugin" + // PluginManagementServiceDisablePluginProcedure is the fully-qualified name of the + // PluginManagementService's DisablePlugin RPC. + PluginManagementServiceDisablePluginProcedure = "/plugins.v1.PluginManagementService/DisablePlugin" +) + +// RegisterServiceClient is a client for the plugins.v1.RegisterService service. +type RegisterServiceClient interface { + // RegisterPlugin registers the plugin with the engine. + RegisterPlugin(context.Context, *connect.Request[v1.RegisterPluginRequest]) (*connect.Response[v1.RegisterPluginResponse], error) +} + +// NewRegisterServiceClient constructs a client for the plugins.v1.RegisterService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewRegisterServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RegisterServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + registerServiceMethods := v1.File_plugins_v1_api_proto.Services().ByName("RegisterService").Methods() + return ®isterServiceClient{ + registerPlugin: connect.NewClient[v1.RegisterPluginRequest, v1.RegisterPluginResponse]( + httpClient, + baseURL+RegisterServiceRegisterPluginProcedure, + connect.WithSchema(registerServiceMethods.ByName("RegisterPlugin")), + connect.WithClientOptions(opts...), + ), + } +} + +// registerServiceClient implements RegisterServiceClient. +type registerServiceClient struct { + registerPlugin *connect.Client[v1.RegisterPluginRequest, v1.RegisterPluginResponse] +} + +// RegisterPlugin calls plugins.v1.RegisterService.RegisterPlugin. +func (c *registerServiceClient) RegisterPlugin(ctx context.Context, req *connect.Request[v1.RegisterPluginRequest]) (*connect.Response[v1.RegisterPluginResponse], error) { + return c.registerPlugin.CallUnary(ctx, req) +} + +// RegisterServiceHandler is an implementation of the plugins.v1.RegisterService service. +type RegisterServiceHandler interface { + // RegisterPlugin registers the plugin with the engine. + RegisterPlugin(context.Context, *connect.Request[v1.RegisterPluginRequest]) (*connect.Response[v1.RegisterPluginResponse], error) +} + +// NewRegisterServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewRegisterServiceHandler(svc RegisterServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + registerServiceMethods := v1.File_plugins_v1_api_proto.Services().ByName("RegisterService").Methods() + registerServiceRegisterPluginHandler := connect.NewUnaryHandler( + RegisterServiceRegisterPluginProcedure, + svc.RegisterPlugin, + connect.WithSchema(registerServiceMethods.ByName("RegisterPlugin")), + connect.WithHandlerOptions(opts...), + ) + return "/plugins.v1.RegisterService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case RegisterServiceRegisterPluginProcedure: + registerServiceRegisterPluginHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedRegisterServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedRegisterServiceHandler struct{} + +func (UnimplementedRegisterServiceHandler) RegisterPlugin(context.Context, *connect.Request[v1.RegisterPluginRequest]) (*connect.Response[v1.RegisterPluginResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugins.v1.RegisterService.RegisterPlugin is not implemented")) +} + +// PluginServiceClient is a client for the plugins.v1.PluginService service. +type PluginServiceClient interface { + // Shutdown a plugin (let it know the runtime is going down). + Shutdown(context.Context, *connect.Request[v1.ShutdownRequest]) (*connect.Response[v1.ShutdownResponse], error) +} + +// NewPluginServiceClient constructs a client for the plugins.v1.PluginService service. By default, +// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and +// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() +// or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewPluginServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PluginServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + pluginServiceMethods := v1.File_plugins_v1_api_proto.Services().ByName("PluginService").Methods() + return &pluginServiceClient{ + shutdown: connect.NewClient[v1.ShutdownRequest, v1.ShutdownResponse]( + httpClient, + baseURL+PluginServiceShutdownProcedure, + connect.WithSchema(pluginServiceMethods.ByName("Shutdown")), + connect.WithClientOptions(opts...), + ), + } +} + +// pluginServiceClient implements PluginServiceClient. +type pluginServiceClient struct { + shutdown *connect.Client[v1.ShutdownRequest, v1.ShutdownResponse] +} + +// Shutdown calls plugins.v1.PluginService.Shutdown. +func (c *pluginServiceClient) Shutdown(ctx context.Context, req *connect.Request[v1.ShutdownRequest]) (*connect.Response[v1.ShutdownResponse], error) { + return c.shutdown.CallUnary(ctx, req) +} + +// PluginServiceHandler is an implementation of the plugins.v1.PluginService service. +type PluginServiceHandler interface { + // Shutdown a plugin (let it know the runtime is going down). + Shutdown(context.Context, *connect.Request[v1.ShutdownRequest]) (*connect.Response[v1.ShutdownResponse], error) +} + +// NewPluginServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewPluginServiceHandler(svc PluginServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + pluginServiceMethods := v1.File_plugins_v1_api_proto.Services().ByName("PluginService").Methods() + pluginServiceShutdownHandler := connect.NewUnaryHandler( + PluginServiceShutdownProcedure, + svc.Shutdown, + connect.WithSchema(pluginServiceMethods.ByName("Shutdown")), + connect.WithHandlerOptions(opts...), + ) + return "/plugins.v1.PluginService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case PluginServiceShutdownProcedure: + pluginServiceShutdownHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedPluginServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedPluginServiceHandler struct{} + +func (UnimplementedPluginServiceHandler) Shutdown(context.Context, *connect.Request[v1.ShutdownRequest]) (*connect.Response[v1.ShutdownResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugins.v1.PluginService.Shutdown is not implemented")) +} + +// PluginManagementServiceClient is a client for the plugins.v1.PluginManagementService service. +type PluginManagementServiceClient interface { + // ListPlugins returns all plugins registered with the engine. + ListPlugins(context.Context, *connect.Request[v1.ListPluginsRequest]) (*connect.Response[v1.ListPluginsResponse], error) + // EnablePlugin enables a plugin by name. + EnablePlugin(context.Context, *connect.Request[v1.EnablePluginRequest]) (*connect.Response[v1.EnablePluginResponse], error) + // DisablePlugin disables a plugin by name. + DisablePlugin(context.Context, *connect.Request[v1.DisablePluginRequest]) (*connect.Response[v1.DisablePluginResponse], error) +} + +// NewPluginManagementServiceClient constructs a client for the plugins.v1.PluginManagementService +// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for +// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply +// the connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewPluginManagementServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PluginManagementServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + pluginManagementServiceMethods := v1.File_plugins_v1_api_proto.Services().ByName("PluginManagementService").Methods() + return &pluginManagementServiceClient{ + listPlugins: connect.NewClient[v1.ListPluginsRequest, v1.ListPluginsResponse]( + httpClient, + baseURL+PluginManagementServiceListPluginsProcedure, + connect.WithSchema(pluginManagementServiceMethods.ByName("ListPlugins")), + connect.WithClientOptions(opts...), + ), + enablePlugin: connect.NewClient[v1.EnablePluginRequest, v1.EnablePluginResponse]( + httpClient, + baseURL+PluginManagementServiceEnablePluginProcedure, + connect.WithSchema(pluginManagementServiceMethods.ByName("EnablePlugin")), + connect.WithClientOptions(opts...), + ), + disablePlugin: connect.NewClient[v1.DisablePluginRequest, v1.DisablePluginResponse]( + httpClient, + baseURL+PluginManagementServiceDisablePluginProcedure, + connect.WithSchema(pluginManagementServiceMethods.ByName("DisablePlugin")), + connect.WithClientOptions(opts...), + ), + } +} + +// pluginManagementServiceClient implements PluginManagementServiceClient. +type pluginManagementServiceClient struct { + listPlugins *connect.Client[v1.ListPluginsRequest, v1.ListPluginsResponse] + enablePlugin *connect.Client[v1.EnablePluginRequest, v1.EnablePluginResponse] + disablePlugin *connect.Client[v1.DisablePluginRequest, v1.DisablePluginResponse] +} + +// ListPlugins calls plugins.v1.PluginManagementService.ListPlugins. +func (c *pluginManagementServiceClient) ListPlugins(ctx context.Context, req *connect.Request[v1.ListPluginsRequest]) (*connect.Response[v1.ListPluginsResponse], error) { + return c.listPlugins.CallUnary(ctx, req) +} + +// EnablePlugin calls plugins.v1.PluginManagementService.EnablePlugin. +func (c *pluginManagementServiceClient) EnablePlugin(ctx context.Context, req *connect.Request[v1.EnablePluginRequest]) (*connect.Response[v1.EnablePluginResponse], error) { + return c.enablePlugin.CallUnary(ctx, req) +} + +// DisablePlugin calls plugins.v1.PluginManagementService.DisablePlugin. +func (c *pluginManagementServiceClient) DisablePlugin(ctx context.Context, req *connect.Request[v1.DisablePluginRequest]) (*connect.Response[v1.DisablePluginResponse], error) { + return c.disablePlugin.CallUnary(ctx, req) +} + +// PluginManagementServiceHandler is an implementation of the plugins.v1.PluginManagementService +// service. +type PluginManagementServiceHandler interface { + // ListPlugins returns all plugins registered with the engine. + ListPlugins(context.Context, *connect.Request[v1.ListPluginsRequest]) (*connect.Response[v1.ListPluginsResponse], error) + // EnablePlugin enables a plugin by name. + EnablePlugin(context.Context, *connect.Request[v1.EnablePluginRequest]) (*connect.Response[v1.EnablePluginResponse], error) + // DisablePlugin disables a plugin by name. + DisablePlugin(context.Context, *connect.Request[v1.DisablePluginRequest]) (*connect.Response[v1.DisablePluginResponse], error) +} + +// NewPluginManagementServiceHandler builds an HTTP handler from the service implementation. It +// returns the path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewPluginManagementServiceHandler(svc PluginManagementServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + pluginManagementServiceMethods := v1.File_plugins_v1_api_proto.Services().ByName("PluginManagementService").Methods() + pluginManagementServiceListPluginsHandler := connect.NewUnaryHandler( + PluginManagementServiceListPluginsProcedure, + svc.ListPlugins, + connect.WithSchema(pluginManagementServiceMethods.ByName("ListPlugins")), + connect.WithHandlerOptions(opts...), + ) + pluginManagementServiceEnablePluginHandler := connect.NewUnaryHandler( + PluginManagementServiceEnablePluginProcedure, + svc.EnablePlugin, + connect.WithSchema(pluginManagementServiceMethods.ByName("EnablePlugin")), + connect.WithHandlerOptions(opts...), + ) + pluginManagementServiceDisablePluginHandler := connect.NewUnaryHandler( + PluginManagementServiceDisablePluginProcedure, + svc.DisablePlugin, + connect.WithSchema(pluginManagementServiceMethods.ByName("DisablePlugin")), + connect.WithHandlerOptions(opts...), + ) + return "/plugins.v1.PluginManagementService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case PluginManagementServiceListPluginsProcedure: + pluginManagementServiceListPluginsHandler.ServeHTTP(w, r) + case PluginManagementServiceEnablePluginProcedure: + pluginManagementServiceEnablePluginHandler.ServeHTTP(w, r) + case PluginManagementServiceDisablePluginProcedure: + pluginManagementServiceDisablePluginHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedPluginManagementServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedPluginManagementServiceHandler struct{} + +func (UnimplementedPluginManagementServiceHandler) ListPlugins(context.Context, *connect.Request[v1.ListPluginsRequest]) (*connect.Response[v1.ListPluginsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugins.v1.PluginManagementService.ListPlugins is not implemented")) +} + +func (UnimplementedPluginManagementServiceHandler) EnablePlugin(context.Context, *connect.Request[v1.EnablePluginRequest]) (*connect.Response[v1.EnablePluginResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugins.v1.PluginManagementService.EnablePlugin is not implemented")) +} + +func (UnimplementedPluginManagementServiceHandler) DisablePlugin(context.Context, *connect.Request[v1.DisablePluginRequest]) (*connect.Response[v1.DisablePluginResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugins.v1.PluginManagementService.DisablePlugin is not implemented")) +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/resolver/resolver.go b/vendor/github.com/docker/secrets-engine/x/api/resolver/resolver.go new file mode 100644 index 000000000000..d55bd1538309 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/resolver/resolver.go @@ -0,0 +1,203 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resolver + +import ( + "context" + "errors" + "fmt" + "time" + + "connectrpc.com/connect" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + + resolverv1 "github.com/docker/secrets-engine/x/api/resolver/v1" + "github.com/docker/secrets-engine/x/api/resolver/v1/resolverv1connect" + "github.com/docker/secrets-engine/x/secrets" +) + +var _ resolverv1connect.ResolverServiceHandler = &resolverService{} + +type resolverService struct { + resolver secrets.Resolver +} + +func NewResolverHandler(r secrets.Resolver) resolverv1connect.ResolverServiceHandler { + return &resolverService{r} +} + +func (r resolverService) GetSecrets(ctx context.Context, c *connect.Request[resolverv1.GetSecretsRequest]) (*connect.Response[resolverv1.GetSecretsResponse], error) { + msgPattern := c.Msg.GetPattern() + pattern, err := secrets.ParsePattern(msgPattern) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid pattern %q: %w", msgPattern, err)) + } + + envelopes, err := r.resolver.GetSecrets(ctx, pattern) + if err != nil { + if errors.Is(err, secrets.ErrNotFound) { + return nil, connect.NewError(connect.CodeNotFound, secrets.ErrNotFound) + } + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to get secret %q: %w", msgPattern, err)) + } + if len(envelopes) == 0 { + return nil, connect.NewError(connect.CodeNotFound, secrets.ErrNotFound) + } + var items []*resolverv1.GetSecretsResponse_Envelope + for _, envelope := range envelopes { + items = append(items, resolverv1.GetSecretsResponse_Envelope_builder{ + Id: proto.String(envelope.ID.String()), + Value: envelope.Value, + Metadata: envelope.Metadata, + Provider: proto.String(envelope.Provider), + Version: proto.String(envelope.Version), + CreatedAt: timestamppb.New(envelope.CreatedAt), + ResolvedAt: timestamppb.New(envelope.ResolvedAt), + ExpiresAt: timestamppb.New(envelope.ExpiresAt), + }.Build()) + } + return connect.NewResponse(resolverv1.GetSecretsResponse_builder{ + Envelopes: items, + }.Build()), nil +} + +var _ secrets.Resolver = &resolverClient{} + +type resolverClient struct { + resolverClient resolverv1connect.ResolverServiceClient +} + +func NewResolverClient(httpClient connect.HTTPClient) secrets.Resolver { + return &resolverClient{resolverClient: resolverv1connect.NewResolverServiceClient(httpClient, "http://unix")} +} + +func (r resolverClient) GetSecrets(ctx context.Context, pattern secrets.Pattern) ([]secrets.Envelope, error) { + req := connect.NewRequest(resolverv1.GetSecretsRequest_builder{ + Pattern: proto.String(pattern.String()), + }.Build()) + resp, err := r.resolverClient.GetSecrets(ctx, req) + if err != nil { + if connect.CodeOf(err) == connect.CodeNotFound { + err = secrets.ErrNotFound + } + return nil, err + } + + var envelopes []secrets.Envelope + for _, item := range resp.Msg.GetEnvelopes() { + id, err := secrets.ParseID(item.GetId()) + if err != nil { + continue + } + envelopes = append(envelopes, secrets.Envelope{ + ID: id, + Value: item.GetValue(), + Metadata: item.GetMetadata(), + Provider: item.GetProvider(), + Version: item.GetVersion(), + CreatedAt: item.GetCreatedAt().AsTime(), + ResolvedAt: item.GetResolvedAt().AsTime(), + ExpiresAt: item.GetExpiresAt().AsTime(), + }) + } + return envelopes, nil +} + +var _ resolverv1connect.AuthorizerServiceHandler = &authorizerService{} + +type authorizerService struct { + authorizer secrets.Authorizer +} + +// NewAuthorizerHandler adapts a secrets.Authorizer to the generated connect +// handler interface. It is served by the engine, not by secrets-provider +// plugins. +func NewAuthorizerHandler(a secrets.Authorizer) resolverv1connect.AuthorizerServiceHandler { + return &authorizerService{a} +} + +func (s authorizerService) Authorize(ctx context.Context, c *connect.Request[resolverv1.AuthorizeRequest]) (*connect.Response[resolverv1.AuthorizeResponse], error) { + msgPatterns := c.Msg.GetPatterns() + if len(msgPatterns) == 0 { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("at least one pattern is required")) + } + patterns := make([]secrets.Pattern, 0, len(msgPatterns)) + for _, msgPattern := range msgPatterns { + pattern, err := secrets.ParsePattern(msgPattern) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid pattern %q: %w", msgPattern, err)) + } + patterns = append(patterns, pattern) + } + + resp, err := s.authorizer.Authorize(ctx, patterns...) + if err != nil { + if errors.Is(err, secrets.ErrAccessDenied) { + return nil, connect.NewError(connect.CodePermissionDenied, secrets.ErrAccessDenied) + } + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("authorize failed for %q: %w", msgPatterns, err)) + } + decision := resolverv1.Decision_DECISION_DENY + if resp.Allow { + decision = resolverv1.Decision_DECISION_ALLOW + } + // A zero Expiry means the decision never expires: leave the timestamp absent. + var expiresAt *timestamppb.Timestamp + if !resp.Expiry.IsZero() { + expiresAt = timestamppb.New(resp.Expiry) + } + return connect.NewResponse(resolverv1.AuthorizeResponse_builder{ + ExpiresAt: expiresAt, + Decision: &decision, + }.Build()), nil +} + +var _ secrets.Authorizer = &authorizerClient{} + +type authorizerClient struct { + client resolverv1connect.AuthorizerServiceClient +} + +func NewAuthorizerClient(httpClient connect.HTTPClient) secrets.Authorizer { + return &authorizerClient{client: resolverv1connect.NewAuthorizerServiceClient(httpClient, "http://unix")} +} + +func (a authorizerClient) Authorize(ctx context.Context, patterns ...secrets.Pattern) (secrets.AuthorizeResponse, error) { + msgPatterns := make([]string, 0, len(patterns)) + for _, p := range patterns { + msgPatterns = append(msgPatterns, p.String()) + } + resp, err := a.client.Authorize(ctx, connect.NewRequest(resolverv1.AuthorizeRequest_builder{ + Patterns: msgPatterns, + }.Build())) + if err != nil { + if connect.CodeOf(err) == connect.CodePermissionDenied { + err = secrets.ErrAccessDenied + } + return secrets.AuthorizeResponse{}, err + } + + // An absent timestamp means the decision never expires: keep the zero time. + var expiry time.Time + if resp.Msg.HasExpiresAt() { + expiry = resp.Msg.GetExpiresAt().AsTime() + } + return secrets.AuthorizeResponse{ + Expiry: expiry, + Allow: resp.Msg.GetDecision() == resolverv1.Decision_DECISION_ALLOW, + }, nil +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/resolver/v1/api.pb.go b/vendor/github.com/docker/secrets-engine/x/api/resolver/v1/api.pb.go new file mode 100644 index 000000000000..a81fe7361470 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/resolver/v1/api.pb.go @@ -0,0 +1,728 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc (unknown) +// source: resolver/v1/api.proto + +package resolverv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Decision int32 + +const ( + // buf:lint:ignore ENUM_ZERO_VALUE_SUFFIX + Decision_DECISION_DENY Decision = 0 + Decision_DECISION_ALLOW Decision = 1 +) + +// Enum value maps for Decision. +var ( + Decision_name = map[int32]string{ + 0: "DECISION_DENY", + 1: "DECISION_ALLOW", + } + Decision_value = map[string]int32{ + "DECISION_DENY": 0, + "DECISION_ALLOW": 1, + } +) + +func (x Decision) Enum() *Decision { + p := new(Decision) + *p = x + return p +} + +func (x Decision) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Decision) Descriptor() protoreflect.EnumDescriptor { + return file_resolver_v1_api_proto_enumTypes[0].Descriptor() +} + +func (Decision) Type() protoreflect.EnumType { + return &file_resolver_v1_api_proto_enumTypes[0] +} + +func (x Decision) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +type GetSecretsRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Pattern *string `protobuf:"bytes,1,opt,name=pattern"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSecretsRequest) Reset() { + *x = GetSecretsRequest{} + mi := &file_resolver_v1_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSecretsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSecretsRequest) ProtoMessage() {} + +func (x *GetSecretsRequest) ProtoReflect() protoreflect.Message { + mi := &file_resolver_v1_api_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *GetSecretsRequest) GetPattern() string { + if x != nil { + if x.xxx_hidden_Pattern != nil { + return *x.xxx_hidden_Pattern + } + return "" + } + return "" +} + +func (x *GetSecretsRequest) SetPattern(v string) { + x.xxx_hidden_Pattern = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 1) +} + +func (x *GetSecretsRequest) HasPattern() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *GetSecretsRequest) ClearPattern() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Pattern = nil +} + +type GetSecretsRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // ID of the secret to resolve. + Pattern *string +} + +func (b0 GetSecretsRequest_builder) Build() *GetSecretsRequest { + m0 := &GetSecretsRequest{} + b, x := &b0, m0 + _, _ = b, x + if b.Pattern != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 1) + x.xxx_hidden_Pattern = b.Pattern + } + return m0 +} + +type AuthorizeRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Patterns []string `protobuf:"bytes,1,rep,name=patterns"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizeRequest) Reset() { + *x = AuthorizeRequest{} + mi := &file_resolver_v1_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizeRequest) ProtoMessage() {} + +func (x *AuthorizeRequest) ProtoReflect() protoreflect.Message { + mi := &file_resolver_v1_api_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *AuthorizeRequest) GetPatterns() []string { + if x != nil { + return x.xxx_hidden_Patterns + } + return nil +} + +func (x *AuthorizeRequest) SetPatterns(v []string) { + x.xxx_hidden_Patterns = v +} + +type AuthorizeRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // Patterns to authorize; at least one required. + Patterns []string +} + +func (b0 AuthorizeRequest_builder) Build() *AuthorizeRequest { + m0 := &AuthorizeRequest{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_Patterns = b.Patterns + return m0 +} + +type AuthorizeResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Decision Decision `protobuf:"varint,1,opt,name=decision,enum=resolver.v1.Decision"` + xxx_hidden_ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizeResponse) Reset() { + *x = AuthorizeResponse{} + mi := &file_resolver_v1_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizeResponse) ProtoMessage() {} + +func (x *AuthorizeResponse) ProtoReflect() protoreflect.Message { + mi := &file_resolver_v1_api_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *AuthorizeResponse) GetDecision() Decision { + if x != nil { + if protoimpl.X.Present(&(x.XXX_presence[0]), 0) { + return x.xxx_hidden_Decision + } + } + return Decision_DECISION_DENY +} + +func (x *AuthorizeResponse) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.xxx_hidden_ExpiresAt + } + return nil +} + +func (x *AuthorizeResponse) SetDecision(v Decision) { + x.xxx_hidden_Decision = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 2) +} + +func (x *AuthorizeResponse) SetExpiresAt(v *timestamppb.Timestamp) { + x.xxx_hidden_ExpiresAt = v +} + +func (x *AuthorizeResponse) HasDecision() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *AuthorizeResponse) HasExpiresAt() bool { + if x == nil { + return false + } + return x.xxx_hidden_ExpiresAt != nil +} + +func (x *AuthorizeResponse) ClearDecision() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Decision = Decision_DECISION_DENY +} + +func (x *AuthorizeResponse) ClearExpiresAt() { + x.xxx_hidden_ExpiresAt = nil +} + +type AuthorizeResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // The authorization decision. + Decision *Decision + // When the authorization decision expires. + ExpiresAt *timestamppb.Timestamp +} + +func (b0 AuthorizeResponse_builder) Build() *AuthorizeResponse { + m0 := &AuthorizeResponse{} + b, x := &b0, m0 + _, _ = b, x + if b.Decision != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 2) + x.xxx_hidden_Decision = *b.Decision + } + x.xxx_hidden_ExpiresAt = b.ExpiresAt + return m0 +} + +type GetSecretsResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Envelopes *[]*GetSecretsResponse_Envelope `protobuf:"bytes,1,rep,name=envelopes"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSecretsResponse) Reset() { + *x = GetSecretsResponse{} + mi := &file_resolver_v1_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSecretsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSecretsResponse) ProtoMessage() {} + +func (x *GetSecretsResponse) ProtoReflect() protoreflect.Message { + mi := &file_resolver_v1_api_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *GetSecretsResponse) GetEnvelopes() []*GetSecretsResponse_Envelope { + if x != nil { + if x.xxx_hidden_Envelopes != nil { + return *x.xxx_hidden_Envelopes + } + } + return nil +} + +func (x *GetSecretsResponse) SetEnvelopes(v []*GetSecretsResponse_Envelope) { + x.xxx_hidden_Envelopes = &v +} + +type GetSecretsResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Envelopes []*GetSecretsResponse_Envelope +} + +func (b0 GetSecretsResponse_builder) Build() *GetSecretsResponse { + m0 := &GetSecretsResponse{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_Envelopes = &b.Envelopes + return m0 +} + +type GetSecretsResponse_Envelope struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Id *string `protobuf:"bytes,1,opt,name=id"` + xxx_hidden_Value []byte `protobuf:"bytes,2,opt,name=value"` + xxx_hidden_Provider *string `protobuf:"bytes,3,opt,name=provider"` + xxx_hidden_Version *string `protobuf:"bytes,4,opt,name=version"` + xxx_hidden_CreatedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=created_at,json=createdAt"` + xxx_hidden_ResolvedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=resolved_at,json=resolvedAt"` + xxx_hidden_ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expires_at,json=expiresAt"` + xxx_hidden_Metadata map[string]string `protobuf:"bytes,8,rep,name=metadata" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSecretsResponse_Envelope) Reset() { + *x = GetSecretsResponse_Envelope{} + mi := &file_resolver_v1_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSecretsResponse_Envelope) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSecretsResponse_Envelope) ProtoMessage() {} + +func (x *GetSecretsResponse_Envelope) ProtoReflect() protoreflect.Message { + mi := &file_resolver_v1_api_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *GetSecretsResponse_Envelope) GetId() string { + if x != nil { + if x.xxx_hidden_Id != nil { + return *x.xxx_hidden_Id + } + return "" + } + return "" +} + +func (x *GetSecretsResponse_Envelope) GetValue() []byte { + if x != nil { + return x.xxx_hidden_Value + } + return nil +} + +func (x *GetSecretsResponse_Envelope) GetProvider() string { + if x != nil { + if x.xxx_hidden_Provider != nil { + return *x.xxx_hidden_Provider + } + return "" + } + return "" +} + +func (x *GetSecretsResponse_Envelope) GetVersion() string { + if x != nil { + if x.xxx_hidden_Version != nil { + return *x.xxx_hidden_Version + } + return "" + } + return "" +} + +func (x *GetSecretsResponse_Envelope) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.xxx_hidden_CreatedAt + } + return nil +} + +func (x *GetSecretsResponse_Envelope) GetResolvedAt() *timestamppb.Timestamp { + if x != nil { + return x.xxx_hidden_ResolvedAt + } + return nil +} + +func (x *GetSecretsResponse_Envelope) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.xxx_hidden_ExpiresAt + } + return nil +} + +func (x *GetSecretsResponse_Envelope) GetMetadata() map[string]string { + if x != nil { + return x.xxx_hidden_Metadata + } + return nil +} + +func (x *GetSecretsResponse_Envelope) SetId(v string) { + x.xxx_hidden_Id = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 8) +} + +func (x *GetSecretsResponse_Envelope) SetValue(v []byte) { + if v == nil { + v = []byte{} + } + x.xxx_hidden_Value = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 8) +} + +func (x *GetSecretsResponse_Envelope) SetProvider(v string) { + x.xxx_hidden_Provider = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 8) +} + +func (x *GetSecretsResponse_Envelope) SetVersion(v string) { + x.xxx_hidden_Version = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 3, 8) +} + +func (x *GetSecretsResponse_Envelope) SetCreatedAt(v *timestamppb.Timestamp) { + x.xxx_hidden_CreatedAt = v +} + +func (x *GetSecretsResponse_Envelope) SetResolvedAt(v *timestamppb.Timestamp) { + x.xxx_hidden_ResolvedAt = v +} + +func (x *GetSecretsResponse_Envelope) SetExpiresAt(v *timestamppb.Timestamp) { + x.xxx_hidden_ExpiresAt = v +} + +func (x *GetSecretsResponse_Envelope) SetMetadata(v map[string]string) { + x.xxx_hidden_Metadata = v +} + +func (x *GetSecretsResponse_Envelope) HasId() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *GetSecretsResponse_Envelope) HasValue() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *GetSecretsResponse_Envelope) HasProvider() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 2) +} + +func (x *GetSecretsResponse_Envelope) HasVersion() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 3) +} + +func (x *GetSecretsResponse_Envelope) HasCreatedAt() bool { + if x == nil { + return false + } + return x.xxx_hidden_CreatedAt != nil +} + +func (x *GetSecretsResponse_Envelope) HasResolvedAt() bool { + if x == nil { + return false + } + return x.xxx_hidden_ResolvedAt != nil +} + +func (x *GetSecretsResponse_Envelope) HasExpiresAt() bool { + if x == nil { + return false + } + return x.xxx_hidden_ExpiresAt != nil +} + +func (x *GetSecretsResponse_Envelope) ClearId() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Id = nil +} + +func (x *GetSecretsResponse_Envelope) ClearValue() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_Value = nil +} + +func (x *GetSecretsResponse_Envelope) ClearProvider() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 2) + x.xxx_hidden_Provider = nil +} + +func (x *GetSecretsResponse_Envelope) ClearVersion() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 3) + x.xxx_hidden_Version = nil +} + +func (x *GetSecretsResponse_Envelope) ClearCreatedAt() { + x.xxx_hidden_CreatedAt = nil +} + +func (x *GetSecretsResponse_Envelope) ClearResolvedAt() { + x.xxx_hidden_ResolvedAt = nil +} + +func (x *GetSecretsResponse_Envelope) ClearExpiresAt() { + x.xxx_hidden_ExpiresAt = nil +} + +type GetSecretsResponse_Envelope_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // ID of the secret to resolve. + Id *string + // The resolved secret value. + Value []byte + Provider *string + Version *string + CreatedAt *timestamppb.Timestamp + ResolvedAt *timestamppb.Timestamp + ExpiresAt *timestamppb.Timestamp + Metadata map[string]string +} + +func (b0 GetSecretsResponse_Envelope_builder) Build() *GetSecretsResponse_Envelope { + m0 := &GetSecretsResponse_Envelope{} + b, x := &b0, m0 + _, _ = b, x + if b.Id != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 8) + x.xxx_hidden_Id = b.Id + } + if b.Value != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 8) + x.xxx_hidden_Value = b.Value + } + if b.Provider != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 8) + x.xxx_hidden_Provider = b.Provider + } + if b.Version != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 3, 8) + x.xxx_hidden_Version = b.Version + } + x.xxx_hidden_CreatedAt = b.CreatedAt + x.xxx_hidden_ResolvedAt = b.ResolvedAt + x.xxx_hidden_ExpiresAt = b.ExpiresAt + x.xxx_hidden_Metadata = b.Metadata + return m0 +} + +var File_resolver_v1_api_proto protoreflect.FileDescriptor + +const file_resolver_v1_api_proto_rawDesc = "" + + "\n" + + "\x15resolver/v1/api.proto\x12\vresolver.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"-\n" + + "\x11GetSecretsRequest\x12\x18\n" + + "\apattern\x18\x01 \x01(\tR\apattern\".\n" + + "\x10AuthorizeRequest\x12\x1a\n" + + "\bpatterns\x18\x01 \x03(\tR\bpatterns\"\x81\x01\n" + + "\x11AuthorizeResponse\x121\n" + + "\bdecision\x18\x01 \x01(\x0e2\x15.resolver.v1.DecisionR\bdecision\x129\n" + + "\n" + + "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"\x89\x04\n" + + "\x12GetSecretsResponse\x12F\n" + + "\tenvelopes\x18\x01 \x03(\v2(.resolver.v1.GetSecretsResponse.EnvelopeR\tenvelopes\x1a\xaa\x03\n" + + "\bEnvelope\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value\x12\x1a\n" + + "\bprovider\x18\x03 \x01(\tR\bprovider\x12\x18\n" + + "\aversion\x18\x04 \x01(\tR\aversion\x129\n" + + "\n" + + "created_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12;\n" + + "\vresolved_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "resolvedAt\x129\n" + + "\n" + + "expires_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12R\n" + + "\bmetadata\x18\b \x03(\v26.resolver.v1.GetSecretsResponse.Envelope.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01*1\n" + + "\bDecision\x12\x11\n" + + "\rDECISION_DENY\x10\x00\x12\x12\n" + + "\x0eDECISION_ALLOW\x10\x012`\n" + + "\x0fResolverService\x12M\n" + + "\n" + + "GetSecrets\x12\x1e.resolver.v1.GetSecretsRequest\x1a\x1f.resolver.v1.GetSecretsResponse2_\n" + + "\x11AuthorizerService\x12J\n" + + "\tAuthorize\x12\x1d.resolver.v1.AuthorizeRequest\x1a\x1e.resolver.v1.AuthorizeResponseB\xa7\x01\n" + + "\x0fcom.resolver.v1B\bApiProtoP\x01Z=github.com/docker/secrets-engine/x/api/resolver/v1;resolverv1\xa2\x02\x03RXX\xaa\x02\vResolver.V1\xca\x02\vResolver\\V1\xe2\x02\x17Resolver\\V1\\GPBMetadata\xea\x02\fResolver::V1b\beditionsp\xe8\a" + +var file_resolver_v1_api_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_resolver_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_resolver_v1_api_proto_goTypes = []any{ + (Decision)(0), // 0: resolver.v1.Decision + (*GetSecretsRequest)(nil), // 1: resolver.v1.GetSecretsRequest + (*AuthorizeRequest)(nil), // 2: resolver.v1.AuthorizeRequest + (*AuthorizeResponse)(nil), // 3: resolver.v1.AuthorizeResponse + (*GetSecretsResponse)(nil), // 4: resolver.v1.GetSecretsResponse + (*GetSecretsResponse_Envelope)(nil), // 5: resolver.v1.GetSecretsResponse.Envelope + nil, // 6: resolver.v1.GetSecretsResponse.Envelope.MetadataEntry + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp +} +var file_resolver_v1_api_proto_depIdxs = []int32{ + 0, // 0: resolver.v1.AuthorizeResponse.decision:type_name -> resolver.v1.Decision + 7, // 1: resolver.v1.AuthorizeResponse.expires_at:type_name -> google.protobuf.Timestamp + 5, // 2: resolver.v1.GetSecretsResponse.envelopes:type_name -> resolver.v1.GetSecretsResponse.Envelope + 7, // 3: resolver.v1.GetSecretsResponse.Envelope.created_at:type_name -> google.protobuf.Timestamp + 7, // 4: resolver.v1.GetSecretsResponse.Envelope.resolved_at:type_name -> google.protobuf.Timestamp + 7, // 5: resolver.v1.GetSecretsResponse.Envelope.expires_at:type_name -> google.protobuf.Timestamp + 6, // 6: resolver.v1.GetSecretsResponse.Envelope.metadata:type_name -> resolver.v1.GetSecretsResponse.Envelope.MetadataEntry + 1, // 7: resolver.v1.ResolverService.GetSecrets:input_type -> resolver.v1.GetSecretsRequest + 2, // 8: resolver.v1.AuthorizerService.Authorize:input_type -> resolver.v1.AuthorizeRequest + 4, // 9: resolver.v1.ResolverService.GetSecrets:output_type -> resolver.v1.GetSecretsResponse + 3, // 10: resolver.v1.AuthorizerService.Authorize:output_type -> resolver.v1.AuthorizeResponse + 9, // [9:11] is the sub-list for method output_type + 7, // [7:9] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_resolver_v1_api_proto_init() } +func file_resolver_v1_api_proto_init() { + if File_resolver_v1_api_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_resolver_v1_api_proto_rawDesc), len(file_resolver_v1_api_proto_rawDesc)), + NumEnums: 1, + NumMessages: 6, + NumExtensions: 0, + NumServices: 2, + }, + GoTypes: file_resolver_v1_api_proto_goTypes, + DependencyIndexes: file_resolver_v1_api_proto_depIdxs, + EnumInfos: file_resolver_v1_api_proto_enumTypes, + MessageInfos: file_resolver_v1_api_proto_msgTypes, + }.Build() + File_resolver_v1_api_proto = out.File + file_resolver_v1_api_proto_goTypes = nil + file_resolver_v1_api_proto_depIdxs = nil +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/resolver/v1/api.proto b/vendor/github.com/docker/secrets-engine/x/api/resolver/v1/api.proto new file mode 100644 index 000000000000..ddb0482bc222 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/resolver/v1/api.proto @@ -0,0 +1,79 @@ +// Copyright 2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +edition = "2023"; + +package resolver.v1; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/docker/secrets-engine/x/api/resolver/v1;resolverv1"; + +service ResolverService { + // Resolve a secret by its ID. + rpc GetSecrets(GetSecretsRequest) returns (GetSecretsResponse); +} + +service AuthorizerService { + // Preflight: authorizes the caller for the patterns ahead of use, valid + // until the returned expiry. + rpc Authorize(AuthorizeRequest) returns (AuthorizeResponse); +} + +message GetSecretsRequest { + // ID of the secret to resolve. + string pattern = 1; +} + +message AuthorizeRequest { + // Patterns to authorize; at least one required. + repeated string patterns = 1; +} + +enum Decision { + // buf:lint:ignore ENUM_ZERO_VALUE_SUFFIX + DECISION_DENY = 0; + DECISION_ALLOW = 1; +} + +message AuthorizeResponse { + // The authorization decision. + Decision decision = 1; + + // When the authorization decision expires. + google.protobuf.Timestamp expires_at = 2; +} + +message GetSecretsResponse { + repeated Envelope envelopes = 1; + + message Envelope { + // ID of the secret to resolve. + string id = 1; + // The resolved secret value. + bytes value = 2; + + string provider = 3; + + string version = 4; + + google.protobuf.Timestamp created_at = 5; + + google.protobuf.Timestamp resolved_at = 6; + + google.protobuf.Timestamp expires_at = 7; + + map metadata = 8; + } +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/resolver/v1/resolverv1connect/api.connect.go b/vendor/github.com/docker/secrets-engine/x/api/resolver/v1/resolverv1connect/api.connect.go new file mode 100644 index 000000000000..f18413027aa4 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/resolver/v1/resolverv1connect/api.connect.go @@ -0,0 +1,190 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: resolver/v1/api.proto + +package resolverv1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/docker/secrets-engine/x/api/resolver/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // ResolverServiceName is the fully-qualified name of the ResolverService service. + ResolverServiceName = "resolver.v1.ResolverService" + // AuthorizerServiceName is the fully-qualified name of the AuthorizerService service. + AuthorizerServiceName = "resolver.v1.AuthorizerService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // ResolverServiceGetSecretsProcedure is the fully-qualified name of the ResolverService's + // GetSecrets RPC. + ResolverServiceGetSecretsProcedure = "/resolver.v1.ResolverService/GetSecrets" + // AuthorizerServiceAuthorizeProcedure is the fully-qualified name of the AuthorizerService's + // Authorize RPC. + AuthorizerServiceAuthorizeProcedure = "/resolver.v1.AuthorizerService/Authorize" +) + +// ResolverServiceClient is a client for the resolver.v1.ResolverService service. +type ResolverServiceClient interface { + // Resolve a secret by its ID. + GetSecrets(context.Context, *connect.Request[v1.GetSecretsRequest]) (*connect.Response[v1.GetSecretsResponse], error) +} + +// NewResolverServiceClient constructs a client for the resolver.v1.ResolverService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewResolverServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ResolverServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + resolverServiceMethods := v1.File_resolver_v1_api_proto.Services().ByName("ResolverService").Methods() + return &resolverServiceClient{ + getSecrets: connect.NewClient[v1.GetSecretsRequest, v1.GetSecretsResponse]( + httpClient, + baseURL+ResolverServiceGetSecretsProcedure, + connect.WithSchema(resolverServiceMethods.ByName("GetSecrets")), + connect.WithClientOptions(opts...), + ), + } +} + +// resolverServiceClient implements ResolverServiceClient. +type resolverServiceClient struct { + getSecrets *connect.Client[v1.GetSecretsRequest, v1.GetSecretsResponse] +} + +// GetSecrets calls resolver.v1.ResolverService.GetSecrets. +func (c *resolverServiceClient) GetSecrets(ctx context.Context, req *connect.Request[v1.GetSecretsRequest]) (*connect.Response[v1.GetSecretsResponse], error) { + return c.getSecrets.CallUnary(ctx, req) +} + +// ResolverServiceHandler is an implementation of the resolver.v1.ResolverService service. +type ResolverServiceHandler interface { + // Resolve a secret by its ID. + GetSecrets(context.Context, *connect.Request[v1.GetSecretsRequest]) (*connect.Response[v1.GetSecretsResponse], error) +} + +// NewResolverServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewResolverServiceHandler(svc ResolverServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + resolverServiceMethods := v1.File_resolver_v1_api_proto.Services().ByName("ResolverService").Methods() + resolverServiceGetSecretsHandler := connect.NewUnaryHandler( + ResolverServiceGetSecretsProcedure, + svc.GetSecrets, + connect.WithSchema(resolverServiceMethods.ByName("GetSecrets")), + connect.WithHandlerOptions(opts...), + ) + return "/resolver.v1.ResolverService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case ResolverServiceGetSecretsProcedure: + resolverServiceGetSecretsHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedResolverServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedResolverServiceHandler struct{} + +func (UnimplementedResolverServiceHandler) GetSecrets(context.Context, *connect.Request[v1.GetSecretsRequest]) (*connect.Response[v1.GetSecretsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("resolver.v1.ResolverService.GetSecrets is not implemented")) +} + +// AuthorizerServiceClient is a client for the resolver.v1.AuthorizerService service. +type AuthorizerServiceClient interface { + // Preflight: authorizes the caller for the patterns ahead of use, valid + // until the returned expiry. + Authorize(context.Context, *connect.Request[v1.AuthorizeRequest]) (*connect.Response[v1.AuthorizeResponse], error) +} + +// NewAuthorizerServiceClient constructs a client for the resolver.v1.AuthorizerService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewAuthorizerServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AuthorizerServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + authorizerServiceMethods := v1.File_resolver_v1_api_proto.Services().ByName("AuthorizerService").Methods() + return &authorizerServiceClient{ + authorize: connect.NewClient[v1.AuthorizeRequest, v1.AuthorizeResponse]( + httpClient, + baseURL+AuthorizerServiceAuthorizeProcedure, + connect.WithSchema(authorizerServiceMethods.ByName("Authorize")), + connect.WithClientOptions(opts...), + ), + } +} + +// authorizerServiceClient implements AuthorizerServiceClient. +type authorizerServiceClient struct { + authorize *connect.Client[v1.AuthorizeRequest, v1.AuthorizeResponse] +} + +// Authorize calls resolver.v1.AuthorizerService.Authorize. +func (c *authorizerServiceClient) Authorize(ctx context.Context, req *connect.Request[v1.AuthorizeRequest]) (*connect.Response[v1.AuthorizeResponse], error) { + return c.authorize.CallUnary(ctx, req) +} + +// AuthorizerServiceHandler is an implementation of the resolver.v1.AuthorizerService service. +type AuthorizerServiceHandler interface { + // Preflight: authorizes the caller for the patterns ahead of use, valid + // until the returned expiry. + Authorize(context.Context, *connect.Request[v1.AuthorizeRequest]) (*connect.Response[v1.AuthorizeResponse], error) +} + +// NewAuthorizerServiceHandler builds an HTTP handler from the service implementation. It returns +// the path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewAuthorizerServiceHandler(svc AuthorizerServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + authorizerServiceMethods := v1.File_resolver_v1_api_proto.Services().ByName("AuthorizerService").Methods() + authorizerServiceAuthorizeHandler := connect.NewUnaryHandler( + AuthorizerServiceAuthorizeProcedure, + svc.Authorize, + connect.WithSchema(authorizerServiceMethods.ByName("Authorize")), + connect.WithHandlerOptions(opts...), + ) + return "/resolver.v1.AuthorizerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case AuthorizerServiceAuthorizeProcedure: + authorizerServiceAuthorizeHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedAuthorizerServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedAuthorizerServiceHandler struct{} + +func (UnimplementedAuthorizerServiceHandler) Authorize(context.Context, *connect.Request[v1.AuthorizeRequest]) (*connect.Response[v1.AuthorizeResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("resolver.v1.AuthorizerService.Authorize is not implemented")) +} diff --git a/vendor/github.com/docker/secrets-engine/x/api/version.go b/vendor/github.com/docker/secrets-engine/x/api/version.go new file mode 100644 index 000000000000..08df5d3e2539 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/api/version.go @@ -0,0 +1,80 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "errors" + "fmt" + + "golang.org/x/mod/semver" +) + +var ( + ErrEmptyVersion = errors.New("empty version") + ErrVPrefix = errors.New("missing version prefix") +) + +type Version interface { + String() string +} + +type version struct { + value string +} + +func (v *version) String() string { + return v.value +} + +// NewVersion creates a new [Version] from a string +// If a validation error occurs, it returns nil and the error. +func NewVersion(s string) (Version, error) { + if err := valid(s); err != nil { + return nil, fmt.Errorf("parsing version: %w", err) + } + return &version{ + value: s, + }, nil +} + +// MustNewVersion creates a new Version as [NewVersion] does, but panics when a validation +// error occurs. +func MustNewVersion(s string) Version { + v, err := NewVersion(s) + if err != nil { + panic(err) + } + return v +} + +// Compare returns an integer comparing two versions according to semantic versioning. +// The result will be 0 if a == b, -1 if a < b, or +1 if a > b. +// An invalid semver string is considered less than a valid one. +func Compare(a, b Version) int { + return semver.Compare(a.String(), b.String()) +} + +func valid(s string) error { + if len(s) > 0 && s[0] != 'v' { + return ErrVPrefix + } + if s == "" { + return ErrEmptyVersion + } + if !semver.IsValid(s) { + return fmt.Errorf("invalid version: %s", s) + } + return nil +} diff --git a/vendor/github.com/docker/secrets-engine/x/realms/docker.go b/vendor/github.com/docker/secrets-engine/x/realms/docker.go new file mode 100644 index 000000000000..7c93b234bf88 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/realms/docker.go @@ -0,0 +1,69 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package realms + +import "github.com/docker/secrets-engine/x/secrets" + +// Docker realms all start with `docker/` as the prefix. +// +// Authentication flows done by the Docker CLI, Docker Desktop and Docker related +// products must go through `docker/auth`. +// +// Docker Hub authentication (browser based OAuth login) will be prefixed +// with `docker/auth/hub/`. +// +// Docker Registry authentication will be prefixed with +// `docker/auth/registry/docker/`. +var ( + DockerHubAuthentication = secrets.MustParsePattern("docker/auth/hub/**") + DockerHubStagingAuthentication = secrets.MustParsePattern("docker/auth/hub-staging/**") + DockerRegistryAuthentication = secrets.MustParsePattern("docker/auth/registry/docker/**") + DockerRegistryStagingAuthentication = secrets.MustParsePattern("docker/auth/registry/docker-staging/**") +) + +var ( + // DockerHubAuthenticationMetadata holds one profile entry per signed-in + // Docker Hub account, plus a default entry duplicating the default + // account's profile. + DockerHubAuthenticationMetadata = secrets.MustParsePattern("docker/auth/metadata/hub/**") + // DockerHubStagingAuthenticationMetadata is the staging variant of + // [DockerHubAuthenticationMetadata]. + DockerHubStagingAuthenticationMetadata = secrets.MustParsePattern("docker/auth/metadata/hub-staging/**") +) + +var ( + // DockerMCPDefault is the default realm used for MCP related secrets + DockerMCPDefault = secrets.MustParsePattern("docker/mcp/**") + // DockerMCPOAuth is the realm used for all MCP OAuth credentials retrieved by Docker. + DockerMCPOAuth = secrets.MustParsePattern("docker/mcp/oauth/**") + // DockerMCPOAuthDCR is the realm used to hold Dynamic Client Registered (DCR) + // OAuth configurations for supported MCP servers. + DockerMCPOAuthDCR = secrets.MustParsePattern("docker/mcp/oauth-dcr/**") +) + +var ( + // DockerSandbox is the default realm used for Sandbox related secrets + DockerSandbox = secrets.MustParsePattern("docker/sandbox/**") + // DockerSandboxOAuth is the realm used for all Sandbox OAuth credentials + // such as third-party tokens - this does not store the Docker Auth tokens. + DockerSandboxOAuth = secrets.MustParsePattern("docker/sandbox/oauth/**") +) + +var ( + // SecretsEngine is the default realm used for secrets-engine related secrets. + SecretsEngine = secrets.MustParsePattern("docker/secrets-engine/**") + // SecretsEnginePlugins is the realm used to store credentials and configuration for secrets-engine plugins. + SecretsEnginePlugins = secrets.MustParsePattern("docker/secrets-engine/plugins/**") +) diff --git a/vendor/github.com/docker/secrets-engine/x/realms/realm.go b/vendor/github.com/docker/secrets-engine/x/realms/realm.go new file mode 100644 index 000000000000..e90c6d5f0874 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/realms/realm.go @@ -0,0 +1,27 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package realms keeps track of known Docker realms +// +// Realms do not define a permission model and should not be used as such! +// Realms are simply contracts that clients may use to query a set of secrets. +// Once a realm has been established it MUST not change as clients would treat +// the realm as a contract. +// Clients may pin themselves to a specific realm or a group of realms. +// +// Examples: +// +// `docker/` is a realm for all known Docker secrets. +// `docker/auth` is a realm for all known Docker Auth secrets. +package realms diff --git a/vendor/github.com/docker/secrets-engine/x/secrets/identifiers.go b/vendor/github.com/docker/secrets-engine/x/secrets/identifiers.go new file mode 100644 index 000000000000..0b79bec6b9b5 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/secrets/identifiers.go @@ -0,0 +1,176 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package secrets + +import ( + "fmt" + "strings" +) + +type ErrInvalidID struct { + ID string +} + +func (e ErrInvalidID) Error() string { + return fmt.Sprintf("invalid identifier: %q must match ^[A-Za-z0-9._:-]+(?:/[A-Za-z0-9._:-]+)*$", e.ID) +} + +// validIdentifier checks if an identifier is valid without using regexp or unicode. +// Rules: +// - Components separated by '/' +// - Each component is non-empty +// - Only characters A-Z, a-z, 0-9, '.', '_', '-' or ':' +// - No leading, trailing, or double slashes +func validIdentifier(s string) bool { + if len(s) == 0 { + return false + } + + componentLen := 0 + + for _, r := range s { + switch { + case r == '/': + if componentLen == 0 { + // Empty component (leading, trailing, or double slash) + return false + } + componentLen = 0 + case isValidRune(r): + componentLen++ + default: + return false + } + } + + // Final component must not be empty + return componentLen > 0 +} + +func isValidRune(c rune) bool { + return (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '.' || c == '-' || c == '_' || c == ':' +} + +func split(s string) []string { + s = strings.Trim(s, "/") + if s == "" { + return []string{} + } + return strings.Split(s, "/") +} + +func match(pattern, path []string) bool { + pi, si := 0, 0 + for pi < len(pattern) && si < len(path) { + switch pattern[pi] { + case "**": + // Try to consume any number of path segments + if pi+1 == len(pattern) { + return true // trailing ** matches rest + } + // Try to find a match for the rest of the pattern + for skip := 0; si+skip <= len(path); skip++ { + if match(pattern[pi+1:], path[si+skip:]) { + return true + } + } + return false + case "*": + // Match exactly one path component + pi++ + si++ + default: + if pattern[pi] != path[si] { + return false + } + pi++ + si++ + } + } + + // Handle trailing pattern parts (like **) + for pi < len(pattern) && pattern[pi] == "**" { + pi++ + } + + return pi == len(pattern) && si == len(path) +} + +func valid(id string) error { + if !validIdentifier(id) { + return ErrInvalidID{ + ID: id, + } + } + + return nil +} + +// ID contains a secret identifier. +// Valid secret identifiers must match the format ^[A-Za-z0-9._:-]+(?:/[A-Za-z0-9._:-]+)*$. +// +// For storage, we don't really differentiate much about the ID format but +// by convention we do simple, slash-separated management, providing a +// groupable access control system for management across plugins. +type ID interface { + // String formats the [ID] as a string + String() string + // Match the [ID] against a [Pattern] + // It checks if a given identifier matches the pattern. + // - "*" matches a single component + // - "**" matches zero or more components + // - "/" is the separator + Match(pattern Pattern) bool +} + +type id string + +func (i id) Match(pattern Pattern) bool { + pathParts := split(string(i)) + patternParts := split(pattern.String()) + + return match(patternParts, pathParts) +} + +func (i id) String() string { + return string(i) +} + +// ParseID creates a new [ID] from a string +// If a validation error occurs, it returns nil and the error. +// Rules: +// - Components separated by '/' +// - Each component is non-empty +// - Only characters A-Z, a-z, 0-9, '.', '_', '-' or ':' +// - No leading, trailing, or double slashes +func ParseID(s string) (ID, error) { + if err := valid(s); err != nil { + return nil, err + } + + return id(s), nil +} + +// MustParseID parses a string into a [ID] and behaves similar to +// [ParseID], however, it panics when the id is invalid +func MustParseID(s string) ID { + if err := valid(s); err != nil { + panic(err) + } + return id(s) +} diff --git a/vendor/github.com/docker/secrets-engine/x/secrets/minimize.go b/vendor/github.com/docker/secrets-engine/x/secrets/minimize.go new file mode 100644 index 000000000000..53dcbdabd152 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/secrets/minimize.go @@ -0,0 +1,128 @@ +// Copyright 2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package secrets + +import "strings" + +func canonicalKey(toks []token) string { + var sb strings.Builder + for i, t := range toks { + if i > 0 { + sb.WriteByte('/') + } + switch t.kind { + case tokenStar: + sb.WriteByte('*') + case tokenGap: + sb.WriteString("**") + case tokenLit: + sb.WriteString(t.lit) + } + } + return sb.String() +} + +// Minimize removes each pattern that another contains (see +// [Pattern.Contains]); when patterns match the same IDs, only the first +// stays. The result preserves input order. +// +// Examples: +// +// [** a/*] -> [**] +// [**/* */** **] -> [**/*] +// [a/** **/a] -> [a/** **/a] +// +// Complexity: O(n²·L²) for n patterns of at most L components each. +func Minimize(patterns []Pattern) []Pattern { + entries := make([]entry, 0, len(patterns)) + seen := make(map[string]struct{}, len(patterns)) + for _, p := range patterns { + toks := canonicalize(p.String()) + key := canonicalKey(toks) + if _, dup := seen[key]; dup { + continue + } + seen[key] = struct{}{} + entries = append(entries, newEntry(p, toks)) + } + + result := make([]Pattern, 0, len(entries)) + for i := range entries { + redundant := false + for j := range entries { + if i == j || !subsumes(&entries[j], &entries[i]) { + continue + } + if j < i || !covers(entries[i].toks, entries[j].toks) { + redundant = true + break + } + } + if !redundant { + result = append(result, entries[i].original) + } + } + return result +} + +type entry struct { + original Pattern + toks []token + minLen int + hasWild bool + hasGaps bool + firstLit string + lastLit string +} + +func newEntry(p Pattern, toks []token) entry { + e := entry{original: p, toks: toks} + for _, t := range toks { + if t.kind != tokenLit { + e.hasWild = true + } + if t.kind == tokenGap { + e.hasGaps = true + } else { + e.minLen++ + } + } + if len(toks) > 0 { + e.firstLit = toks[0].lit + e.lastLit = toks[len(toks)-1].lit + } + return e +} + +// subsumes reports whether other matches every ID e matches. It compares +// the precomputed fields first, so most pairs skip the O(L²) covers call. +func subsumes(other, e *entry) bool { + if !other.hasWild { + return false + } + if other.minLen > e.minLen { + return false + } + if !other.hasGaps && (e.hasGaps || other.minLen != e.minLen) { + return false + } + if other.firstLit != "" && other.firstLit != e.firstLit { + return false + } + if other.lastLit != "" && other.lastLit != e.lastLit { + return false + } + return covers(other.toks, e.toks) +} diff --git a/vendor/github.com/docker/secrets-engine/x/secrets/pattern.go b/vendor/github.com/docker/secrets-engine/x/secrets/pattern.go new file mode 100644 index 000000000000..eb4917349ad9 --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/secrets/pattern.go @@ -0,0 +1,295 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package secrets + +import ( + "errors" + "fmt" + "strings" +) + +var ErrInvalidPattern = errors.New("invalid pattern") + +// validPattern reports whether s is a valid pattern: non-empty '/'-separated +// components of A-Z, a-z, 0-9, '.', '-', '_', ':', where a component may +// instead be '*' or '**'. +func validPattern(s string) bool { + if len(s) == 0 { + return false + } + + componentLen := 0 + wildcardLen := 0 + + for _, r := range s { + switch { + case r == '/': + if !isValidComponentMatcher(componentLen, wildcardLen) { + return false + } + componentLen = 0 + wildcardLen = 0 + case isValidPatternRune(r): + componentLen++ + if r == '*' { + wildcardLen++ + } + default: + return false + } + } + // Final component + return isValidComponentMatcher(componentLen, wildcardLen) +} + +func isValidComponentMatcher(componentLen, wildcardLen int) bool { + if wildcardLen > 2 { + // No more than two wildcards per component + return false + } + if wildcardLen > 0 && wildcardLen != componentLen { + // Wildcard can't be mixed with other characters in the same component + return false + } + // Component must not be empty + return componentLen > 0 +} + +func isValidPatternRune(c rune) bool { + return isValidRune(c) || c == '*' +} + +// Pattern matches secret IDs. It follows the ID validation rules, except +// that '*' matches one component and '**' matches zero or more. +// Below, matches(p) denotes the set of IDs a pattern p matches. +type Pattern interface { + // Match reports whether the pattern matches id. + // Complexity: O(n*m^k), where id has n components and the pattern has + // m components and k occurrences of '**'. + Match(id ID) bool + // Contains reports whether every ID that [other] matches can also be + // matched by the pattern: the set of IDs [other] matches is contained + // in the set the pattern matches, i.e., matches(other) ⊆ matches(p). + // + // Examples: + // - docker/** contains docker/*/mcp/*: '**' is more general than + // '*/mcp/*'. + // - docker/proj1/** does not contain docker/*/mcp/*: docker/*/mcp/* + // matches docker/proj2/mcp/x, but docker/proj1/** does not. + // + // Complexity: O(n*m) + Contains(other Pattern) bool + // Overlaps reports whether the pattern and [other] match at least one + // ID in common, i.e., matches(p) ∩ matches(other) ≠ ∅. + // + // Examples: + // - docker/*/mcp/* and docker/proj1/** overlap: both match e.g. docker/proj1/mcp/x. + // - bar/** and foo/** do not overlap: an ID cannot begin with both bar and foo. + // + // Complexity: O(n*m) + Overlaps(other Pattern) bool + // String returns the pattern text. + String() string + + ExpandID(other ID) (ID, error) + ExpandPattern(other Pattern) (Pattern, error) +} + +type pattern string + +func (p pattern) Match(id ID) bool { + pathParts := split(id.String()) + patternParts := split(string(p)) + + return match(patternParts, pathParts) +} + +func (p pattern) Contains(other Pattern) bool { + return covers(canonicalize(string(p)), canonicalize(other.String())) +} + +func (p pattern) Overlaps(other Pattern) bool { + return compatible(canonicalize(string(p)), canonicalize(other.String())) +} + +type tokenKind uint8 + +const ( + tokenLit tokenKind = iota + tokenStar + tokenGap +) + +type token struct { + kind tokenKind + lit string +} + +// canonicalize tokenizes a pattern so that two patterns match the same set +// of IDs exactly when their canonical tokens are equal. Two rewrites give +// that property: +// +// - In a run of consecutive wildcard components, the '*' move to the +// front and the '**' merge into one at the end: order within a run +// does not affect what it matches, and one '**' already matches any +// number of components. +// - A pattern consisting only of '**' becomes "*/**": an ID always has +// at least one component, so both match the same IDs, and the first +// rewrite already turns the equivalent spelling "**/*" into "*/**". +// +// Examples: +// +// a/a/** and a/a/**/** -> [a a **] +// a/**/*/**/b -> [a * ** b] +// ** and **/* -> [* **] +func canonicalize(s string) []token { + parts := split(s) + toks := make([]token, 0, len(parts)) + stars, gap := 0, false + flush := func() { + for range stars { + toks = append(toks, token{kind: tokenStar}) + } + if gap { + toks = append(toks, token{kind: tokenGap}) + } + stars, gap = 0, false + } + for _, part := range parts { + switch part { + case "*": + stars++ + case "**": + gap = true + default: + flush() + toks = append(toks, token{kind: tokenLit, lit: part}) + } + } + flush() + if len(toks) == 1 && toks[0].kind == tokenGap { + toks = []token{{kind: tokenStar}, {kind: tokenGap}} + } + return toks +} + +// covers reports whether p matches every ID q matches, for canonical tokens. +// The component alphabet is unbounded, so a literal in p never covers an '*' +// or '**' in q. +func covers(p, q []token) bool { + np, nq := len(p), len(q) + prev := make([]bool, nq+1) + cur := make([]bool, nq+1) + prev[nq] = true + for i := np - 1; i >= 0; i-- { + cur[nq] = p[i].kind == tokenGap && prev[nq] + for j := nq - 1; j >= 0; j-- { + switch { + case p[i].kind == tokenGap: + cur[j] = prev[j] || cur[j+1] + case q[j].kind == tokenGap: + cur[j] = p[i].kind == tokenStar && cur[j+1] && prev[j] + case p[i].kind == tokenStar: + cur[j] = prev[j+1] + default: + cur[j] = q[j].kind == tokenLit && p[i].lit == q[j].lit && prev[j+1] + } + } + prev, cur = cur, prev + } + return prev[0] +} + +// compatible reports whether p and q match at least one ID in common, for +// canonical tokens. A common ID aligns both token lists over its +// components: wildcards accept anything, so only differing literals or +// mismatched lengths (* vs */foo) rule one out. +func compatible(p, q []token) bool { + np, nq := len(p), len(q) + prev := make([]bool, nq+1) + cur := make([]bool, nq+1) + prev[nq] = true + for j := nq - 1; j >= 0; j-- { + prev[j] = q[j].kind == tokenGap && prev[j+1] + } + for i := np - 1; i >= 0; i-- { + cur[nq] = p[i].kind == tokenGap && prev[nq] + for j := nq - 1; j >= 0; j-- { + switch { + case p[i].kind == tokenGap || q[j].kind == tokenGap: + cur[j] = prev[j] || cur[j+1] + case p[i].kind == tokenLit && q[j].kind == tokenLit && p[i].lit != q[j].lit: + cur[j] = false + default: + cur[j] = prev[j+1] + } + } + prev, cur = cur, prev + } + return prev[0] +} + +func (p pattern) String() string { + return string(p) +} + +// ParsePattern parses s into a [Pattern]: non-empty '/'-separated components +// of A-Z, a-z, 0-9, '.', '-', '_', ':', where a component may instead be '*' +// or '**'. It returns [ErrInvalidPattern] for anything else. +func ParsePattern(s string) (Pattern, error) { + if !validPattern(s) { + return nil, ErrInvalidPattern + } + return pattern(s), nil +} + +// MustParsePattern is like [ParsePattern] but panics on an invalid pattern. +func MustParsePattern(s string) Pattern { + if !validPattern(s) { + panic(ErrInvalidPattern) + } + return pattern(s) +} + +func (p pattern) ExpandID(other ID) (ID, error) { + val, err := replace1(string(p), other.String()) + if err != nil { + return nil, err + } + return ParseID(val) +} + +func (p pattern) ExpandPattern(other Pattern) (Pattern, error) { + val, err := replace1(string(p), other.String()) + if err != nil { + return nil, err + } + return pattern(val), err +} + +func replace1(original, other string) (string, error) { + components := split(original) + var candidates []int + for idx, val := range components { + if val == "**" { + candidates = append(candidates, idx) + } + } + if len(candidates) != 1 { + return "", fmt.Errorf("expand only supports one expansion glob, pattern %s has %d", original, len(candidates)) + } + components[candidates[0]] = other + return strings.Join(components, "/"), nil +} diff --git a/vendor/github.com/docker/secrets-engine/x/secrets/secrets.go b/vendor/github.com/docker/secrets-engine/x/secrets/secrets.go new file mode 100644 index 000000000000..95e113e7b6bb --- /dev/null +++ b/vendor/github.com/docker/secrets-engine/x/secrets/secrets.go @@ -0,0 +1,60 @@ +// Copyright 2025-2026 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package secrets + +import ( + "context" + "encoding/json" + "errors" + "time" +) + +var ( + ErrNotFound = errors.New("secret not found") + ErrAccessDenied = errors.New("access denied") // nuh, uh, uh! + ErrEmptySecret = errors.New("secret value must not be empty") +) + +type Envelope struct { + ID ID `json:"-"` + Value []byte `json:"-"` + Metadata map[string]string `json:"-"` + Provider string `json:"-"` + Version string `json:"-"` + CreatedAt time.Time `json:"-"` + ResolvedAt time.Time `json:"-"` + ExpiresAt time.Time `json:"-"` +} + +var _ json.Marshaler = Envelope{} + +func (e Envelope) MarshalJSON() ([]byte, error) { + panic("secrets.Envelope does not support json.Marshal") +} + +type Resolver interface { + GetSecrets(ctx context.Context, pattern Pattern) ([]Envelope, error) +} + +type AuthorizeResponse struct { + // Expiry is when the decision stops holding. A zero Expiry means the + // decision never expires, and the wire message then carries no timestamp. + Expiry time.Time + Allow bool +} + +type Authorizer interface { + Authorize(ctx context.Context, pattern ...Pattern) (AuthorizeResponse, error) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 622cc56e974c..797fc23e8d8e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,3 +1,7 @@ +# connectrpc.com/connect v1.19.1 +## explicit; go 1.24.0 +connectrpc.com/connect +connectrpc.com/connect/internal/gen/connectext/grpc/status/v1 # dario.cat/mergo v1.0.2 ## explicit; go 1.13 dario.cat/mergo @@ -87,6 +91,23 @@ github.com/docker/go-metrics github.com/docker/go-units # github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 ## explicit +# github.com/docker/secrets-engine/client v0.2.2 +## explicit; go 1.26.10 +github.com/docker/secrets-engine/client +github.com/docker/secrets-engine/client/dockerhub +github.com/docker/secrets-engine/client/realms +# github.com/docker/secrets-engine/x v0.8.5 +## explicit; go 1.26.10 +github.com/docker/secrets-engine/x/api +github.com/docker/secrets-engine/x/api/health/v1 +github.com/docker/secrets-engine/x/api/health/v1/healthv1connect +github.com/docker/secrets-engine/x/api/plugins/v1 +github.com/docker/secrets-engine/x/api/plugins/v1/pluginsv1connect +github.com/docker/secrets-engine/x/api/resolver +github.com/docker/secrets-engine/x/api/resolver/v1 +github.com/docker/secrets-engine/x/api/resolver/v1/resolverv1connect +github.com/docker/secrets-engine/x/realms +github.com/docker/secrets-engine/x/secrets # github.com/felixge/httpsnoop v1.1.0 ## explicit; go 1.25 github.com/felixge/httpsnoop