From cef28c0f9acab5d318fb71a9a80e574b61744c49 Mon Sep 17 00:00:00 2001 From: Emily Ring <ering@gitlab.com> Date: Fri, 9 Apr 2021 07:56:13 +0000 Subject: [PATCH] Order cluster token by last used --- app/models/clusters/agent.rb | 1 + app/models/clusters/agent_token.rb | 4 +++- changelogs/unreleased/322128-token-order.yml | 5 +++++ ...52925_add_cluster_agent_token_last_used.rb | 20 +++++++++++++++++++ db/schema_migrations/20210407152925 | 1 + db/structure.sql | 2 ++ .../clusters/agent_tokens_resolver.rb | 2 +- .../resolvers/clusters/agents_resolver.rb | 2 +- .../clusters/agent_tokens_resolver_spec.rb | 8 ++++---- .../graphql/project/cluster_agents_spec.rb | 20 +++++++++---------- spec/models/clusters/agent_spec.rb | 1 + spec/models/clusters/agent_token_spec.rb | 13 ++++++++++++ 12 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 changelogs/unreleased/322128-token-order.yml create mode 100644 db/migrate/20210407152925_add_cluster_agent_token_last_used.rb create mode 100644 db/schema_migrations/20210407152925 diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb index c5b9dddb1daea..7c42fc28a8de2 100644 --- a/app/models/clusters/agent.rb +++ b/app/models/clusters/agent.rb @@ -8,6 +8,7 @@ class Agent < ApplicationRecord belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project 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 :with_name, -> (name) { where(name: name) } diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb index d42279502c5dd..27a3cd8d13d57 100644 --- a/app/models/clusters/agent_token.rb +++ b/app/models/clusters/agent_token.rb @@ -6,7 +6,7 @@ class AgentToken < ApplicationRecord include TokenAuthenticatable 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' @@ -21,6 +21,8 @@ class AgentToken < ApplicationRecord validates :description, length: { maximum: 1024 } 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 track_values = { last_used_at: Time.current.utc } diff --git a/changelogs/unreleased/322128-token-order.yml b/changelogs/unreleased/322128-token-order.yml new file mode 100644 index 0000000000000..cf2ccd5789817 --- /dev/null +++ b/changelogs/unreleased/322128-token-order.yml @@ -0,0 +1,5 @@ +--- +title: Add ability to order cluster token by last used +merge_request: 57520 +author: +type: changed diff --git a/db/migrate/20210407152925_add_cluster_agent_token_last_used.rb b/db/migrate/20210407152925_add_cluster_agent_token_last_used.rb new file mode 100644 index 0000000000000..8ade2971539c3 --- /dev/null +++ b/db/migrate/20210407152925_add_cluster_agent_token_last_used.rb @@ -0,0 +1,20 @@ +# 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 diff --git a/db/schema_migrations/20210407152925 b/db/schema_migrations/20210407152925 new file mode 100644 index 0000000000000..1a248d78c8f84 --- /dev/null +++ b/db/schema_migrations/20210407152925 @@ -0,0 +1 @@ +079ca92ac58519ce8f575c4cb94bfe6cf209e0c9eac20d3d3a294f5b468bc586 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index a553b47ff84dd..98f3206ab404b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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_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 INDEX index_cluster_agents_on_created_by_user_id ON cluster_agents USING btree (created_by_user_id); diff --git a/ee/app/graphql/resolvers/clusters/agent_tokens_resolver.rb b/ee/app/graphql/resolvers/clusters/agent_tokens_resolver.rb index b0aae648b90ce..758010239dfef 100644 --- a/ee/app/graphql/resolvers/clusters/agent_tokens_resolver.rb +++ b/ee/app/graphql/resolvers/clusters/agent_tokens_resolver.rb @@ -12,7 +12,7 @@ class AgentTokensResolver < BaseResolver def resolve(**args) return ::Clusters::AgentToken.none unless can_read_agent_tokens? - agent.agent_tokens + agent.last_used_agent_tokens end private diff --git a/ee/app/graphql/resolvers/clusters/agents_resolver.rb b/ee/app/graphql/resolvers/clusters/agents_resolver.rb index fca1258befc0f..f2630297f069f 100644 --- a/ee/app/graphql/resolvers/clusters/agents_resolver.rb +++ b/ee/app/graphql/resolvers/clusters/agents_resolver.rb @@ -28,7 +28,7 @@ def resolve_with_lookahead(**args) private def preloads - { tokens: :agent_tokens } + { tokens: [:last_used_agent_tokens, { last_used_agent_tokens: :agent }] } end end end diff --git a/ee/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb b/ee/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb index 7eeb63d2503a8..1a58100441131 100644 --- a/ee/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb @@ -14,8 +14,8 @@ let(:feature_available) { true } let(:ctx) { Hash(current_user: user) } - let!(:matching_token1) { create(:cluster_agent_token, agent: agent) } - let!(:mathcing_token2) { create(:cluster_agent_token, agent: agent) } + let!(:matching_token1) { create(:cluster_agent_token, agent: agent, last_used_at: 5.days.ago) } + let!(:matching_token2) { create(:cluster_agent_token, agent: agent, last_used_at: 2.days.ago) } let!(:other_token) { create(:cluster_agent_token) } subject { resolve(described_class, obj: agent, ctx: ctx) } @@ -24,8 +24,8 @@ stub_licensed_features(cluster_agents: feature_available) end - it 'returns tokens associated with the agent' do - expect(subject).to contain_exactly(matching_token1, mathcing_token2) + it 'returns tokens associated with the agent, ordered by last_used_at' do + expect(subject).to eq([matching_token2, matching_token1]) end context 'feature is not available' do diff --git a/ee/spec/requests/api/graphql/project/cluster_agents_spec.rb b/ee/spec/requests/api/graphql/project/cluster_agents_spec.rb index 65ec967d82e05..95cd4306c3067 100644 --- a/ee/spec/requests/api/graphql/project/cluster_agents_spec.rb +++ b/ee/spec/requests/api/graphql/project/cluster_agents_spec.rb @@ -50,22 +50,22 @@ end 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 - create(:cluster_agent_token, agent: agents.first) - create(:cluster_agent_token, agent: agents.second) - end + let(:cluster_agents_fields) { [:id, query_nodes(:tokens, of: 'ClusterAgentToken')] } - it 'can select tokens' do + it 'can select tokens in last_used_at order' do post_graphql(query, current_user: current_user) tokens = graphql_data_at(:project, :cluster_agents, :nodes, :tokens, :nodes) - expect(tokens).to contain_exactly( - a_hash_including('id' => be_present), - a_hash_including('id' => be_present) - ) + expect(tokens).to match([ + a_hash_including('id' => global_id_of(token_1)), + a_hash_including('id' => global_id_of(token_3)), + a_hash_including('id' => global_id_of(token_2)) + ]) end it 'does not suffer from N+1 performance issues' do diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb index a85a72eba0b71..ea7a55480a8a6 100644 --- a/spec/models/clusters/agent_spec.rb +++ b/spec/models/clusters/agent_spec.rb @@ -8,6 +8,7 @@ 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 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_length_of(:name).is_at_most(63) } diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb index 680b351d24a7f..bde4798abecb3 100644 --- a/spec/models/clusters/agent_token_spec.rb +++ b/spec/models/clusters/agent_token_spec.rb @@ -9,6 +9,19 @@ it { is_expected.to validate_length_of(:name).is_at_most(255) } 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 it 'is generated on save' do agent_token = build(:cluster_agent_token, token_encrypted: nil) -- GitLab