~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
114 lines · 3.2 KB Raw History
package kineetcd

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	clientv3 "go.etcd.io/etcd/client/v3"
	"go.uber.org/zap"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/tenantstore"
)

const DefaultTimeout = 15 * time.Second

const registryPrefix = "/registry/"

type Config struct {
	Timeout time.Duration
}

type Store struct {
	timeout time.Duration
}

var _ tenantstore.Eraser = (*Store)(nil)

func New(cfg Config) *Store {
	if cfg.Timeout <= 0 {
		cfg.Timeout = DefaultTimeout
	}
	return &Store{timeout: cfg.Timeout}
}

func ServiceAccountKey(sa tenantstore.ServiceAccount) string {
	return registryPrefix + "serviceaccounts/" + sa.Namespace + "/" + sa.Name
}

func TokenSecretPrefix(sa tenantstore.ServiceAccount) string {
	return registryPrefix + "secrets/" + sa.Namespace + "/" + sa.Name + "-token-"
}

func (s *Store) EraseServiceAccount(ctx context.Context, endpoint string, sa tenantstore.ServiceAccount) (tenantstore.Erased, error) {
	if endpoint == "" {
		return tenantstore.Erased{}, errors.New("kineetcd: no datastore endpoint to erase the ServiceAccount from")
	}
	if sa.Namespace == "" || sa.Name == "" || strings.Contains(sa.Namespace+sa.Name, "/") {
		return tenantstore.Erased{}, fmt.Errorf("kineetcd: %q/%q does not name one ServiceAccount", sa.Namespace, sa.Name)
	}
	ctx, cancel := context.WithTimeout(ctx, s.timeout)
	defer cancel()
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{endpoint},
		DialTimeout: s.timeout,
		Context:     ctx,
		Logger:      zap.NewNop(),
	})
	if err != nil {
		return tenantstore.Erased{}, fmt.Errorf("kineetcd: dial the datastore at %s: %w", endpoint, err)
	}
	defer cli.Close()

	var out tenantstore.Erased
	prefix := TokenSecretPrefix(sa)
	secrets, err := cli.Get(ctx, prefix, clientv3.WithPrefix(), clientv3.WithKeysOnly())
	if err != nil {
		return out, fmt.Errorf("kineetcd: list the token Secrets of %s/%s at %s: %w", sa.Namespace, sa.Name, endpoint, err)
	}
	for _, kv := range secrets.Kvs {
		erased, err := erase(ctx, cli, string(kv.Key), kv.ModRevision)
		if err != nil {
			return out, err
		}
		if erased {
			out.Secrets = append(out.Secrets, strings.TrimPrefix(string(kv.Key), registryPrefix+"secrets/"+sa.Namespace+"/"))
		}
	}

	key := ServiceAccountKey(sa)
	account, err := cli.Get(ctx, key, clientv3.WithKeysOnly())
	if err != nil {
		return out, fmt.Errorf("kineetcd: read %s at %s: %w", key, endpoint, err)
	}
	for _, kv := range account.Kvs {
		erased, err := erase(ctx, cli, string(kv.Key), kv.ModRevision)
		if err != nil {
			return out, err
		}
		out.ServiceAccount = out.ServiceAccount || erased
	}
	return out, nil
}

func erase(ctx context.Context, cli *clientv3.Client, key string, rev int64) (bool, error) {
	resp, err := cli.Txn(ctx).
		If(clientv3.Compare(clientv3.ModRevision(key), "=", rev)).
		Then(clientv3.OpDelete(key)).
		Else(clientv3.OpGet(key, clientv3.WithKeysOnly())).
		Commit()
	if err != nil {
		return false, fmt.Errorf("kineetcd: delete %s at revision %d: %w", key, rev, err)
	}
	if resp.Succeeded {
		return true, nil
	}
	for _, r := range resp.Responses {
		if rng := r.GetResponseRange(); rng != nil && len(rng.Kvs) == 0 {
			return false, nil
		}
	}
	return false, fmt.Errorf("kineetcd: %s changed after revision %d while it was being deleted", key, rev)
}