Skip to content

Commit 23e8f6b

Browse files
committed
feat(front): add service accounts service layer
1 parent b8b892f commit 23e8f6b

22 files changed

+1057
-10
lines changed

front/config/config.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ config :front, default_user_name: "Semaphore User"
3232
config :feature_provider, provider: {Front.FeatureHubProvider, []}
3333
config :front, :superjerry_client, {Support.FakeClients.Superjerry, []}
3434
config :front, :scouter_client, {Front.Clients.Scouter, []}
35+
config :front, :service_account_client, {Support.FakeClients.ServiceAccount, []}
3536

3637
config :money,
3738
default_currency: :USD,

front/config/prod.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ config :front, FrontWeb.Endpoint,
88
cache_static_manifest: "priv/static/cache_manifest.json"
99

1010
config :front, :superjerry_client, {Front.Clients.Superjerry, []}
11+
config :front, :service_account_client, {Front.Clients.ServiceAccount, []}
1112
config :front, guard_grpc_timeout: 15_000
1213
config :front, permission_patrol_timeout: 15_000
1314
config :front, me_host: "me.", me_path: "/"

front/config/runtime.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ if config_env() == :prod do
109109
# sobelow_skip ["Config.Secrets"]
110110
secrets_api_grpc_endpoint: System.fetch_env!("INTERNAL_API_URL_SECRETHUB"),
111111
self_hosted_agents_grpc_endpoint: System.fetch_env!("INTERNAL_API_URL_SELFHOSTEDHUB"),
112+
service_account_grpc_endpoint: System.fetch_env!("INTERNAL_API_URL_SERVICE_ACCOUNT"),
112113
superjerry_grpc_endpoint: System.fetch_env!("INTERNAL_API_URL_SUPERJERRY"),
113114
task_grpc_endpoint: System.fetch_env!("INTERNAL_API_URL_TASK"),
114115
velocity_grpc_endpoint: System.fetch_env!("INTERNAL_API_URL_VELOCITY"),

front/lib/front/application.ex

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,21 @@ defmodule Front.Application do
1616
{&:logger_filters.domain/2, {:stop, :equal, [:progress]}}
1717
)
1818

19+
base_children = [
20+
{Phoenix.PubSub, [name: Front.PubSub, adapter: Phoenix.PubSub.PG2]},
21+
FrontWeb.Endpoint,
22+
{Task.Supervisor, [name: Front.TaskSupervisor]},
23+
Front.Tracing.Store,
24+
Front.FeatureProviderInvalidatorWorker
25+
]
26+
1927
children =
20-
[
21-
{Phoenix.PubSub, [name: Front.PubSub, adapter: Phoenix.PubSub.PG2]},
22-
FrontWeb.Endpoint,
23-
{Task.Supervisor, [name: Front.TaskSupervisor]},
24-
Front.Tracing.Store,
25-
Front.FeatureProviderInvalidatorWorker
26-
] ++ reactor() ++ cache() ++ telemetry() ++ feature_provider(provider)
28+
base_children ++
29+
reactor() ++
30+
cache() ++
31+
telemetry() ++
32+
feature_provider(provider) ++
33+
clients()
2734

2835
opts = [strategy: :one_for_one, name: Front.Supervisor]
2936

@@ -93,6 +100,16 @@ defmodule Front.Application do
93100
end
94101
end
95102

103+
def clients do
104+
if Application.get_env(:front, :environment) in [:dev, :test] do
105+
[
106+
Application.get_env(:front, :service_account_client)
107+
]
108+
else
109+
[]
110+
end
111+
end
112+
96113
def config_change(changed, _new, removed) do
97114
FrontWeb.Endpoint.config_change(changed, removed)
98115
:ok
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
defmodule Front.Clients.ServiceAccount do
2+
require Logger
3+
4+
alias InternalApi.ServiceAccount.{
5+
CreateRequest,
6+
DeleteRequest,
7+
DescribeRequest,
8+
ListRequest,
9+
RegenerateTokenRequest,
10+
UpdateRequest
11+
}
12+
13+
alias Front.Models.ServiceAccount
14+
15+
@behaviour Front.ServiceAccount.Behaviour
16+
17+
@impl Front.ServiceAccount.Behaviour
18+
def create(org_id, name, description, creator_id) do
19+
%CreateRequest{
20+
org_id: org_id,
21+
name: name,
22+
description: description,
23+
creator_id: creator_id
24+
}
25+
|> grpc_call(:create)
26+
|> case do
27+
{:ok, result} ->
28+
service_account = ServiceAccount.from_proto(result.service_account)
29+
{:ok, {service_account, result.api_token}}
30+
31+
err ->
32+
Logger.error("Error creating service account for org #{org_id}: #{inspect(err)}")
33+
handle_error(err)
34+
end
35+
end
36+
37+
@impl Front.ServiceAccount.Behaviour
38+
def list(org_id, page_size, page_token) do
39+
%ListRequest{
40+
org_id: org_id,
41+
page_size: page_size,
42+
page_token: page_token || ""
43+
}
44+
|> grpc_call(:list)
45+
|> case do
46+
{:ok, result} ->
47+
service_accounts = Enum.map(result.service_accounts, &ServiceAccount.from_proto/1)
48+
next_page_token = if result.next_page_token == "", do: nil, else: result.next_page_token
49+
{:ok, {service_accounts, next_page_token}}
50+
51+
err ->
52+
Logger.error("Error listing service accounts for org #{org_id}: #{inspect(err)}")
53+
handle_error(err)
54+
end
55+
end
56+
57+
@impl Front.ServiceAccount.Behaviour
58+
def describe(service_account_id) do
59+
%DescribeRequest{
60+
service_account_id: service_account_id
61+
}
62+
|> grpc_call(:describe)
63+
|> case do
64+
{:ok, result} ->
65+
service_account = ServiceAccount.from_proto(result.service_account)
66+
{:ok, service_account}
67+
68+
err ->
69+
Logger.error("Error describing service account #{service_account_id}: #{inspect(err)}")
70+
handle_error(err)
71+
end
72+
end
73+
74+
@impl Front.ServiceAccount.Behaviour
75+
def update(service_account_id, name, description) do
76+
%UpdateRequest{
77+
service_account_id: service_account_id,
78+
name: name,
79+
description: description
80+
}
81+
|> grpc_call(:update)
82+
|> case do
83+
{:ok, result} ->
84+
service_account = ServiceAccount.from_proto(result.service_account)
85+
{:ok, service_account}
86+
87+
err ->
88+
Logger.error("Error updating service account #{service_account_id}: #{inspect(err)}")
89+
handle_error(err)
90+
end
91+
end
92+
93+
@impl Front.ServiceAccount.Behaviour
94+
def delete(service_account_id) do
95+
%DeleteRequest{
96+
service_account_id: service_account_id
97+
}
98+
|> grpc_call(:delete)
99+
|> case do
100+
{:ok, _result} ->
101+
:ok
102+
103+
err ->
104+
Logger.error("Error deleting service account #{service_account_id}: #{inspect(err)}")
105+
handle_error(err)
106+
end
107+
end
108+
109+
@impl Front.ServiceAccount.Behaviour
110+
def regenerate_token(service_account_id) do
111+
%RegenerateTokenRequest{
112+
service_account_id: service_account_id
113+
}
114+
|> grpc_call(:regenerate_token)
115+
|> case do
116+
{:ok, result} ->
117+
{:ok, result.api_token}
118+
119+
err ->
120+
Logger.error(
121+
"Error regenerating token for service account #{service_account_id}: #{inspect(err)}"
122+
)
123+
124+
handle_error(err)
125+
end
126+
end
127+
128+
defp grpc_call(request, action) do
129+
Watchman.benchmark("service_account.#{action}.duration", fn ->
130+
channel()
131+
|> call_grpc(
132+
InternalApi.ServiceAccount.ServiceAccountService.Stub,
133+
action,
134+
request,
135+
metadata(),
136+
timeout()
137+
)
138+
|> tap(fn
139+
{:ok, _} -> Watchman.increment("service_account.#{action}.success")
140+
{:error, _} -> Watchman.increment("service_account.#{action}.failure")
141+
end)
142+
end)
143+
end
144+
145+
defp call_grpc(error = {:error, err}, _, _, _, _, _) do
146+
Logger.error("""
147+
Unexpected error when connecting to ServiceAccount: #{inspect(err)}
148+
""")
149+
150+
error
151+
end
152+
153+
defp call_grpc({:ok, channel}, module, function_name, request, metadata, timeout) do
154+
apply(module, function_name, [channel, request, [metadata: metadata, timeout: timeout]])
155+
end
156+
157+
defp channel do
158+
Application.fetch_env!(:front, :service_account_grpc_endpoint)
159+
|> GRPC.Stub.connect()
160+
end
161+
162+
defp timeout do
163+
15_000
164+
end
165+
166+
defp metadata do
167+
nil
168+
end
169+
170+
defp handle_error({:error, error}) do
171+
statuses_to_ignore = [GRPC.Status.internal()]
172+
173+
error
174+
|> case do
175+
%GRPC.RPCError{status: status, message: message} when status not in statuses_to_ignore ->
176+
{:error, message}
177+
178+
_ ->
179+
{:error, "Unknown error, if this persists, please contact support."}
180+
end
181+
end
182+
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
defmodule Front.Models.ServiceAccount do
2+
@moduledoc """
3+
Model representing a Service Account
4+
"""
5+
6+
defstruct [
7+
:id,
8+
:name,
9+
:description,
10+
:org_id,
11+
:creator_id,
12+
:created_at,
13+
:updated_at,
14+
:deactivated
15+
]
16+
17+
@type t :: %__MODULE__{
18+
id: String.t(),
19+
name: String.t(),
20+
description: String.t(),
21+
org_id: String.t(),
22+
creator_id: String.t(),
23+
created_at: DateTime.t(),
24+
updated_at: DateTime.t(),
25+
deactivated: boolean()
26+
}
27+
28+
@doc """
29+
Creates a new ServiceAccount struct from protobuf data
30+
"""
31+
@spec from_proto(InternalApi.ServiceAccount.t()) :: t
32+
def from_proto(proto) do
33+
%__MODULE__{
34+
id: proto.id,
35+
name: proto.name,
36+
description: proto.description,
37+
org_id: proto.org_id,
38+
creator_id: proto.creator_id,
39+
created_at: timestamp_to_datetime(proto.created_at),
40+
updated_at: timestamp_to_datetime(proto.updated_at),
41+
deactivated: proto.deactivated
42+
}
43+
end
44+
45+
defp timestamp_to_datetime(%Google.Protobuf.Timestamp{seconds: seconds}) do
46+
DateTime.from_unix!(seconds)
47+
end
48+
49+
defp timestamp_to_datetime(_), do: DateTime.utc_now()
50+
end

front/lib/front/service_account.ex

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
defmodule Front.ServiceAccount do
2+
defmodule Behaviour do
3+
@callback create(
4+
org_id :: String.t(),
5+
name :: String.t(),
6+
description :: String.t(),
7+
creator_id :: String.t()
8+
) :: {:ok, {Front.Models.ServiceAccount.t(), String.t()}} | {:error, any}
9+
10+
@callback list(
11+
org_id :: String.t(),
12+
page_size :: integer(),
13+
page_token :: String.t() | nil
14+
) :: {:ok, {[Front.Models.ServiceAccount.t()], String.t() | nil}} | {:error, any}
15+
16+
@callback describe(service_account_id :: String.t()) ::
17+
{:ok, Front.Models.ServiceAccount.t()} | {:error, any}
18+
19+
@callback update(
20+
service_account_id :: String.t(),
21+
name :: String.t(),
22+
description :: String.t()
23+
) :: {:ok, Front.Models.ServiceAccount.t()} | {:error, any}
24+
25+
@callback delete(service_account_id :: String.t()) :: :ok | {:error, any}
26+
27+
@callback regenerate_token(service_account_id :: String.t()) ::
28+
{:ok, String.t()} | {:error, any}
29+
end
30+
31+
def create(org_id, name, description, creator_id),
32+
do: service_account_impl().create(org_id, name, description, creator_id)
33+
34+
def list(org_id, page_size, page_token \\ nil),
35+
do: service_account_impl().list(org_id, page_size, page_token)
36+
37+
def describe(service_account_id), do: service_account_impl().describe(service_account_id)
38+
39+
def update(service_account_id, name, description),
40+
do: service_account_impl().update(service_account_id, name, description)
41+
42+
def delete(service_account_id), do: service_account_impl().delete(service_account_id)
43+
44+
def regenerate_token(service_account_id),
45+
do: service_account_impl().regenerate_token(service_account_id)
46+
47+
defp service_account_impl do
48+
{client, _client_opts} = Application.fetch_env!(:front, :service_account_client)
49+
50+
client
51+
end
52+
end

0 commit comments

Comments
 (0)