feat: add single tailnet support to pgcoord (#9351)

This commit is contained in:
Colin Adler
2023-09-21 15:30:48 -04:00
committed by GitHub
parent fbad06f406
commit c900b5f8df
24 changed files with 1647 additions and 293 deletions

View File

@ -694,6 +694,13 @@ func (q *querier) DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) e
return q.db.DeleteAPIKeysByUserID(ctx, userID)
}
func (q *querier) DeleteAllTailnetClientSubscriptions(ctx context.Context, arg database.DeleteAllTailnetClientSubscriptionsParams) error {
if err := q.authorizeContext(ctx, rbac.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
return err
}
return q.db.DeleteAllTailnetClientSubscriptions(ctx, arg)
}
func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
// TODO: This is not 100% correct because it omits apikey IDs.
err := q.authorizeContext(ctx, rbac.ActionDelete,
@ -783,6 +790,13 @@ func (q *querier) DeleteTailnetClient(ctx context.Context, arg database.DeleteTa
return q.db.DeleteTailnetClient(ctx, arg)
}
func (q *querier) DeleteTailnetClientSubscription(ctx context.Context, arg database.DeleteTailnetClientSubscriptionParams) error {
if err := q.authorizeContext(ctx, rbac.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
return err
}
return q.db.DeleteTailnetClientSubscription(ctx, arg)
}
func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
}
@ -825,9 +839,9 @@ func (q *querier) GetAllTailnetAgents(ctx context.Context) ([]database.TailnetAg
return q.db.GetAllTailnetAgents(ctx)
}
func (q *querier) GetAllTailnetClients(ctx context.Context) ([]database.TailnetClient, error) {
func (q *querier) GetAllTailnetClients(ctx context.Context) ([]database.GetAllTailnetClientsRow, error) {
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTailnetCoordinator); err != nil {
return []database.TailnetClient{}, err
return []database.GetAllTailnetClientsRow{}, err
}
return q.db.GetAllTailnetClients(ctx)
}
@ -2794,6 +2808,13 @@ func (q *querier) UpsertTailnetClient(ctx context.Context, arg database.UpsertTa
return q.db.UpsertTailnetClient(ctx, arg)
}
func (q *querier) UpsertTailnetClientSubscription(ctx context.Context, arg database.UpsertTailnetClientSubscriptionParams) error {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTailnetCoordinator); err != nil {
return err
}
return q.db.UpsertTailnetClientSubscription(ctx, arg)
}
func (q *querier) UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (database.TailnetCoordinator, error) {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTailnetCoordinator); err != nil {
return database.TailnetCoordinator{}, err

View File

@ -854,6 +854,15 @@ func (q *FakeQuerier) DeleteAPIKeysByUserID(_ context.Context, userID uuid.UUID)
return nil
}
func (*FakeQuerier) DeleteAllTailnetClientSubscriptions(_ context.Context, arg database.DeleteAllTailnetClientSubscriptionsParams) error {
err := validateDatabaseType(arg)
if err != nil {
return err
}
return ErrUnimplemented
}
func (q *FakeQuerier) DeleteApplicationConnectAPIKeysByUserID(_ context.Context, userID uuid.UUID) error {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -987,6 +996,10 @@ func (*FakeQuerier) DeleteTailnetClient(context.Context, database.DeleteTailnetC
return database.DeleteTailnetClientRow{}, ErrUnimplemented
}
func (*FakeQuerier) DeleteTailnetClientSubscription(context.Context, database.DeleteTailnetClientSubscriptionParams) error {
return ErrUnimplemented
}
func (q *FakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIKey, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -1102,7 +1115,7 @@ func (*FakeQuerier) GetAllTailnetAgents(_ context.Context) ([]database.TailnetAg
return nil, ErrUnimplemented
}
func (*FakeQuerier) GetAllTailnetClients(_ context.Context) ([]database.TailnetClient, error) {
func (*FakeQuerier) GetAllTailnetClients(_ context.Context) ([]database.GetAllTailnetClientsRow, error) {
return nil, ErrUnimplemented
}
@ -6112,6 +6125,10 @@ func (*FakeQuerier) UpsertTailnetClient(context.Context, database.UpsertTailnetC
return database.TailnetClient{}, ErrUnimplemented
}
func (*FakeQuerier) UpsertTailnetClientSubscription(context.Context, database.UpsertTailnetClientSubscriptionParams) error {
return ErrUnimplemented
}
func (*FakeQuerier) UpsertTailnetCoordinator(context.Context, uuid.UUID) (database.TailnetCoordinator, error) {
return database.TailnetCoordinator{}, ErrUnimplemented
}

View File

@ -128,6 +128,13 @@ func (m metricsStore) DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUI
return err
}
func (m metricsStore) DeleteAllTailnetClientSubscriptions(ctx context.Context, arg database.DeleteAllTailnetClientSubscriptionsParams) error {
start := time.Now()
r0 := m.s.DeleteAllTailnetClientSubscriptions(ctx, arg)
m.queryLatencies.WithLabelValues("DeleteAllTailnetClientSubscriptions").Observe(time.Since(start).Seconds())
return r0
}
func (m metricsStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
start := time.Now()
err := m.s.DeleteApplicationConnectAPIKeysByUserID(ctx, userID)
@ -209,6 +216,13 @@ func (m metricsStore) DeleteTailnetClient(ctx context.Context, arg database.Dele
return m.s.DeleteTailnetClient(ctx, arg)
}
func (m metricsStore) DeleteTailnetClientSubscription(ctx context.Context, arg database.DeleteTailnetClientSubscriptionParams) error {
start := time.Now()
r0 := m.s.DeleteTailnetClientSubscription(ctx, arg)
m.queryLatencies.WithLabelValues("DeleteTailnetClientSubscription").Observe(time.Since(start).Seconds())
return r0
}
func (m metricsStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
start := time.Now()
apiKey, err := m.s.GetAPIKeyByID(ctx, id)
@ -265,7 +279,7 @@ func (m metricsStore) GetAllTailnetAgents(ctx context.Context) ([]database.Tailn
return r0, r1
}
func (m metricsStore) GetAllTailnetClients(ctx context.Context) ([]database.TailnetClient, error) {
func (m metricsStore) GetAllTailnetClients(ctx context.Context) ([]database.GetAllTailnetClientsRow, error) {
start := time.Now()
r0, r1 := m.s.GetAllTailnetClients(ctx)
m.queryLatencies.WithLabelValues("GetAllTailnetClients").Observe(time.Since(start).Seconds())
@ -1752,6 +1766,13 @@ func (m metricsStore) UpsertTailnetClient(ctx context.Context, arg database.Upse
return m.s.UpsertTailnetClient(ctx, arg)
}
func (m metricsStore) UpsertTailnetClientSubscription(ctx context.Context, arg database.UpsertTailnetClientSubscriptionParams) error {
start := time.Now()
r0 := m.s.UpsertTailnetClientSubscription(ctx, arg)
m.queryLatencies.WithLabelValues("UpsertTailnetClientSubscription").Observe(time.Since(start).Seconds())
return r0
}
func (m metricsStore) UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (database.TailnetCoordinator, error) {
start := time.Now()
defer m.queryLatencies.WithLabelValues("UpsertTailnetCoordinator").Observe(time.Since(start).Seconds())

View File

@ -139,6 +139,20 @@ func (mr *MockStoreMockRecorder) DeleteAPIKeysByUserID(arg0, arg1 interface{}) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).DeleteAPIKeysByUserID), arg0, arg1)
}
// DeleteAllTailnetClientSubscriptions mocks base method.
func (m *MockStore) DeleteAllTailnetClientSubscriptions(arg0 context.Context, arg1 database.DeleteAllTailnetClientSubscriptionsParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAllTailnetClientSubscriptions", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAllTailnetClientSubscriptions indicates an expected call of DeleteAllTailnetClientSubscriptions.
func (mr *MockStoreMockRecorder) DeleteAllTailnetClientSubscriptions(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllTailnetClientSubscriptions", reflect.TypeOf((*MockStore)(nil).DeleteAllTailnetClientSubscriptions), arg0, arg1)
}
// DeleteApplicationConnectAPIKeysByUserID mocks base method.
func (m *MockStore) DeleteApplicationConnectAPIKeysByUserID(arg0 context.Context, arg1 uuid.UUID) error {
m.ctrl.T.Helper()
@ -310,6 +324,20 @@ func (mr *MockStoreMockRecorder) DeleteTailnetClient(arg0, arg1 interface{}) *go
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetClient", reflect.TypeOf((*MockStore)(nil).DeleteTailnetClient), arg0, arg1)
}
// DeleteTailnetClientSubscription mocks base method.
func (m *MockStore) DeleteTailnetClientSubscription(arg0 context.Context, arg1 database.DeleteTailnetClientSubscriptionParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteTailnetClientSubscription", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteTailnetClientSubscription indicates an expected call of DeleteTailnetClientSubscription.
func (mr *MockStoreMockRecorder) DeleteTailnetClientSubscription(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetClientSubscription", reflect.TypeOf((*MockStore)(nil).DeleteTailnetClientSubscription), arg0, arg1)
}
// GetAPIKeyByID mocks base method.
func (m *MockStore) GetAPIKeyByID(arg0 context.Context, arg1 string) (database.APIKey, error) {
m.ctrl.T.Helper()
@ -431,10 +459,10 @@ func (mr *MockStoreMockRecorder) GetAllTailnetAgents(arg0 interface{}) *gomock.C
}
// GetAllTailnetClients mocks base method.
func (m *MockStore) GetAllTailnetClients(arg0 context.Context) ([]database.TailnetClient, error) {
func (m *MockStore) GetAllTailnetClients(arg0 context.Context) ([]database.GetAllTailnetClientsRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAllTailnetClients", arg0)
ret0, _ := ret[0].([]database.TailnetClient)
ret0, _ := ret[0].([]database.GetAllTailnetClientsRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -3681,6 +3709,20 @@ func (mr *MockStoreMockRecorder) UpsertTailnetClient(arg0, arg1 interface{}) *go
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetClient", reflect.TypeOf((*MockStore)(nil).UpsertTailnetClient), arg0, arg1)
}
// UpsertTailnetClientSubscription mocks base method.
func (m *MockStore) UpsertTailnetClientSubscription(arg0 context.Context, arg1 database.UpsertTailnetClientSubscriptionParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpsertTailnetClientSubscription", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpsertTailnetClientSubscription indicates an expected call of UpsertTailnetClientSubscription.
func (mr *MockStoreMockRecorder) UpsertTailnetClientSubscription(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetClientSubscription", reflect.TypeOf((*MockStore)(nil).UpsertTailnetClientSubscription), arg0, arg1)
}
// UpsertTailnetCoordinator mocks base method.
func (m *MockStore) UpsertTailnetCoordinator(arg0 context.Context, arg1 uuid.UUID) (database.TailnetCoordinator, error) {
m.ctrl.T.Helper()

View File

@ -219,13 +219,57 @@ $$;
CREATE FUNCTION tailnet_notify_client_change() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
var_client_id uuid;
var_coordinator_id uuid;
var_agent_ids uuid[];
var_agent_id uuid;
BEGIN
IF (OLD IS NOT NULL) THEN
PERFORM pg_notify('tailnet_client_update', OLD.id || ',' || OLD.agent_id);
RETURN NULL;
IF (NEW.id IS NOT NULL) THEN
var_client_id = NEW.id;
var_coordinator_id = NEW.coordinator_id;
ELSIF (OLD.id IS NOT NULL) THEN
var_client_id = OLD.id;
var_coordinator_id = OLD.coordinator_id;
END IF;
-- Read all agents the client is subscribed to, so we can notify them.
SELECT
array_agg(agent_id)
INTO
var_agent_ids
FROM
tailnet_client_subscriptions subs
WHERE
subs.client_id = NEW.id AND
subs.coordinator_id = NEW.coordinator_id;
-- No agents to notify
if (var_agent_ids IS NULL) THEN
return NULL;
END IF;
-- pg_notify is limited to 8k bytes, which is approximately 221 UUIDs.
-- Instead of sending all agent ids in a single update, send one for each
-- agent id to prevent overflow.
FOREACH var_agent_id IN ARRAY var_agent_ids
LOOP
PERFORM pg_notify('tailnet_client_update', var_client_id || ',' || var_agent_id);
END LOOP;
return NULL;
END;
$$;
CREATE FUNCTION tailnet_notify_client_subscription_change() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF (NEW IS NOT NULL) THEN
PERFORM pg_notify('tailnet_client_update', NEW.id || ',' || NEW.agent_id);
PERFORM pg_notify('tailnet_client_update', NEW.client_id || ',' || NEW.agent_id);
RETURN NULL;
ELSIF (OLD IS NOT NULL) THEN
PERFORM pg_notify('tailnet_client_update', OLD.client_id || ',' || OLD.agent_id);
RETURN NULL;
END IF;
END;
@ -495,10 +539,16 @@ CREATE TABLE tailnet_agents (
node jsonb NOT NULL
);
CREATE TABLE tailnet_client_subscriptions (
client_id uuid NOT NULL,
coordinator_id uuid NOT NULL,
agent_id uuid NOT NULL,
updated_at timestamp with time zone NOT NULL
);
CREATE TABLE tailnet_clients (
id uuid NOT NULL,
coordinator_id uuid NOT NULL,
agent_id uuid NOT NULL,
updated_at timestamp with time zone NOT NULL,
node jsonb NOT NULL
);
@ -1144,6 +1194,9 @@ ALTER TABLE ONLY site_configs
ALTER TABLE ONLY tailnet_agents
ADD CONSTRAINT tailnet_agents_pkey PRIMARY KEY (id, coordinator_id);
ALTER TABLE ONLY tailnet_client_subscriptions
ADD CONSTRAINT tailnet_client_subscriptions_pkey PRIMARY KEY (client_id, coordinator_id, agent_id);
ALTER TABLE ONLY tailnet_clients
ADD CONSTRAINT tailnet_clients_pkey PRIMARY KEY (id, coordinator_id);
@ -1248,8 +1301,6 @@ CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lo
CREATE INDEX idx_tailnet_agents_coordinator ON tailnet_agents USING btree (coordinator_id);
CREATE INDEX idx_tailnet_clients_agent ON tailnet_clients USING btree (agent_id);
CREATE INDEX idx_tailnet_clients_coordinator ON tailnet_clients USING btree (coordinator_id);
CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false);
@ -1284,6 +1335,8 @@ CREATE TRIGGER tailnet_notify_agent_change AFTER INSERT OR DELETE OR UPDATE ON t
CREATE TRIGGER tailnet_notify_client_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_clients FOR EACH ROW EXECUTE FUNCTION tailnet_notify_client_change();
CREATE TRIGGER tailnet_notify_client_subscription_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_client_subscriptions FOR EACH ROW EXECUTE FUNCTION tailnet_notify_client_subscription_change();
CREATE TRIGGER tailnet_notify_coordinator_heartbeat AFTER INSERT OR UPDATE ON tailnet_coordinators FOR EACH ROW EXECUTE FUNCTION tailnet_notify_coordinator_heartbeat();
CREATE TRIGGER trigger_insert_apikeys BEFORE INSERT ON api_keys FOR EACH ROW EXECUTE FUNCTION insert_apikey_fail_if_user_deleted();
@ -1329,6 +1382,9 @@ ALTER TABLE ONLY provisioner_jobs
ALTER TABLE ONLY tailnet_agents
ADD CONSTRAINT tailnet_agents_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE;
ALTER TABLE ONLY tailnet_client_subscriptions
ADD CONSTRAINT tailnet_client_subscriptions_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE;
ALTER TABLE ONLY tailnet_clients
ADD CONSTRAINT tailnet_clients_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE;

View File

@ -19,6 +19,7 @@ const (
ForeignKeyProvisionerJobLogsJobID ForeignKeyConstraint = "provisioner_job_logs_job_id_fkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
ForeignKeyProvisionerJobsOrganizationID ForeignKeyConstraint = "provisioner_jobs_organization_id_fkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ForeignKeyTailnetAgentsCoordinatorID ForeignKeyConstraint = "tailnet_agents_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE;
ForeignKeyTailnetClientSubscriptionsCoordinatorID ForeignKeyConstraint = "tailnet_client_subscriptions_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE;
ForeignKeyTailnetClientsCoordinatorID ForeignKeyConstraint = "tailnet_clients_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_clients ADD CONSTRAINT tailnet_clients_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE;
ForeignKeyTemplateVersionParametersTemplateVersionID ForeignKeyConstraint = "template_version_parameters_template_version_id_fkey" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE;
ForeignKeyTemplateVersionVariablesTemplateVersionID ForeignKeyConstraint = "template_version_variables_template_version_id_fkey" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE;

View File

@ -0,0 +1,39 @@
BEGIN;
ALTER TABLE
tailnet_clients
ADD COLUMN
agent_id uuid;
UPDATE
tailnet_clients
SET
-- there's no reason for us to try and preserve data since coordinators will
-- have to restart anyways, which will create all of the client mappings.
agent_id = '00000000-0000-0000-0000-000000000000'::uuid;
ALTER TABLE
tailnet_clients
ALTER COLUMN
agent_id SET NOT NULL;
DROP TABLE tailnet_client_subscriptions;
DROP FUNCTION tailnet_notify_client_subscription_change;
-- update the tailnet_clients trigger to the old version.
CREATE OR REPLACE FUNCTION tailnet_notify_client_change() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF (OLD IS NOT NULL) THEN
PERFORM pg_notify('tailnet_client_update', OLD.id || ',' || OLD.agent_id);
RETURN NULL;
END IF;
IF (NEW IS NOT NULL) THEN
PERFORM pg_notify('tailnet_client_update', NEW.id || ',' || NEW.agent_id);
RETURN NULL;
END IF;
END;
$$;
COMMIT;

View File

@ -0,0 +1,88 @@
BEGIN;
CREATE TABLE tailnet_client_subscriptions (
client_id uuid NOT NULL,
coordinator_id uuid NOT NULL,
-- this isn't a foreign key since it's more of a list of agents the client
-- *wants* to connect to, and they don't necessarily have to currently
-- exist in the db.
agent_id uuid NOT NULL,
updated_at timestamp with time zone NOT NULL,
PRIMARY KEY (client_id, coordinator_id, agent_id),
FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators (id) ON DELETE CASCADE
-- we don't keep a foreign key to the tailnet_clients table since there's
-- not a great way to guarantee that a subscription is always added after
-- the client is inserted. clients are only created after the client sends
-- its first node update, which can take an undetermined amount of time.
);
CREATE FUNCTION tailnet_notify_client_subscription_change() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF (NEW IS NOT NULL) THEN
PERFORM pg_notify('tailnet_client_update', NEW.client_id || ',' || NEW.agent_id);
RETURN NULL;
ELSIF (OLD IS NOT NULL) THEN
PERFORM pg_notify('tailnet_client_update', OLD.client_id || ',' || OLD.agent_id);
RETURN NULL;
END IF;
END;
$$;
CREATE TRIGGER tailnet_notify_client_subscription_change
AFTER INSERT OR UPDATE OR DELETE ON tailnet_client_subscriptions
FOR EACH ROW
EXECUTE PROCEDURE tailnet_notify_client_subscription_change();
CREATE OR REPLACE FUNCTION tailnet_notify_client_change() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
var_client_id uuid;
var_coordinator_id uuid;
var_agent_ids uuid[];
var_agent_id uuid;
BEGIN
IF (NEW.id IS NOT NULL) THEN
var_client_id = NEW.id;
var_coordinator_id = NEW.coordinator_id;
ELSIF (OLD.id IS NOT NULL) THEN
var_client_id = OLD.id;
var_coordinator_id = OLD.coordinator_id;
END IF;
-- Read all agents the client is subscribed to, so we can notify them.
SELECT
array_agg(agent_id)
INTO
var_agent_ids
FROM
tailnet_client_subscriptions subs
WHERE
subs.client_id = NEW.id AND
subs.coordinator_id = NEW.coordinator_id;
-- No agents to notify
if (var_agent_ids IS NULL) THEN
return NULL;
END IF;
-- pg_notify is limited to 8k bytes, which is approximately 221 UUIDs.
-- Instead of sending all agent ids in a single update, send one for each
-- agent id to prevent overflow.
FOREACH var_agent_id IN ARRAY var_agent_ids
LOOP
PERFORM pg_notify('tailnet_client_update', var_client_id || ',' || var_agent_id);
END LOOP;
return NULL;
END;
$$;
ALTER TABLE
tailnet_clients
DROP COLUMN
agent_id;
COMMIT;

View File

@ -18,7 +18,7 @@ VALUES
);
INSERT INTO tailnet_agents
(id, coordinator_id, updated_at, node)
(id, coordinator_id, updated_at, node)
VALUES
(
'c0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',

View File

@ -0,0 +1,9 @@
INSERT INTO tailnet_client_subscriptions
(client_id, agent_id, coordinator_id, updated_at)
VALUES
(
'b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
'c0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
'2023-06-15 10:23:54+00'
);

View File

@ -1783,11 +1783,17 @@ type TailnetAgent struct {
type TailnetClient struct {
ID uuid.UUID `db:"id" json:"id"`
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Node json.RawMessage `db:"node" json:"node"`
}
type TailnetClientSubscription struct {
ClientID uuid.UUID `db:"client_id" json:"client_id"`
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// We keep this separate from replicas in case we need to break the coordinator out into its own service
type TailnetCoordinator struct {
ID uuid.UUID `db:"id" json:"id"`

View File

@ -36,6 +36,7 @@ type sqlcQuerier interface {
CleanTailnetCoordinators(ctx context.Context) error
DeleteAPIKeyByID(ctx context.Context, id string) error
DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
DeleteAllTailnetClientSubscriptions(ctx context.Context, arg DeleteAllTailnetClientSubscriptionsParams) error
DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
DeleteCoordinator(ctx context.Context, id uuid.UUID) error
DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error
@ -50,6 +51,7 @@ type sqlcQuerier interface {
DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error
DeleteTailnetAgent(ctx context.Context, arg DeleteTailnetAgentParams) (DeleteTailnetAgentRow, error)
DeleteTailnetClient(ctx context.Context, arg DeleteTailnetClientParams) (DeleteTailnetClientRow, error)
DeleteTailnetClientSubscription(ctx context.Context, arg DeleteTailnetClientSubscriptionParams) error
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
// there is no unique constraint on empty token names
GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error)
@ -59,7 +61,7 @@ type sqlcQuerier interface {
GetActiveUserCount(ctx context.Context) (int64, error)
GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error)
GetAllTailnetAgents(ctx context.Context) ([]TailnetAgent, error)
GetAllTailnetClients(ctx context.Context) ([]TailnetClient, error)
GetAllTailnetClients(ctx context.Context) ([]GetAllTailnetClientsRow, error)
GetAppSecurityKey(ctx context.Context) (string, error)
// GetAuditLogsBefore retrieves `row_limit` number of audit logs before the provided
// ID.
@ -324,6 +326,7 @@ type sqlcQuerier interface {
UpsertServiceBanner(ctx context.Context, value string) error
UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error)
UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error)
UpsertTailnetClientSubscription(ctx context.Context, arg UpsertTailnetClientSubscriptionParams) error
UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error)
}

View File

@ -4131,6 +4131,22 @@ func (q *sqlQuerier) CleanTailnetCoordinators(ctx context.Context) error {
return err
}
const deleteAllTailnetClientSubscriptions = `-- name: DeleteAllTailnetClientSubscriptions :exec
DELETE
FROM tailnet_client_subscriptions
WHERE client_id = $1 and coordinator_id = $2
`
type DeleteAllTailnetClientSubscriptionsParams struct {
ClientID uuid.UUID `db:"client_id" json:"client_id"`
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
}
func (q *sqlQuerier) DeleteAllTailnetClientSubscriptions(ctx context.Context, arg DeleteAllTailnetClientSubscriptionsParams) error {
_, err := q.db.ExecContext(ctx, deleteAllTailnetClientSubscriptions, arg.ClientID, arg.CoordinatorID)
return err
}
const deleteCoordinator = `-- name: DeleteCoordinator :exec
DELETE
FROM tailnet_coordinators
@ -4190,6 +4206,23 @@ func (q *sqlQuerier) DeleteTailnetClient(ctx context.Context, arg DeleteTailnetC
return i, err
}
const deleteTailnetClientSubscription = `-- name: DeleteTailnetClientSubscription :exec
DELETE
FROM tailnet_client_subscriptions
WHERE client_id = $1 and agent_id = $2 and coordinator_id = $3
`
type DeleteTailnetClientSubscriptionParams struct {
ClientID uuid.UUID `db:"client_id" json:"client_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
}
func (q *sqlQuerier) DeleteTailnetClientSubscription(ctx context.Context, arg DeleteTailnetClientSubscriptionParams) error {
_, err := q.db.ExecContext(ctx, deleteTailnetClientSubscription, arg.ClientID, arg.AgentID, arg.CoordinatorID)
return err
}
const getAllTailnetAgents = `-- name: GetAllTailnetAgents :many
SELECT id, coordinator_id, updated_at, node
FROM tailnet_agents
@ -4224,26 +4257,32 @@ func (q *sqlQuerier) GetAllTailnetAgents(ctx context.Context) ([]TailnetAgent, e
}
const getAllTailnetClients = `-- name: GetAllTailnetClients :many
SELECT id, coordinator_id, agent_id, updated_at, node
SELECT tailnet_clients.id, tailnet_clients.coordinator_id, tailnet_clients.updated_at, tailnet_clients.node, array_agg(tailnet_client_subscriptions.agent_id)::uuid[] as agent_ids
FROM tailnet_clients
ORDER BY agent_id
LEFT JOIN tailnet_client_subscriptions
ON tailnet_clients.id = tailnet_client_subscriptions.client_id
`
func (q *sqlQuerier) GetAllTailnetClients(ctx context.Context) ([]TailnetClient, error) {
type GetAllTailnetClientsRow struct {
TailnetClient TailnetClient `db:"tailnet_client" json:"tailnet_client"`
AgentIds []uuid.UUID `db:"agent_ids" json:"agent_ids"`
}
func (q *sqlQuerier) GetAllTailnetClients(ctx context.Context) ([]GetAllTailnetClientsRow, error) {
rows, err := q.db.QueryContext(ctx, getAllTailnetClients)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TailnetClient
var items []GetAllTailnetClientsRow
for rows.Next() {
var i TailnetClient
var i GetAllTailnetClientsRow
if err := rows.Scan(
&i.ID,
&i.CoordinatorID,
&i.AgentID,
&i.UpdatedAt,
&i.Node,
&i.TailnetClient.ID,
&i.TailnetClient.CoordinatorID,
&i.TailnetClient.UpdatedAt,
&i.TailnetClient.Node,
pq.Array(&i.AgentIds),
); err != nil {
return nil, err
}
@ -4293,9 +4332,13 @@ func (q *sqlQuerier) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]Tail
}
const getTailnetClientsForAgent = `-- name: GetTailnetClientsForAgent :many
SELECT id, coordinator_id, agent_id, updated_at, node
SELECT id, coordinator_id, updated_at, node
FROM tailnet_clients
WHERE agent_id = $1
WHERE id IN (
SELECT tailnet_client_subscriptions.client_id
FROM tailnet_client_subscriptions
WHERE tailnet_client_subscriptions.agent_id = $1
)
`
func (q *sqlQuerier) GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error) {
@ -4310,7 +4353,6 @@ func (q *sqlQuerier) GetTailnetClientsForAgent(ctx context.Context, agentID uuid
if err := rows.Scan(
&i.ID,
&i.CoordinatorID,
&i.AgentID,
&i.UpdatedAt,
&i.Node,
); err != nil {
@ -4369,47 +4411,67 @@ INSERT INTO
tailnet_clients (
id,
coordinator_id,
agent_id,
node,
updated_at
)
VALUES
($1, $2, $3, $4, now() at time zone 'utc')
($1, $2, $3, now() at time zone 'utc')
ON CONFLICT (id, coordinator_id)
DO UPDATE SET
id = $1,
coordinator_id = $2,
agent_id = $3,
node = $4,
node = $3,
updated_at = now() at time zone 'utc'
RETURNING id, coordinator_id, agent_id, updated_at, node
RETURNING id, coordinator_id, updated_at, node
`
type UpsertTailnetClientParams struct {
ID uuid.UUID `db:"id" json:"id"`
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
Node json.RawMessage `db:"node" json:"node"`
}
func (q *sqlQuerier) UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error) {
row := q.db.QueryRowContext(ctx, upsertTailnetClient,
arg.ID,
arg.CoordinatorID,
arg.AgentID,
arg.Node,
)
row := q.db.QueryRowContext(ctx, upsertTailnetClient, arg.ID, arg.CoordinatorID, arg.Node)
var i TailnetClient
err := row.Scan(
&i.ID,
&i.CoordinatorID,
&i.AgentID,
&i.UpdatedAt,
&i.Node,
)
return i, err
}
const upsertTailnetClientSubscription = `-- name: UpsertTailnetClientSubscription :exec
INSERT INTO
tailnet_client_subscriptions (
client_id,
coordinator_id,
agent_id,
updated_at
)
VALUES
($1, $2, $3, now() at time zone 'utc')
ON CONFLICT (client_id, coordinator_id, agent_id)
DO UPDATE SET
client_id = $1,
coordinator_id = $2,
agent_id = $3,
updated_at = now() at time zone 'utc'
`
type UpsertTailnetClientSubscriptionParams struct {
ClientID uuid.UUID `db:"client_id" json:"client_id"`
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
}
func (q *sqlQuerier) UpsertTailnetClientSubscription(ctx context.Context, arg UpsertTailnetClientSubscriptionParams) error {
_, err := q.db.ExecContext(ctx, upsertTailnetClientSubscription, arg.ClientID, arg.CoordinatorID, arg.AgentID)
return err
}
const upsertTailnetCoordinator = `-- name: UpsertTailnetCoordinator :one
INSERT INTO
tailnet_coordinators (

View File

@ -3,21 +3,36 @@ INSERT INTO
tailnet_clients (
id,
coordinator_id,
agent_id,
node,
updated_at
)
VALUES
($1, $2, $3, $4, now() at time zone 'utc')
($1, $2, $3, now() at time zone 'utc')
ON CONFLICT (id, coordinator_id)
DO UPDATE SET
id = $1,
coordinator_id = $2,
agent_id = $3,
node = $4,
node = $3,
updated_at = now() at time zone 'utc'
RETURNING *;
-- name: UpsertTailnetClientSubscription :exec
INSERT INTO
tailnet_client_subscriptions (
client_id,
coordinator_id,
agent_id,
updated_at
)
VALUES
($1, $2, $3, now() at time zone 'utc')
ON CONFLICT (client_id, coordinator_id, agent_id)
DO UPDATE SET
client_id = $1,
coordinator_id = $2,
agent_id = $3,
updated_at = now() at time zone 'utc';
-- name: UpsertTailnetAgent :one
INSERT INTO
tailnet_agents (
@ -43,6 +58,16 @@ FROM tailnet_clients
WHERE id = $1 and coordinator_id = $2
RETURNING id, coordinator_id;
-- name: DeleteTailnetClientSubscription :exec
DELETE
FROM tailnet_client_subscriptions
WHERE client_id = $1 and agent_id = $2 and coordinator_id = $3;
-- name: DeleteAllTailnetClientSubscriptions :exec
DELETE
FROM tailnet_client_subscriptions
WHERE client_id = $1 and coordinator_id = $2;
-- name: DeleteTailnetAgent :one
DELETE
FROM tailnet_agents
@ -66,12 +91,17 @@ FROM tailnet_agents;
-- name: GetTailnetClientsForAgent :many
SELECT *
FROM tailnet_clients
WHERE agent_id = $1;
WHERE id IN (
SELECT tailnet_client_subscriptions.client_id
FROM tailnet_client_subscriptions
WHERE tailnet_client_subscriptions.agent_id = $1
);
-- name: GetAllTailnetClients :many
SELECT *
SELECT sqlc.embed(tailnet_clients), array_agg(tailnet_client_subscriptions.agent_id)::uuid[] as agent_ids
FROM tailnet_clients
ORDER BY agent_id;
LEFT JOIN tailnet_client_subscriptions
ON tailnet_clients.id = tailnet_client_subscriptions.client_id;
-- name: UpsertTailnetCoordinator :one
INSERT INTO