Commit 85d82d73 authored by Matthias Käppler's avatar Matthias Käppler

Merge branch '322128-token-order' into 'master'

Order cluster token by last used

See merge request gitlab-org/gitlab!57520
parents dfb9d338 cef28c0f
...@@ -8,6 +8,7 @@ module Clusters ...@@ -8,6 +8,7 @@ module Clusters
belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
has_many :agent_tokens, class_name: 'Clusters::AgentToken' has_many :agent_tokens, class_name: 'Clusters::AgentToken'
has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken'
scope :ordered_by_name, -> { order(:name) } scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) } scope :with_name, -> (name) { where(name: name) }
......
...@@ -6,7 +6,7 @@ module Clusters ...@@ -6,7 +6,7 @@ module Clusters
include TokenAuthenticatable include TokenAuthenticatable
add_authentication_token_field :token, encrypted: :required, token_generator: -> { Devise.friendly_token(50) } add_authentication_token_field :token, encrypted: :required, token_generator: -> { Devise.friendly_token(50) }
cached_attr_reader :last_contacted_at cached_attr_reader :last_used_at
self.table_name = 'cluster_agent_tokens' self.table_name = 'cluster_agent_tokens'
...@@ -21,6 +21,8 @@ module Clusters ...@@ -21,6 +21,8 @@ module Clusters
validates :description, length: { maximum: 1024 } validates :description, length: { maximum: 1024 }
validates :name, presence: true, length: { maximum: 255 } validates :name, presence: true, length: { maximum: 255 }
scope :order_last_used_at_desc, -> { order(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
def track_usage def track_usage
track_values = { last_used_at: Time.current.utc } track_values = { last_used_at: Time.current.utc }
......
---
title: Add ability to order cluster token by last used
merge_request: 57520
author:
type: changed
# frozen_string_literal: true
class AddClusterAgentTokenLastUsed < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
INDEX = 'index_cluster_agent_tokens_on_last_used_at'
disable_ddl_transaction!
def up
add_concurrent_index :cluster_agent_tokens,
:last_used_at,
name: INDEX,
order: { last_used_at: 'DESC NULLS LAST' }
end
def down
remove_concurrent_index_by_name :cluster_agent_tokens, INDEX
end
end
079ca92ac58519ce8f575c4cb94bfe6cf209e0c9eac20d3d3a294f5b468bc586
\ No newline at end of file
...@@ -22309,6 +22309,8 @@ CREATE INDEX index_cluster_agent_tokens_on_agent_id ON cluster_agent_tokens USIN ...@@ -22309,6 +22309,8 @@ CREATE INDEX index_cluster_agent_tokens_on_agent_id ON cluster_agent_tokens USIN
CREATE INDEX index_cluster_agent_tokens_on_created_by_user_id ON cluster_agent_tokens USING btree (created_by_user_id); CREATE INDEX index_cluster_agent_tokens_on_created_by_user_id ON cluster_agent_tokens USING btree (created_by_user_id);
CREATE INDEX index_cluster_agent_tokens_on_last_used_at ON cluster_agent_tokens USING btree (last_used_at DESC NULLS LAST);
CREATE UNIQUE INDEX index_cluster_agent_tokens_on_token_encrypted ON cluster_agent_tokens USING btree (token_encrypted); CREATE UNIQUE INDEX index_cluster_agent_tokens_on_token_encrypted ON cluster_agent_tokens USING btree (token_encrypted);
CREATE INDEX index_cluster_agents_on_created_by_user_id ON cluster_agents USING btree (created_by_user_id); CREATE INDEX index_cluster_agents_on_created_by_user_id ON cluster_agents USING btree (created_by_user_id);
...@@ -12,7 +12,7 @@ module Resolvers ...@@ -12,7 +12,7 @@ module Resolvers
def resolve(**args) def resolve(**args)
return ::Clusters::AgentToken.none unless can_read_agent_tokens? return ::Clusters::AgentToken.none unless can_read_agent_tokens?
agent.agent_tokens agent.last_used_agent_tokens
end end
private private
......
...@@ -28,7 +28,7 @@ module Resolvers ...@@ -28,7 +28,7 @@ module Resolvers
private private
def preloads def preloads
{ tokens: :agent_tokens } { tokens: [:last_used_agent_tokens, { last_used_agent_tokens: :agent }] }
end end
end end
end end
......
...@@ -14,8 +14,8 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do ...@@ -14,8 +14,8 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do
let(:feature_available) { true } let(:feature_available) { true }
let(:ctx) { Hash(current_user: user) } let(:ctx) { Hash(current_user: user) }
let!(:matching_token1) { create(:cluster_agent_token, agent: agent) } let!(:matching_token1) { create(:cluster_agent_token, agent: agent, last_used_at: 5.days.ago) }
let!(:mathcing_token2) { create(:cluster_agent_token, agent: agent) } let!(:matching_token2) { create(:cluster_agent_token, agent: agent, last_used_at: 2.days.ago) }
let!(:other_token) { create(:cluster_agent_token) } let!(:other_token) { create(:cluster_agent_token) }
subject { resolve(described_class, obj: agent, ctx: ctx) } subject { resolve(described_class, obj: agent, ctx: ctx) }
...@@ -24,8 +24,8 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do ...@@ -24,8 +24,8 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do
stub_licensed_features(cluster_agents: feature_available) stub_licensed_features(cluster_agents: feature_available)
end end
it 'returns tokens associated with the agent' do it 'returns tokens associated with the agent, ordered by last_used_at' do
expect(subject).to contain_exactly(matching_token1, mathcing_token2) expect(subject).to eq([matching_token2, matching_token1])
end end
context 'feature is not available' do context 'feature is not available' do
......
...@@ -50,22 +50,22 @@ RSpec.describe 'Project.cluster_agents' do ...@@ -50,22 +50,22 @@ RSpec.describe 'Project.cluster_agents' do
end end
context 'selecting tokens' do context 'selecting tokens' do
let(:cluster_agents_fields) { [:id, query_nodes(:tokens, of: 'ClusterAgentToken')] } let_it_be(:token_1) { create(:cluster_agent_token, agent: agents.first) }
let_it_be(:token_2) { create(:cluster_agent_token, agent: agents.second) }
let_it_be(:token_3) { create(:cluster_agent_token, agent: agents.second, last_used_at: 2.days.ago) }
before do let(:cluster_agents_fields) { [:id, query_nodes(:tokens, of: 'ClusterAgentToken')] }
create(:cluster_agent_token, agent: agents.first)
create(:cluster_agent_token, agent: agents.second)
end
it 'can select tokens' do it 'can select tokens in last_used_at order' do
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
tokens = graphql_data_at(:project, :cluster_agents, :nodes, :tokens, :nodes) tokens = graphql_data_at(:project, :cluster_agents, :nodes, :tokens, :nodes)
expect(tokens).to contain_exactly( expect(tokens).to match([
a_hash_including('id' => be_present), a_hash_including('id' => global_id_of(token_1)),
a_hash_including('id' => be_present) a_hash_including('id' => global_id_of(token_3)),
) a_hash_including('id' => global_id_of(token_2))
])
end end
it 'does not suffer from N+1 performance issues' do it 'does not suffer from N+1 performance issues' do
......
...@@ -8,6 +8,7 @@ RSpec.describe Clusters::Agent do ...@@ -8,6 +8,7 @@ RSpec.describe Clusters::Agent do
it { is_expected.to belong_to(:created_by_user).class_name('User').optional } it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
it { is_expected.to belong_to(:project).class_name('::Project') } it { is_expected.to belong_to(:project).class_name('::Project') }
it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken') } it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken') }
it { is_expected.to have_many(:last_used_agent_tokens).class_name('Clusters::AgentToken') }
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(63) } it { is_expected.to validate_length_of(:name).is_at_most(63) }
......
...@@ -9,6 +9,19 @@ RSpec.describe Clusters::AgentToken do ...@@ -9,6 +9,19 @@ RSpec.describe Clusters::AgentToken do
it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
describe 'scopes' do
describe '.order_last_used_at_desc' do
let_it_be(:token_1) { create(:cluster_agent_token, last_used_at: 7.days.ago) }
let_it_be(:token_2) { create(:cluster_agent_token, last_used_at: nil) }
let_it_be(:token_3) { create(:cluster_agent_token, last_used_at: 2.days.ago) }
it 'sorts by last_used_at descending, with null values at last' do
expect(described_class.order_last_used_at_desc)
.to eq([token_3, token_1, token_2])
end
end
end
describe '#token' do describe '#token' do
it 'is generated on save' do it 'is generated on save' do
agent_token = build(:cluster_agent_token, token_encrypted: nil) agent_token = build(:cluster_agent_token, token_encrypted: nil)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment