Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 47 additions & 10 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ var (
// anything.
ErrSilent = xerrors.New("silent error")

ErrClientURLNotConfigured = xerrors.New("client URL is not configured")
Comment thread
pawbana marked this conversation as resolved.

errKeyringNotSupported = xerrors.New("keyring storage is not supported on this operating system; omit --use-keyring to use file-based storage")
)

Expand Down Expand Up @@ -602,23 +604,58 @@ func (r *RootCmd) SetClock(clk quartz.Clock) {
// ensureClientURL loads the client URL from the config file if it
// wasn't provided via --url or CODER_URL.
func (r *RootCmd) ensureClientURL() error {
if r.clientURL != nil && r.clientURL.String() != "" {
return nil
}
rawURL, err := r.createConfig().URL().Read()
// If the configuration files are absent, the user is logged out.
if os.IsNotExist(err) {
binPath, err := os.Executable()
if err != nil {
u, err := r.resolveClientURL()

if errors.Is(err, ErrClientURLNotConfigured) {
binPath, execErr := os.Executable()
if execErr != nil {
binPath = "coder"
}
return xerrors.Errorf(notLoggedInMessage, binPath)
}

if err != nil {
return err
}
r.clientURL, err = url.Parse(strings.TrimSpace(rawURL))
return err

r.clientURL = u
return nil
}

func (r *RootCmd) resolveClientURL() (*url.URL, error) {
Comment thread
pawbana marked this conversation as resolved.
if r.clientURL != nil && r.clientURL.String() != "" {
return r.clientURL, nil
}

rawURL, err := r.createConfig().URL().Read()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrClientURLNotConfigured
}
return nil, xerrors.Errorf("read configured URL: %w", err)
}
parsedURL, err := url.Parse(strings.TrimSpace(rawURL))
if err != nil {
return nil, xerrors.Errorf("parse configured URL: %w", err)
}
return parsedURL, nil
}

// ResolveClientConnection resolves the deployment URL and client TLS transport
// without reading or requiring a user session.
func (r *RootCmd) ResolveClientConnection() (*url.URL, http.RoundTripper, error) {
serverURL, err := r.resolveClientURL()
if err != nil {
return nil, nil, err
}
if err := r.ensureTLSConfig(); err != nil {
return nil, nil, xerrors.Errorf("load client TLS config: %w", err)
}
transport, err := newHTTPTransport(r.tlsConfig)
if err != nil {
return nil, nil, xerrors.Errorf("create HTTP transport: %w", err)
}
return serverURL, transport, nil
}

// ensureTLSConfig loads the TLS configuration from files if specified.
Expand Down
143 changes: 143 additions & 0 deletions cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
Expand Down Expand Up @@ -105,6 +106,148 @@ func TestCommandHelp(t *testing.T) {
))
}

func TestResolveClientConnection(t *testing.T) {
t.Parallel()

run := func(t *testing.T, configure func(config.Root), args ...string) (string, http.RoundTripper, error, error) {
t.Helper()

var root cli.RootCmd
var gotURL string
var gotTransport http.RoundTripper
var gotErr error
cmd, err := root.Command([]*serpent.Command{{
Use: "resolve",
Handler: func(*serpent.Invocation) error {
serverURL, transport, err := root.ResolveClientConnection()
if serverURL != nil {
gotURL = serverURL.String()
}
gotTransport = transport
gotErr = err
return nil
},
}})
require.NoError(t, err)

inv, cfg := clitest.NewWithCommand(t, cmd, args...)
if configure != nil {
configure(cfg)
}
runErr := inv.Run()
return gotURL, gotTransport, gotErr, runErr
}

tests := []struct {
Comment thread
pawbana marked this conversation as resolved.
name string
args []string
configure func(*testing.T, config.Root)
wantURL string
wantTransport bool
wantErr string
wantRunErr string
checkTransport func(*testing.T, http.RoundTripper)
}{
{
name: "MissingURL",
args: []string{"resolve"},
wantErr: cli.ErrClientURLNotConfigured.Error(),
},
{
name: "URLFlag",
args: []string{"--url", "https://example.com", "resolve"},
wantURL: "https://example.com",
wantTransport: true,
},
{
name: "ConfiguredURL",
args: []string{"resolve"},
configure: func(t *testing.T, cfg config.Root) {
t.Helper()
require.NoError(t, cfg.URL().Write("https://configured.example.com"))
},
wantURL: "https://configured.example.com",
wantTransport: true,
},
{
name: "URLFlagOverridesConfig",
args: []string{"--url", "https://flag.example.com", "resolve"},
configure: func(t *testing.T, cfg config.Root) {
t.Helper()
require.NoError(t, cfg.URL().Write("https://configured.example.com"))
},
wantURL: "https://flag.example.com",
wantTransport: true,
},
{
name: "InvalidURLFlag",
args: []string{"--url", "%zz", "resolve"},
wantRunErr: "invalid URL escape",
},
{
name: "ClientTLSConfig",
args: func() []string {
certPath, keyPath := generateTLSCertificate(t)
return []string{
"--url", "https://example.com",
"--client-tls-cert-file", certPath,
"--client-tls-key-file", keyPath,
"resolve",
}
}(),
wantURL: "https://example.com",
wantTransport: true,
checkTransport: func(t *testing.T, transport http.RoundTripper) {
t.Helper()

httpTransport, ok := transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, httpTransport.TLSClientConfig)
require.Len(t, httpTransport.TLSClientConfig.Certificates, 1)
},
},
{
name: "TLSConfigError",
args: []string{
"--url", "https://example.com",
"--client-tls-cert-file", "/tmp/missing-cert.pem",
"resolve",
},
wantErr: "load client TLS config: --client-tls-cert-file and --client-tls-key-file must be specified together",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

var configure func(config.Root)
if tc.configure != nil {
configure = func(cfg config.Root) {
tc.configure(t, cfg)
}
}

serverURL, transport, err, runErr := run(t, configure, tc.args...)
if tc.wantRunErr != "" {
require.ErrorContains(t, runErr, tc.wantRunErr)
return
}
require.NoError(t, runErr)
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
require.Equal(t, tc.wantURL, serverURL)
require.Equal(t, tc.wantTransport, transport != nil)
if tc.checkTransport != nil {
tc.checkTransport(t, transport)
}
})
}
}

func TestRoot(t *testing.T) {
t.Parallel()
t.Run("MissingRootCommand", func(t *testing.T) {
Expand Down
67 changes: 56 additions & 11 deletions coderd/aibridged/aibridged.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import (
"github.com/coder/retry"
)

var _ io.Closer = &Server{}
var (
_ io.Closer = &Server{}

ErrShutdown = xerrors.New("aibridged server shutdown")
)

// Server provides the AI Bridge functionality.
// It is responsible for:
Expand All @@ -42,10 +46,11 @@ type Server struct {
initConnectionCh chan struct{}
initConnectionOnce sync.Once

// lifecycleCtx is canceled when we start closing.
// lifecycleCtx is canceled when we start closing or when the
// connection loop exits permanently.
lifecycleCtx context.Context
// cancelFn closes the lifecycleCtx.
cancelFn func()
// cancelFn closes the lifecycleCtx with the reason it closed.
cancelFn context.CancelCauseFunc

shutdownOnce sync.Once
}
Expand All @@ -55,7 +60,7 @@ func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger,
return nil, xerrors.Errorf("nil rpcDialer given")
}

ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancelCause(ctx)
daemon := &Server{
logger: logger,
tracer: tracer,
Expand All @@ -78,6 +83,11 @@ func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger,
func (s *Server) connect() {
defer s.logger.Debug(s.lifecycleCtx, "connect loop exited")
defer s.wg.Done()
defer func() {
if s.lifecycleCtx.Err() == nil {
s.cancelFn(xerrors.New("connect loop exited"))
}
}()

logConnect := s.logger.With(slog.F("context", "aibridged.server")).Debug
// An exponential back-off occurs when the connection is failing to dial.
Expand All @@ -93,13 +103,25 @@ connectLoop:
client, err := s.clientDialer(s.lifecycleCtx)
if err != nil {
if errors.Is(err, context.Canceled) {
if s.lifecycleCtx.Err() == nil {
s.cancelFn(err)
}
return
}
var sdkErr *codersdk.Error
// If something is wrong with our auth, stop trying to connect.
if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusForbidden {
s.logger.Error(s.lifecycleCtx, "not authorized to dial coderd", slog.Error(err))
return
// If something is wrong with configuration, stop trying to connect.
if errors.As(err, &sdkErr) {
switch sdkErr.StatusCode() {
Comment thread
pawbana marked this conversation as resolved.
// These statuses are terminal failures from the /api/v2/ai-gateway/serve
// handshake: wrong gateway key, incompatible API version, or entitlement failure.
case http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden:
err = xerrors.Errorf("dial coderd: %w", err)
s.logger.Error(s.lifecycleCtx, "fatal error dialing coderd", slog.Error(err))
s.cancelFn(err)
return
default:
err = xerrors.Errorf("unexpected HTTP response dialing coderd: %w", err)
}
}
if s.isShutdown() {
return
Expand Down Expand Up @@ -133,9 +155,32 @@ connectLoop:
}
}

// Done returns a channel that is closed when the server lifecycle ends.
// It closes on explicit shutdown and on fatal connection-loop exit.
func (s *Server) Done() <-chan struct{} {
return s.lifecycleCtx.Done()
}

// Err returns the reason the server lifecycle ended.
func (s *Server) Err() error {
if cause := context.Cause(s.lifecycleCtx); cause != nil {
return cause
}
return s.lifecycleCtx.Err()
}

func (s *Server) Client() (DRPCClient, error) {
return s.ClientContext(context.Background())
}

func (s *Server) ClientContext(ctx context.Context) (DRPCClient, error) {
select {
case <-s.lifecycleCtx.Done():
case <-ctx.Done():
return nil, ctx.Err()
case <-s.Done():
if err := s.Err(); err != nil {
return nil, err
}
return nil, xerrors.New("context closed")
case client := <-s.clientCh:
return client, nil
Expand Down Expand Up @@ -170,7 +215,7 @@ func (s *Server) isShutdown() bool {
func (s *Server) Shutdown(ctx context.Context) error {
var err error
s.shutdownOnce.Do(func() {
s.cancelFn()
s.cancelFn(ErrShutdown)

// Wait for any outstanding connections to terminate.
s.wg.Wait()
Expand Down
Loading
Loading