~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
222 lines · 7.1 KB Raw History
package kcpclient

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync"

	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/rest"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/clusterhome"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime/kube"
)

const (
	NodeClaimAgentGrant  = "socialweb-nodeclaim-agent"
	NodeClaimHomeLabel   = "socialweb.computer/nodeclaim-home"
	NodeClaimHomeKey     = "workspace"
	foreignAccountPrefix = "system:kcp:serviceaccount:"
	agentFieldManager    = "kcpclient"
)

type ServiceAccountRef struct {
	Workspace string
	Namespace string
	Name      string
}

func ParseServiceAccountRef(s string) (*ServiceAccountRef, error) {
	if s == "" {
		return nil, nil
	}
	parts := strings.Split(s, "/")
	if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" {
		return nil, fmt.Errorf("kcpclient: %q is not <workspace path>/<namespace>/<name>", s)
	}
	return &ServiceAccountRef{Workspace: parts[0], Namespace: parts[1], Name: parts[2]}, nil
}

func HomeAgentRules() []rbacv1.PolicyRule {
	return []rbacv1.PolicyRule{
		{NonResourceURLs: []string{"/"}, Verbs: []string{"access"}},
		{APIGroups: []string{socialwebResource.Group}, Resources: []string{"nodeclaims"}, Verbs: []string{"get", "list", "patch"}},
		{APIGroups: []string{socialwebResource.Group}, Resources: []string{"nodeclaims/status"}, Verbs: []string{"patch"}},
		{APIGroups: []string{socialwebResource.Group}, Resources: []string{"clusterlinks"}, Verbs: []string{"get"}},
	}
}

type agent struct {
	ref  *ServiceAccountRef
	mu   sync.Mutex
	user string
}

func (a *agent) userName(ctx context.Context, c *Client) (string, error) {
	a.mu.Lock()
	defer a.mu.Unlock()
	if a.user != "" {
		return a.user, nil
	}
	i := strings.LastIndex(a.ref.Workspace, ":")
	if i < 0 {
		return "", fmt.Errorf("kcpclient: the node claim agent's workspace %q has no parent", a.ref.Workspace)
	}
	ws, err := c.readWorkspace(ctx, a.ref.Workspace[:i], a.ref.Workspace[i+1:])
	if err != nil {
		return "", fmt.Errorf("kcpclient: the node claim agent's workspace %s: %w", a.ref.Workspace, err)
	}
	if ws.Spec.Cluster == "" {
		return "", fmt.Errorf("kcpclient: the node claim agent's workspace %s has no logical cluster yet", a.ref.Workspace)
	}
	a.user = foreignAccountPrefix + ws.Spec.Cluster + ":" + a.ref.Namespace + ":" + a.ref.Name
	return a.user, nil
}

func apply(ctx context.Context, req *rest.Request, where string, obj any) error {
	if err := applyDoc(ctx, req, agentFieldManager, true, obj, false); err != nil {
		return fmt.Errorf("kcpclient: the node claim agent's grant in %s: %w", where, err)
	}
	return nil
}

func applyDoc(ctx context.Context, req *rest.Request, manager string, force bool, obj any, status bool) error {
	doc, err := kube.ApplyBody(obj, status)
	if err != nil {
		return err
	}
	body, err := json.Marshal(doc)
	if err != nil {
		return err
	}
	req = req.Param("fieldManager", manager)
	if force {
		req = req.Param("force", "true")
	}
	return req.Body(body).Do(ctx).Error()
}

func (c *Client) grantAgentHome(ctx context.Context, home string) error {
	if c.agent == nil {
		return nil
	}
	user, err := c.agent.userName(ctx, c)
	if err != nil {
		return err
	}
	r, err := c.bind(home, rbacv1.SchemeGroupVersion, "/apis")
	if err != nil {
		return err
	}
	role := rbacv1.ClusterRole{TypeMeta: metav1.TypeMeta{APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: "ClusterRole"},
		ObjectMeta: metav1.ObjectMeta{Name: NodeClaimAgentGrant}, Rules: HomeAgentRules()}
	if err := apply(ctx, r.Patch(types.ApplyPatchType).Resource("clusterroles").Name(NodeClaimAgentGrant), home, role); err != nil {
		return err
	}
	binding := rbacv1.ClusterRoleBinding{TypeMeta: metav1.TypeMeta{APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: "ClusterRoleBinding"},
		ObjectMeta: metav1.ObjectMeta{Name: NodeClaimAgentGrant},
		RoleRef:    rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: NodeClaimAgentGrant},
		Subjects:   []rbacv1.Subject{{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: user}}}
	if err := apply(ctx, r.Patch(types.ApplyPatchType).Resource("clusterrolebindings").Name(NodeClaimAgentGrant), home, binding); err != nil {
		return err
	}
	key, err := clusterhome.Key(home)
	if err != nil {
		return err
	}
	core, err := c.coreResource(c.agent.ref.Workspace)
	if err != nil {
		return err
	}
	index := corev1.ConfigMap{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"},
		ObjectMeta: metav1.ObjectMeta{Name: key, Namespace: c.agent.ref.Namespace, Labels: map[string]string{NodeClaimHomeLabel: "true"}},
		Data:       map[string]string{NodeClaimHomeKey: home}}
	return apply(ctx, core.Patch(types.ApplyPatchType).Namespace(c.agent.ref.Namespace).Resource("configmaps").Name(key),
		c.agent.ref.Workspace, index)
}

func (c *Client) GrantAgentHome(ctx context.Context, home string) error {
	return c.grantAgentHome(ctx, home)
}

func (c *Client) DropAgentHome(ctx context.Context, home string) (bool, error) {
	if c == nil || c.agent == nil {
		return false, nil
	}
	key, err := clusterhome.Key(home)
	if err != nil {
		return false, err
	}
	i := strings.LastIndex(home, ":")
	_, err = c.readWorkspace(ctx, home[:i], home[i+1:])
	switch {
	case apierrors.IsNotFound(err):
	case err != nil:
		return false, fmt.Errorf("kcpclient: the home workspace %s: %w", home, err)
	default:
		cl, err := c.readCluster(ctx, home, home[i+1:])
		if err == nil && cl.DeletionTimestamp == nil {
			return false, nil
		}
		if err != nil && !apierrors.IsNotFound(err) {
			return false, fmt.Errorf("kcpclient: the cluster of %s: %w", home, err)
		}
		claims, err := c.ListNodeClaims(ctx, home)
		if err != nil && !apierrors.IsNotFound(err) {
			return false, err
		}
		if len(claims) > 0 {
			return false, nil
		}
	}
	core, err := c.coreResource(c.agent.ref.Workspace)
	if err != nil {
		return false, err
	}
	err = core.Delete().Namespace(c.agent.ref.Namespace).Resource("configmaps").Name(key).Do(ctx).Error()
	if err != nil && !apierrors.IsNotFound(err) {
		return false, fmt.Errorf("kcpclient: the node claim index entry %s in %s: %w", key, c.agent.ref.Workspace, err)
	}
	return true, nil
}

func (c *Client) SweepAgentHomes(ctx context.Context, live map[string]bool) ([]string, error) {
	if c == nil || c.agent == nil {
		return nil, nil
	}
	core, err := c.coreResource(c.agent.ref.Workspace)
	if err != nil {
		return nil, err
	}
	raw, err := core.Get().Namespace(c.agent.ref.Namespace).Resource("configmaps").
		Param("labelSelector", NodeClaimHomeLabel+"=true").DoRaw(ctx)
	if err != nil {
		return nil, fmt.Errorf("kcpclient: list the node claim index in %s: %w", c.agent.ref.Workspace, err)
	}
	var list corev1.ConfigMapList
	if err := json.Unmarshal(raw, &list); err != nil {
		return nil, err
	}
	var dropped []string
	var errs []error
	for _, cm := range list.Items {
		if live[cm.Name] {
			continue
		}
		ok, err := c.DropAgentHome(ctx, cm.Data[NodeClaimHomeKey])
		if err != nil {
			errs = append(errs, fmt.Errorf("%s: %w", cm.Name, err))
		}
		if ok {
			dropped = append(dropped, cm.Name)
		}
	}
	return dropped, errors.Join(errs...)
}