~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
252 lines · 6.1 KB Raw History
package requester

import (
	"cmp"
	"context"
	"errors"
	"log/slog"
	"net/http"
	"sync"
	"time"

	"github.com/bluesky-social/indigo/atproto/identity"
	"k8s.io/utils/ptr"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/component"
	sshcontract "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/ssh"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/tunnel"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/supervise"
	role "github.com/johnandersen777/socialweb-computer-kcp/internal/market/requester"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/oauthcallback"
)

const Kind = role.KindRequester

const requestTimeout = role.DefaultHTTPTimeout

type Deps struct {
	component.Instance

	Egress component.Egress

	OAuthCallbacks *oauthcallback.Registry

	OpenIngress func(handler http.Handler, options tunnel.ServiceOptions) (tunnel.Service, error)
}

type Service struct {
	spec v1alpha1.RequesterSpec

	deps Deps

	log *slog.Logger

	cfg role.Config

	client *http.Client

	trust *component.Trust

	directory identity.Directory

	mu sync.Mutex

	started bool

	ready bool

	readiness component.ReadySignal

	cancel context.CancelFunc

	done chan struct{}

	core *role.Requester

	ingress tunnel.Service

	wg sync.WaitGroup
}

var (
	_ component.Service  = (*Service)(nil)
	_ sshcontract.Runner = (*Service)(nil)
)

func New(ctx context.Context, spec v1alpha1.RequesterSpec, deps Deps) (*Service, error) {
	log := cmp.Or(deps.Log, slog.New(slog.DiscardHandler))
	s := &Service{spec: spec, deps: deps, log: log}
	placement := role.Placement{Workspace: deps.Self.Workspace, Name: deps.Self.Name, UID: string(deps.UID), StateDir: deps.StateDir}
	cfg, err := role.Configure(spec, placement)
	if err != nil {
		return nil, err
	}
	s.cfg = cfg
	s.trust = component.NewTrust(spec.TLS.CA(), log)
	s.client = egressFor(spec, deps, s.trust).Client(requestTimeout)
	s.directory = directoryFor(cfg.Network().PLCDirectoryURL, int(ptr.Deref(spec.DirectoryKeys, 0)), time.Duration(ptr.Deref(spec.DirectoryTTLSeconds, 0))*time.Second)
	s.log = log.With("component", "requester", "did", cfg.DID)
	return s, nil
}

func (s *Service) Start(ctx context.Context) error {
	s.mu.Lock()
	if s.started {
		s.mu.Unlock()
		return errors.New("requester: the service is already started")
	}
	s.started = true
	s.ready = false
	s.done = make(chan struct{})
	runCtx, cancel := context.WithCancel(ctx)
	s.cancel = cancel
	s.mu.Unlock()

	stopTrust, err := s.trust.Follow(runCtx, s.deps.Instance)
	if err != nil {
		cancel()
		s.finish(ctx, nil)
		return err
	}
	defer stopTrust()
	if err := s.register(); err != nil {
		cancel()
		s.finish(ctx, nil)
		return err
	}
	s.log.Info("requester_registered", "workspace", s.cfg.Workspace, "name", s.deps.Self.Name)
	if s.cfg.WebRTCIngress() {
		if _, err := s.openIngress(); err != nil {
			s.log.Warn("requester_webrtc_ingress_off", "err", err.Error())
		} else {
			s.log.Info("requester_webrtc_ingress", "id", tunnel.ID(s.cfg.RepoDID))
		}
	}
	core, err := s.setup(runCtx)
	if err != nil {
		cancel()
		s.finish(ctx, core)
		return err
	}
	s.mu.Lock()
	s.ready = true
	s.mu.Unlock()
	if core.Records() == nil {
		s.readiness.Fail(errors.New("requester: the repo is not open"))
	} else {
		s.readiness.Signal()
	}
	s.log.Info("requester_started", "workspace", s.cfg.Workspace, "name", s.deps.Self.Name, "repo", core.RepoDID(), "mode", core.Mode())
	<-runCtx.Done()
	s.finish(ctx, core)
	return nil
}

func (s *Service) register() error {
	if s.deps.Bus == nil {
		return errors.New("requester: the handler registry is not available")
	}
	return s.deps.Bus.RegisterHTTP(component.Key{Workspace: s.cfg.Workspace, Kind: Kind, Name: s.deps.Self.Name}, s.handler())
}

func (s *Service) openIngress() (tunnel.Service, error) {
	if s.deps.OpenIngress == nil {
		return nil, errors.New("requester: no WebRTC ingress factory is configured")
	}
	key := component.Key{Workspace: s.cfg.Workspace, Kind: Kind, Name: s.deps.Self.Name}
	mux := s.handler()
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		mux.ServeHTTP(w, r.WithContext(component.WithCaller(r.Context(), key)))
	})
	service, err := s.deps.OpenIngress(handler, s.cfg.WebRTCIngressOptions())
	if err != nil {
		return nil, err
	}
	s.mu.Lock()
	s.ingress = service
	s.mu.Unlock()
	return service, nil
}

func (s *Service) finish(ctx context.Context, core *role.Requester) {
	stopCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), component.DefaultStopTimeout)
	defer cancel()
	_ = supervise.Join(stopCtx, s.wg.Wait)
	if core != nil {
		core.Shutdown(stopCtx)
	}
	s.mu.Lock()
	s.started = false
	s.ready = false
	s.core = nil
	ingress := s.ingress
	s.ingress = nil
	done := s.done
	s.mu.Unlock()
	if ingress != nil {
		_ = ingress.Close()
	}
	close(done)
}

func (s *Service) role() *role.Requester {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.core
}

func (s *Service) healthy() *role.Requester {
	s.mu.Lock()
	defer s.mu.Unlock()
	if !s.ready || s.core == nil || s.core.Records() == nil {
		return nil
	}
	return s.core
}

func (s *Service) Ready() component.Readiness {
	return &s.readiness
}

func (s *Service) Endpoints() []component.Endpoint {
	s.mu.Lock()
	defer s.mu.Unlock()
	if !s.ready {
		return nil
	}
	endpoints := []component.Endpoint{{Name: "http", Protocol: component.ProtocolHTTP}}
	if s.cfg.WebRTCIngress() {
		if id := tunnel.ID(s.cfg.RepoDID); id != "" {
			endpoints = append(endpoints, component.Endpoint{Name: "webrtc", Protocol: "webrtc", Address: tunnel.Ref(id)})
		}
	}
	return endpoints
}

func (s *Service) Stop(ctx context.Context) error {
	s.mu.Lock()
	cancel, done := s.cancel, s.done
	s.mu.Unlock()
	if cancel == nil {
		return nil
	}
	cancel()
	if done == nil {
		return nil
	}
	select {
	case <-done:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func (s *Service) RunCommand(ctx context.Context, account sshcontract.Account, command string, env map[string]string, io sshcontract.CommandIO) error {
	core := s.role()
	if core == nil {
		return errors.New("requester: the service is not ready to run commands")
	}
	return core.RunCommand(ctx, account, command, env, io)
}