Skip to content

Commit dcc5223

Browse files
committed
sc-35229: Aptible Managed AI
1 parent f6ab918 commit dcc5223

File tree

5 files changed

+625
-0
lines changed

5 files changed

+625
-0
lines changed

lib/aptible/cli/agent.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
require_relative 'subcommands/metric_drain'
4646
require_relative 'subcommands/maintenance'
4747
require_relative 'subcommands/backup_retention_policy'
48+
require_relative 'subcommands/ai_tokens'
4849

4950
module Aptible
5051
module CLI
@@ -74,6 +75,7 @@ class Agent < Thor
7475
include Subcommands::MetricDrain
7576
include Subcommands::Maintenance
7677
include Subcommands::BackupRetentionPolicy
78+
include Subcommands::AiTokens
7779

7880
# Forward return codes on failures.
7981
def self.exit_on_failure?
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
require 'ostruct'
2+
3+
module Aptible
4+
module CLI
5+
module Subcommands
6+
module AiTokens
7+
def self.included(thor)
8+
thor.class_eval do
9+
include Helpers::Token
10+
include Helpers::Environment
11+
include Helpers::Telemetry
12+
13+
desc 'ai:tokens:create', 'Create a new AI token'
14+
option :environment, aliases: '--env', desc: 'Environment to create the token in'
15+
option :name, type: :string, desc: 'Name for the AI token'
16+
define_method 'ai:tokens:create' do
17+
# telemetry(__method__, options)
18+
19+
account = ensure_environment(options)
20+
21+
# POST /accounts/:account_id/ai_tokens
22+
ai_token_resource = Aptible::Api::Resource.new(token: fetch_token)
23+
ai_token_resource.href = "#{account.href}/ai_tokens"
24+
25+
params = {}
26+
params[:name] = options[:name] if options[:name]
27+
28+
response = ai_token_resource.post(params)
29+
30+
Formatter.render(Renderer.current) do |root|
31+
root.object do |node|
32+
node.value('id', response.id)
33+
node.value('name', response.name)
34+
# Use attributes[] to avoid collision with HyperResource's auth token property
35+
token_value = response.attributes['token']
36+
node.value('token', token_value) if token_value
37+
node.value('created_at', response.created_at)
38+
end
39+
end
40+
41+
token_value = response.attributes['token']
42+
if token_value
43+
CLI.logger.warn "\nSave the token value now - it will not be shown again!"
44+
end
45+
rescue HyperResource::ClientError, HyperResource::ServerError => e
46+
# Extract clean error message from response
47+
error_message = if e.respond_to?(:body) && e.body.is_a?(Hash)
48+
e.body['error'] || e.message
49+
else
50+
e.message
51+
end
52+
raise Thor::Error, error_message
53+
end
54+
55+
desc 'ai:tokens:list', 'List all AI tokens'
56+
option :environment, aliases: '--env', desc: 'Environment to list tokens from'
57+
define_method 'ai:tokens:list' do
58+
# telemetry(__method__, options)
59+
60+
Formatter.render(Renderer.current) do |root|
61+
root.grouped_keyed_list(
62+
{ 'environment' => 'handle' },
63+
'handle'
64+
) do |node|
65+
accounts = scoped_environments(options)
66+
67+
accounts.each do |account|
68+
# GET /accounts/:account_id/ai_tokens
69+
ai_tokens_resource = Aptible::Api::Resource.new(token: fetch_token)
70+
ai_tokens_resource.href = "#{account.href}/ai_tokens"
71+
72+
begin
73+
response = ai_tokens_resource.get
74+
75+
# HyperResource stores parsed JSON in the body attribute
76+
# Access the _embedded.ai_tokens array from the body
77+
tokens = if response.body && response.body['_embedded'] && response.body['_embedded']['ai_tokens']
78+
embedded_tokens = response.body['_embedded']['ai_tokens']
79+
# Convert hashes to OpenStruct for dot notation access
80+
# Add the account handle for grouped list formatting
81+
embedded_tokens.map do |token_data|
82+
OpenStruct.new(token_data.merge('handle' => account.handle))
83+
end
84+
else
85+
[]
86+
end
87+
88+
tokens.each do |ai_token|
89+
node.object do |n|
90+
# Show ID and name for text output, all fields for JSON
91+
n.value('handle', "#{ai_token.id} #{ai_token.name}")
92+
n.value('id', ai_token.id)
93+
n.value('name', ai_token.name)
94+
n.value('created_at', ai_token.created_at)
95+
n.value('last_used_at', ai_token.last_used_at) if ai_token.respond_to?(:last_used_at)
96+
97+
# Create nested environment structure for grouped_keyed_list
98+
n.keyed_object('environment', 'handle') do |env|
99+
env.value('handle', account.handle)
100+
end
101+
end
102+
end
103+
rescue HyperResource::ClientError => e
104+
# Skip if endpoint not available for this account
105+
next if e.response.status == 404
106+
raise
107+
end
108+
end
109+
end
110+
end
111+
end
112+
113+
desc 'ai:tokens:show GUID', 'Show details of an AI token'
114+
define_method 'ai:tokens:show' do |guid|
115+
# telemetry(__method__, options.merge(guid: guid))
116+
117+
# GET /ai_tokens/:guid via HAL
118+
ai_token_resource = Aptible::Api::Resource.new(token: fetch_token)
119+
ai_token_resource.href = "/ai_tokens/#{guid}"
120+
121+
begin
122+
response = ai_token_resource.get
123+
124+
# Parse the response from body
125+
token_data = if response.body
126+
OpenStruct.new(response.body)
127+
else
128+
raise Thor::Error, "No data received for token #{guid}"
129+
end
130+
131+
Formatter.render(Renderer.current) do |root|
132+
root.object do |node|
133+
node.value('id', token_data.id)
134+
node.value('name', token_data.name)
135+
node.value('created_at', token_data.created_at)
136+
node.value('updated_at', token_data.updated_at)
137+
node.value('last_used_at', token_data.last_used_at) if token_data.respond_to?(:last_used_at)
138+
node.value('revoked_at', token_data.revoked_at) if token_data.respond_to?(:revoked_at)
139+
end
140+
end
141+
rescue HyperResource::ClientError => e
142+
if e.response.status == 404
143+
raise Thor::Error, "AI token #{guid} not found or access denied"
144+
else
145+
raise Thor::Error, "Failed to retrieve token: #{e.message}"
146+
end
147+
end
148+
end
149+
150+
desc 'ai:tokens:revoke GUID', 'Revoke an AI token'
151+
define_method 'ai:tokens:revoke' do |guid|
152+
# telemetry(__method__, options.merge(guid: guid))
153+
154+
# DELETE /ai_tokens/:guid via HAL
155+
ai_token = Aptible::Api::Resource.new(token: fetch_token)
156+
ai_token.href = "/ai_tokens/#{guid}"
157+
158+
begin
159+
ai_token.delete
160+
CLI.logger.info 'AI token revoked successfully'
161+
rescue HyperResource::ClientError => e
162+
if e.response.status == 404
163+
raise Thor::Error, "AI token #{guid} not found or access denied"
164+
else
165+
raise Thor::Error, "Failed to revoke token: #{e.message}"
166+
end
167+
end
168+
end
169+
end
170+
end
171+
end
172+
end
173+
end
174+
end
175+

0 commit comments

Comments
 (0)