~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
191 lines · 6.2 KB Raw History
package marketnetwork

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/app/compose"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/bus"
	contractvault "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/vault"
)

const (
	NetworkDir         = "network"
	NetworkVaultServer = "network"
	NetworkVaultClient = "components"
	NetworkPKIMount    = "pki"
	NetworkOwnerMount  = "owner"
)

func NetworkStateDir(instanceState string) string { return filepath.Join(instanceState, NetworkDir) }

func (ctl *Controller) vaultReady(ctx context.Context, ws string) error {
	o, err := compose.Get[v1alpha1.OpenBaoServer](ctx, ctl.c, ws, compose.OpenBaoServerGVR, NetworkVaultServer)
	if err != nil {
		return err
	}
	st := o.Status
	if st.Phase != v1alpha1.OpenBaoServerPhaseReady || st.Address == "" || st.Refs == nil {
		return fmt.Errorf("the OpenBaoServer %s is %s, not Ready", NetworkVaultServer, st.Phase)
	}
	for _, x := range st.Clients {
		if x.Name == NetworkVaultClient && x.ClientCert != "" {
			return nil
		}
	}
	return fmt.Errorf("the OpenBaoServer %s reports no client %s yet", NetworkVaultServer, NetworkVaultClient)
}

func vaultLinks(r compose.Root) []compose.Link {
	return append([]compose.Link{compose.VaultPKILink(r.Ref(), NetworkVaultServer, NetworkPKIMount)}, verifierLinks(r)...)
}

func (ctl *Controller) pkiCA(ctx context.Context, r compose.Root) ([]byte, error) {
	pki, err := bus.Resolve[contractvault.PKI](ctx, ctl.c.Bus(), r.Ref(), compose.VaultRefIn(NetworkVaultServer, r.Workspace),
		bus.Capability{Name: contractvault.CapabilityPKIIssue, Scope: []string{NetworkPKIMount}})
	if err != nil {
		return nil, err
	}
	pem, err := pki.CA(ctx, NetworkPKIMount)
	if err != nil {
		return nil, fmt.Errorf("the network's PKI root CA: %w", err)
	}
	return []byte(pem), nil
}

func (ctl *Controller) caBundle(ctx context.Context, r compose.Root) (string, error) {
	if len(ctl.pkiPEM) == 0 {
		pem, err := ctl.pkiCA(ctx, r)
		if err != nil {
			return "", err
		}
		ctl.pkiPEM = pem
	}
	guest, err := os.ReadFile(filepath.Join(NetworkStateDir(ctl.c.StateRoot()), GuestTLSDir, guestCAFile))
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(string(ctl.pkiPEM)) + "\n" + strings.TrimSpace(string(guest)) + "\n", nil
}

func (ctl *Controller) oneKcp(ws string, apps []v1alpha1.NetworkAppStatus) []string {
	front := ctl.c.KcpServer()
	workspaces := map[string]string{ws: "the network"}
	var wrong []string
	for _, a := range apps {
		if a.KcpURL == "" {
			continue
		}
		if f, _, _ := strings.Cut(strings.TrimSuffix(a.KcpURL, "/"), "/clusters/"); f != front {
			wrong = append(wrong, fmt.Sprintf("%s reports the kcp %s, not the network's %s", a.Name, f, front))
		}
		if a.Workspace == "" {
			wrong = append(wrong, a.Name+" reports no workspace")
			continue
		}
		if other, ok := workspaces[a.Workspace]; ok {
			wrong = append(wrong, fmt.Sprintf("%s shares the workspace %s with %s", a.Name, a.Workspace, other))
		}
		workspaces[a.Workspace] = a.Name
	}
	return wrong
}

func (ctl *Controller) verifierTransport(req *http.Request) http.RoundTripper {
	handlers := ctl.c.Bus()
	if handlers == nil || req == nil || req.URL == nil {
		return nil
	}
	host := strings.ToLower(req.URL.Hostname())
	if host == "" {
		return nil
	}
	if _, ok := handlers.HandlerForHost(host); ok {
		return handlers.RoundTripper()
	}
	return nil
}

var verifierChecks = []struct{ kind, name, path string }{
	{compose.ComponentKindPlc, compose.NetworkPLCName, "/health"},
	{compose.ComponentKindRelay, compose.NetworkRelayName, "/xrpc/_health"},
	{compose.ComponentKindPds, NetworkAlicePdsName, "/xrpc/_health"},
	{compose.ComponentKindPds, NetworkBobPdsName, "/xrpc/_health"},
	{compose.ComponentKindIngressProxy, compose.NetworkIngressName, "/.well-known/did.json"},
}

func verifierLinks(r compose.Root) []compose.Link {
	out := make([]compose.Link, 0, len(verifierChecks))
	for _, check := range verifierChecks {
		out = append(out, compose.RefLink(r.Ref(), compose.AppRef(check.kind, check.name, r.Workspace), compose.Cap(bus.CapabilityXRPC)))
	}
	return out
}

func (ctl *Controller) verify(ctx context.Context, snap compose.Snapshot, apps []v1alpha1.NetworkAppStatus) error {
	for _, check := range verifierChecks {
		v, ok := snap.View(check.kind, check.name)
		if !ok {
			return fmt.Errorf("%s %s is not there", check.kind, check.name)
		}
		if v.Status.Phase == v1alpha1.ComponentPhaseSuspended {
			continue
		}
		if !v.Status.Ready {
			return fmt.Errorf("%s %s is not ready", check.kind, check.name)
		}
		e, ok := compose.ComponentEndpoint(v, "http")
		if !ok {
			return fmt.Errorf("%s %s reports no endpoint", check.kind, check.name)
		}
		base := compose.EndpointURL(e)
		if base == "" {
			return fmt.Errorf("%s %s reports no endpoint URL", check.kind, check.name)
		}
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(base, "/")+check.path, nil)
		if err != nil {
			return err
		}
		req.Header.Set("Accept", "application/json")
		if check.kind == compose.ComponentKindIngressProxy {
			host := GuestTLSHost
			if ctl.value != nil && ctl.value.IngressProxyHost != "" {
				if h, _, ok := strings.Cut(ctl.value.IngressProxyHost, ":"); ok && h != "" {
					host = h
				} else {
					host = ctl.value.IngressProxyHost
				}
			}
			req.Host = host
		}
		client := &http.Client{Timeout: VerifierProbeTimeout}
		if transport := ctl.verifierTransport(req); transport != nil {
			client.Transport = transport
		}
		resp, err := client.Do(req)
		if err != nil {
			return fmt.Errorf("%s %s at %s: %w", check.kind, check.name, check.path, err)
		}
		resp.Body.Close()
		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
			return fmt.Errorf("%s %s at %s returned HTTP %d", check.kind, check.name, check.path, resp.StatusCode)
		}
	}
	for _, a := range apps {
		if a.Kind == v1alpha1.ApplicationKindMarketRequester || a.Kind == v1alpha1.ApplicationKindSshGateway {
			continue
		}
		if a.Message == StoppedMessage || a.Phase == v1alpha1.ApplicationPhaseSuspended {
			continue
		}
		if a.Phase != v1alpha1.ApplicationPhaseReady {
			return fmt.Errorf("the child application %s is %s: %s", a.Name, a.Phase, a.Message)
		}
	}
	return nil
}