//go:build !slim package cli import ( "context" "database/sql" "encoding/base64" "errors" "io" "net/url" "time" "golang.org/x/xerrors" "tailscale.com/derp" "tailscale.com/types/key" agplcli "github.com/coder/coder/v2/cli" agplcoderd "github.com/coder/coder/v2/coderd" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/cryptorand" "github.com/coder/coder/v2/enterprise/audit" "github.com/coder/coder/v2/enterprise/audit/backends" "github.com/coder/coder/v2/enterprise/coderd" "github.com/coder/coder/v2/enterprise/coderd/dormancy" "github.com/coder/coder/v2/enterprise/coderd/usage" "github.com/coder/coder/v2/enterprise/dbcrypt" "github.com/coder/coder/v2/enterprise/trialer" "github.com/coder/coder/v2/tailnet" "github.com/coder/quartz" "github.com/coder/serpent" ) func (r *RootCmd) Server(_ func()) *serpent.Command { cmd := r.RootCmd.Server(func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, io.Closer, error) { if options.DeploymentValues.DERP.Server.RelayURL.String() != "" { _, err := url.Parse(options.DeploymentValues.DERP.Server.RelayURL.String()) if err != nil { return nil, nil, xerrors.Errorf("derp-server-relay-address must be a valid HTTP URL: %w", err) } } // Always generate a mesh key, even if the built-in DERP server is // disabled. This mesh key is still used by workspace proxies running // HA. var meshKey string err := options.Database.InTx(func(tx database.Store) error { // This will block until the lock is acquired, and will be // automatically released when the transaction ends. err := tx.AcquireLock(ctx, database.LockIDEnterpriseDeploymentSetup) if err != nil { return xerrors.Errorf("acquire lock: %w", err) } meshKey, err = tx.GetDERPMeshKey(ctx) if err == nil { return nil } if !errors.Is(err, sql.ErrNoRows) { return xerrors.Errorf("get DERP mesh key: %w", err) } meshKey, err = cryptorand.String(32) if err != nil { return xerrors.Errorf("generate DERP mesh key: %w", err) } err = tx.InsertDERPMeshKey(ctx, meshKey) if err != nil { return xerrors.Errorf("insert DERP mesh key: %w", err) } return nil }, nil) if err != nil { return nil, nil, err } if meshKey == "" { return nil, nil, xerrors.New("mesh key is empty") } if options.DeploymentValues.DERP.Server.Enable { options.DERPServer = derp.NewServer(key.NewNode(), tailnet.Logger(options.Logger.Named("derp"))) options.DERPServer.SetMeshKey(meshKey) } options.Auditor = audit.NewAuditor( options.Database, audit.DefaultFilter, backends.NewPostgres(options.Database, true), backends.NewSlog(options.Logger), ) options.TrialGenerator = trialer.New(options.Database, "https://v2-licensor.coder.com/trial", coderd.Keys) o := &coderd.Options{ Options: options, AuditLogging: true, ConnectionLogging: true, BrowserOnly: options.DeploymentValues.BrowserOnly.Value(), SCIMAPIKey: []byte(options.DeploymentValues.SCIMAPIKey.Value()), RBAC: true, DERPServerRelayAddress: options.DeploymentValues.DERP.Server.RelayURL.String(), DERPServerRegionID: int(options.DeploymentValues.DERP.Server.RegionID.Value()), ProxyHealthInterval: options.DeploymentValues.ProxyHealthStatusInterval.Value(), DefaultQuietHoursSchedule: options.DeploymentValues.UserQuietHoursSchedule.DefaultSchedule.Value(), ProvisionerDaemonPSK: options.DeploymentValues.Provisioner.DaemonPSK.Value(), CheckInactiveUsersCancelFunc: dormancy.CheckInactiveUsers(ctx, options.Logger, quartz.NewReal(), options.Database, options.Auditor), } if encKeys := options.DeploymentValues.ExternalTokenEncryptionKeys.Value(); len(encKeys) != 0 { keys := make([][]byte, 0, len(encKeys)) for idx, ek := range encKeys { dk, err := base64.StdEncoding.DecodeString(ek) if err != nil { return nil, nil, xerrors.Errorf("decode external-token-encryption-key %d: %w", idx, err) } keys = append(keys, dk) } cs, err := dbcrypt.NewCiphers(keys...) if err != nil { return nil, nil, xerrors.Errorf("initialize encryption: %w", err) } o.ExternalTokenEncryption = cs } if o.LicenseKeys == nil { o.LicenseKeys = coderd.Keys } closers := &multiCloser{} // Create the enterprise API. api, err := coderd.New(ctx, o) if err != nil { return nil, nil, err } closers.Add(api) // Start the enterprise usage publisher routine. This won't do anything // unless the deployment is licensed and one of the licenses has usage // publishing enabled. publisher := usage.NewTallymanPublisher(ctx, options.Logger, options.Database, o.LicenseKeys, usage.PublisherWithHTTPClient(api.HTTPClient), ) err = publisher.Start() if err != nil { _ = closers.Close() return nil, nil, xerrors.Errorf("start usage publisher: %w", err) } closers.Add(publisher) // usageCron are heartbeat events to the usage table. These events are eventually sent // to Tallyman. usageCron := usage.NewCron(quartz.NewReal(), options.Logger.Named("usage-cron"), options.Database, *options.UsageInserter.Load()) // ai-seats heartbeats track the number of users that have used an AI feature. // These users consume a seat for the AI addon to our License. _ = usageCron.Register(usage.CronJob{ Name: "ai-seats", Interval: usage.AISeatsInterval, Jitter: 10 * time.Minute, Fn: usage.AISeatsHeartbeat(options.Database), }) usageCron.Start(ctx) closers.Add(usageCron) // In-memory AI Bridge Proxy daemon. The bridge daemon itself is // started unconditionally by AGPL cli/server.go (chatd uses its // in-memory roundtripper regardless of license); only the proxy // daemon remains enterprise-gated by config. if options.DeploymentValues.AI.BridgeProxyConfig.Enabled.Value() { providers, err := agplcli.BuildProviders(ctx, options.Database, options.DeploymentValues.AI.BridgeConfig, options.Logger.Named("aibridge.providers")) if err != nil { return nil, nil, xerrors.Errorf("build AI providers: %w", err) } aiBridgeProxyServer, err := newAIBridgeProxyDaemon(api, providers) if err != nil { _ = closers.Close() return nil, nil, xerrors.Errorf("create aibridgeproxyd: %w", err) } closers.Add(aiBridgeProxyServer) // Register the handler so coderd can serve the proxy endpoints. api.RegisterInMemoryAIBridgeProxydHTTPHandler(aiBridgeProxyServer.Handler()) } return api.AGPL, closers, nil }) cmd.AddSubcommands( r.dbcryptCmd(), ) return cmd } type multiCloser struct { closers []io.Closer } var _ io.Closer = &multiCloser{} func (m *multiCloser) Add(closer io.Closer) { m.closers = append(m.closers, closer) } func (m *multiCloser) Close() error { var errs []error for _, closer := range m.closers { if err := closer.Close(); err != nil { errs = append(errs, xerrors.Errorf("close %T: %w", closer, err)) } } return errors.Join(errs...) }