package requester import ( "cmp" "context" "errors" "fmt" "strings" "time" "github.com/bluesky-social/indigo/atproto/identity" commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1" "github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1" "github.com/johnandersen777/socialweb-computer-kcp/internal/bus" "github.com/johnandersen777/socialweb-computer-kcp/internal/component" "github.com/johnandersen777/socialweb-computer-kcp/internal/computevm" "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/atprotorepo" contractmarket "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/market" "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/pds" "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/policy" "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/vault" "github.com/johnandersen777/socialweb-computer-kcp/internal/guestidentity" "github.com/johnandersen777/socialweb-computer-kcp/internal/market/participant" role "github.com/johnandersen777/socialweb-computer-kcp/internal/market/requester" ) func egressFor(spec v1alpha1.RequesterSpec, deps Deps, trust *component.Trust) component.Egress { egress := deps.Egress egress.Log = cmp.Or(egress.Log, deps.Log) if trust.Enabled() { egress.TLS = trust.Client(nil) } if spec.Network == nil { return egress } if hosts := participant.NetworkHosts(*spec.Network, egress.Hosts); hosts != nil { egress.Hosts = hosts } return egress } func directoryFor(raw string, keys int, ttl time.Duration) identity.Directory { base := identity.DefaultDirectory() raw = strings.TrimRight(raw, "/") if raw == "" || raw == strings.TrimRight(identity.DefaultPLCURL, "/") { return base } cached, ok := base.(*identity.CacheDirectory) if !ok { return base } inner := cached.Inner if directory, ok := inner.(*identity.BaseDirectory); ok { inner = &identity.BaseDirectory{ PLCURL: raw, PLCLimiter: directory.PLCLimiter, DIDWebLimitFunc: directory.DIDWebLimitFunc, HTTPClient: directory.HTTPClient, PLCClient: directory.PLCClient, TryAuthoritativeDNS: directory.TryAuthoritativeDNS, SkipDNSDomainSuffixes: append([]string(nil), directory.SkipDNSDomainSuffixes...), FallbackDNSServers: append([]string(nil), directory.FallbackDNSServers...), SkipHandleVerification: directory.SkipHandleVerification, UserAgent: directory.UserAgent, } } return identity.NewCacheDirectory(inner, keys, ttl, ttl, ttl) } func (s *Service) self() commonv1.ObjectRef { return commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: Kind, Name: s.deps.Self.Name, Workspace: s.cfg.Workspace} } func (s *Service) setup(ctx context.Context) (*role.Requester, error) { if s.deps.Bus == nil { return nil, fmt.Errorf("requester: the bus is not available") } self := s.self() intents, err := bus.Resolve[contractmarket.Intents](ctx, s.deps.Bus, self, self, bus.Capability{Name: contractmarket.CapabilityIntents}) if err != nil { return nil, fmt.Errorf("requester: the market intents handle: %w", err) } inputs := participant.NewInputs() if _, err := intents.Subscribe(ctx, contractmarket.Subscription{ Kinds: []string{contractmarket.KindExposure, contractmarket.KindMarketIdentity}, Observer: inputs}); err != nil { return nil, fmt.Errorf("requester: subscribing to the exposures: %w", err) } if extra := role.IdentityWorkspaces(s.cfg.Workspace)[1:]; len(extra) > 0 && s.identityRef() { if _, err := intents.Subscribe(ctx, contractmarket.Subscription{ Kinds: []string{contractmarket.KindMarketIdentity}, Workspaces: extra, Observer: inputs}); err != nil { return nil, fmt.Errorf("requester: subscribing to the market identity: %w", err) } } events, err := bus.Resolve[contractmarket.ContractEvents](ctx, s.deps.Bus, self, self, bus.Capability{Name: contractmarket.CapabilityContractEvents}) if err != nil { return nil, fmt.Errorf("requester: the contract events handle: %w", err) } handlers := s.deps.Bus runner := func(ctx context.Context, engine commonv1.ObjectRef, workflow string) (policy.Runner, error) { return bus.Resolve[policy.Runner](ctx, handlers, self, engine, bus.Capability{Name: policy.CapabilityRun, Scope: []string{workflow}}) } core, err := role.New(s.cfg, role.Deps{ Intents: intents, Inputs: inputs, OpenRepo: s.openRepo, AwaitRepo: s.awaitRepo, WatchBinds: s.watchBinds, Policy: runner, Vault: s.secretsVault, VaultWrite: s.secretsVaultWrite, ServiceAuth: s.openServiceAuth, Guests: guests{}, ContractEvents: events, Ingress: s.ingress, Directory: s.directory, HTTP: s.client, Now: time.Now, Log: s.deps.Log, }) if err != nil { return nil, err } if err := core.Start(ctx); err != nil { return core, err } s.mu.Lock() s.core = core s.mu.Unlock() if _, err := intents.Subscribe(ctx, contractmarket.Subscription{ Kinds: []string{contractmarket.KindComputeContract, contractmarket.KindStateSync}, Observer: core, Synced: core.Synced}); err != nil { return core, fmt.Errorf("requester: subscribing to the contracts: %w", err) } if _, err := events.SubscribeContractEvents(ctx, core); err != nil { return core, fmt.Errorf("requester: subscribing to the contract events: %w", err) } if relays := s.cfg.Relays(); len(relays) > 0 { crawl := participant.Crawl{Relays: relays, Address: func() string { return inputs.Address(role.PDSExposure) }, Period: participant.CrawlPeriod, Log: s.log} s.wg.Go(func() { crawl.Run(ctx) }) } return core, nil } func (s *Service) identityRef() bool { identity := s.spec.Identity return identity != nil && identity.MarketIdentityRef != nil && identity.MarketIdentityRef.Name != "" } func (s *Service) repoRef(did string) commonv1.ObjectRef { return commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: "AtprotoRepo", Name: atprotorepo.RepoName(did), Workspace: s.cfg.Workspace} } func (s *Service) openRepo(ctx context.Context, did string) (atprotorepo.Repo, error) { return bus.Resolve[atprotorepo.Repo](ctx, s.deps.Bus, s.self(), s.repoRef(did), bus.Capability{Name: atprotorepo.CapabilityWrite}) } func (s *Service) watchBinds() (<-chan struct{}, func()) { wake := make(chan struct{}, 1) if s.deps.Bus == nil { return wake, func() {} } cancel := s.deps.Bus.Subscribe(func(key bus.Key) { switch key.Kind { case "Vault", "MarketIdentity", "AtprotoRepo": default: return } select { case wake <- struct{}{}: default: } }) return wake, cancel } func (s *Service) awaitRepo(ctx context.Context, did string) { repo := s.repoRef(did) bound := make(chan struct{}, 1) cancel := s.deps.Bus.Subscribe(func(key bus.Key) { if key.Workspace != repo.Workspace || key.Name != repo.Name { return } select { case bound <- struct{}{}: default: } }) defer cancel() waited := false for { _, err := s.openRepo(ctx, did) if !errors.Is(err, atprotorepo.ErrNotBound) && !errors.Is(err, pds.ErrNotFound) { if waited { s.log.Info("the requester repo is bound; the requester runs its contracts and releases", "repo", repo.Name) } return } if !waited { s.log.Info("the requester waits for its repo binding before it runs contracts or releases", "repo", repo.Name, "err", err.Error()) waited = true } select { case <-ctx.Done(): return case <-bound: } } } func (s *Service) openServiceAuth(ctx context.Context, identity commonv1.ObjectRef, did string) (pds.ServiceAuth, error) { identity.Group, identity.Kind = cmp.Or(identity.Group, v1alpha1.GroupName), cmp.Or(identity.Kind, "MarketIdentity") return bus.Resolve[pds.ServiceAuth](ctx, s.deps.Bus, s.self(), identity, bus.Capability{Name: pds.CapabilityServiceAuth, Scope: []string{did}}) } func (s *Service) secretsVault(ctx context.Context, ref commonv1.SecretRef) (vault.KV, error) { return s.openSecretsVault(ctx, ref, vault.CapabilityRead) } func (s *Service) secretsVaultWrite(ctx context.Context, ref commonv1.SecretRef) (vault.KV, error) { return s.openSecretsVault(ctx, ref, vault.CapabilityWrite) } func (s *Service) openSecretsVault(ctx context.Context, ref commonv1.SecretRef, capability string) (vault.KV, error) { vaultRef := s.spec.VaultRef if vaultRef == nil || vaultRef.Name == "" { return nil, fmt.Errorf("requester: the contract names a secrets bundle and the component names no OpenBaoServer") } target := commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: "Vault", Name: vaultRef.Name, Workspace: cmp.Or(vaultRef.Workspace, s.deps.Self.Workspace)} kv, err := bus.Resolve[vault.KV](s.deps.Caller(ctx), s.deps.Bus, s.deps.Self, target, bus.Capability{Name: capability, Scope: []string{ref.Mount + "/" + ref.Path}}) if err != nil { return nil, fmt.Errorf("requester: the vault client: %w", err) } return kv, nil } type guests struct{} var _ role.Guests = guests{} func (g guests) Subject(bidderDID, requesterDID, roleName string) (string, error) { return guestidentity.SubjectFor(bidderDID, requesterDID, roleName) } func (g guests) Verifier(issuer string, keys func(ctx context.Context) ([]byte, error)) role.TokenVerifier { return guestidentity.NewVerifier(keys, func() string { return issuer }) } func (g guests) CheckHostKeys(keys []v1alpha1.ComputeVMHostKey) ([]v1alpha1.ComputeVMHostKey, error) { return computevm.CheckHostKeys(keys) }