1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6 "context"
7
8 "connectrpc.com/connect"
9
10 pb "bnema.dev/ermite-api/gen/ermite"
11 "bnema.dev/ermite/internal/domain"
12 "bnema.dev/ermite/internal/ports"
13)
14
15// FeatureCISecrets is listed by GetVersion when the server serves the secret,
16// variable and environment services and sends secrets, variables and the trust
17// flag to runners.
18const FeatureCISecrets = "ci.secrets"
19
20// settingsScopeFromPB reads a SettingsScope. A scope that is not set, a false instance
21// and a name that breaks the rules are invalid_argument.
22func settingsScopeFromPB(s *pb.SettingsScope) (domain.SettingsScope, error) {
23 bad := func(reason string) error {
24 return &domain.InvalidFieldsError{Fields: []domain.FieldViolation{{Field: "scope", Reason: reason}}}
25 }
26 switch w := s.GetWhere().(type) {
27 case *pb.SettingsScope_Instance:
28 if !w.Instance {
29 return domain.SettingsScope{}, bad("instance must be true when set")
30 }
31 return domain.InstanceScope(), nil
32 case *pb.SettingsScope_Repo:
33 name, err := domain.ParseRepoName(w.Repo)
34 if err != nil {
35 return domain.SettingsScope{}, err
36 }
37 return domain.RepoScope(name), nil
38 case *pb.SettingsScope_Environment:
39 name, err := domain.ParseRepoName(w.Environment.GetRepo())
40 if err != nil {
41 return domain.SettingsScope{}, err
42 }
43 env, err := domain.ParseEnvironmentName(w.Environment.GetName())
44 if err != nil {
45 return domain.SettingsScope{}, bad("environment name " + err.Error())
46 }
47 return domain.EnvironmentScope(name, env), nil
48 }
49 return domain.SettingsScope{}, bad("is required")
50}
51
52func secretToPB(s domain.Secret) *pb.Secret {
53 return &pb.Secret{Name: s.Name, UpdatedAt: timestamp(s.UpdatedAt)} // never a value
54}
55
56func variableToPB(v domain.Variable) *pb.Variable {
57 return &pb.Variable{Name: v.Name, Value: v.Value, UpdatedAt: timestamp(v.UpdatedAt)}
58}
59
60func environmentToPB(e domain.Environment) *pb.Environment {
61 return &pb.Environment{Repo: e.Repo.String(), Name: e.Name, CreatedAt: timestamp(e.CreatedAt)}
62}
63
64// secretService implements ermiteconnect.SecretServiceHandler. A request holds
65// a secret value: nothing of it is logged here, and the interceptor logs
66// procedures only.
67type secretService struct{ secrets ports.SecretService }
68
69func (s *secretService) SetSecret(ctx context.Context, req *connect.Request[pb.SetSecretRequest]) (*connect.Response[pb.SetSecretResponse], error) {
70 scope, err := settingsScopeFromPB(req.Msg.GetScope())
71 if err != nil {
72 return nil, connectError(ctx, err)
73 }
74 sec, err := s.secrets.SetSecret(ctx, caller(ctx), scope, req.Msg.GetName(), req.Msg.GetValue())
75 if err != nil {
76 return nil, connectError(ctx, err)
77 }
78 return connect.NewResponse(&pb.SetSecretResponse{Secret: secretToPB(sec)}), nil
79}
80
81func (s *secretService) ListSecrets(ctx context.Context, req *connect.Request[pb.ListSecretsRequest]) (*connect.Response[pb.ListSecretsResponse], error) {
82 scope, err := settingsScopeFromPB(req.Msg.GetScope())
83 if err != nil {
84 return nil, connectError(ctx, err)
85 }
86 page, err := s.secrets.ListSecrets(ctx, caller(ctx), scope, int(req.Msg.GetPageSize()), req.Msg.GetPageToken())
87 if err != nil {
88 return nil, connectError(ctx, err)
89 }
90 out := &pb.ListSecretsResponse{NextPageToken: page.NextPageToken, Secrets: make([]*pb.Secret, 0, len(page.Secrets))}
91 for _, x := range page.Secrets {
92 out.Secrets = append(out.Secrets, secretToPB(x))
93 }
94 return connect.NewResponse(out), nil
95}
96
97func (s *secretService) DeleteSecret(ctx context.Context, req *connect.Request[pb.DeleteSecretRequest]) (*connect.Response[pb.DeleteSecretResponse], error) {
98 scope, err := settingsScopeFromPB(req.Msg.GetScope())
99 if err != nil {
100 return nil, connectError(ctx, err)
101 }
102 if err := s.secrets.DeleteSecret(ctx, caller(ctx), scope, req.Msg.GetName()); err != nil {
103 return nil, connectError(ctx, err)
104 }
105 return connect.NewResponse(&pb.DeleteSecretResponse{}), nil
106}
107
108// variableService implements ermiteconnect.VariableServiceHandler.
109type variableService struct{ vars ports.VariableService }
110
111func (s *variableService) SetVariable(ctx context.Context, req *connect.Request[pb.SetVariableRequest]) (*connect.Response[pb.SetVariableResponse], error) {
112 scope, err := settingsScopeFromPB(req.Msg.GetScope())
113 if err != nil {
114 return nil, connectError(ctx, err)
115 }
116 v, err := s.vars.SetVariable(ctx, caller(ctx), scope, req.Msg.GetName(), req.Msg.GetValue())
117 if err != nil {
118 return nil, connectError(ctx, err)
119 }
120 return connect.NewResponse(&pb.SetVariableResponse{Variable: variableToPB(v)}), nil
121}
122
123func (s *variableService) GetVariable(ctx context.Context, req *connect.Request[pb.GetVariableRequest]) (*connect.Response[pb.GetVariableResponse], error) {
124 scope, err := settingsScopeFromPB(req.Msg.GetScope())
125 if err != nil {
126 return nil, connectError(ctx, err)
127 }
128 v, err := s.vars.GetVariable(ctx, caller(ctx), scope, req.Msg.GetName())
129 if err != nil {
130 return nil, connectError(ctx, err)
131 }
132 return connect.NewResponse(&pb.GetVariableResponse{Variable: variableToPB(v)}), nil
133}
134
135func (s *variableService) ListVariables(ctx context.Context, req *connect.Request[pb.ListVariablesRequest]) (*connect.Response[pb.ListVariablesResponse], error) {
136 scope, err := settingsScopeFromPB(req.Msg.GetScope())
137 if err != nil {
138 return nil, connectError(ctx, err)
139 }
140 page, err := s.vars.ListVariables(ctx, caller(ctx), scope, int(req.Msg.GetPageSize()), req.Msg.GetPageToken())
141 if err != nil {
142 return nil, connectError(ctx, err)
143 }
144 out := &pb.ListVariablesResponse{NextPageToken: page.NextPageToken, Variables: make([]*pb.Variable, 0, len(page.Variables))}
145 for _, x := range page.Variables {
146 out.Variables = append(out.Variables, variableToPB(x))
147 }
148 return connect.NewResponse(out), nil
149}
150
151func (s *variableService) DeleteVariable(ctx context.Context, req *connect.Request[pb.DeleteVariableRequest]) (*connect.Response[pb.DeleteVariableResponse], error) {
152 scope, err := settingsScopeFromPB(req.Msg.GetScope())
153 if err != nil {
154 return nil, connectError(ctx, err)
155 }
156 if err := s.vars.DeleteVariable(ctx, caller(ctx), scope, req.Msg.GetName()); err != nil {
157 return nil, connectError(ctx, err)
158 }
159 return connect.NewResponse(&pb.DeleteVariableResponse{}), nil
160}
161
162// environmentService implements ermiteconnect.EnvironmentServiceHandler.
163type environmentService struct{ envs ports.EnvironmentService }
164
165func (s *environmentService) CreateEnvironment(ctx context.Context, req *connect.Request[pb.CreateEnvironmentRequest]) (*connect.Response[pb.CreateEnvironmentResponse], error) {
166 repo, err := repoOf(req.Msg.GetRepo())
167 if err != nil {
168 return nil, connectError(ctx, err)
169 }
170 e, err := s.envs.CreateEnvironment(ctx, caller(ctx), repo, req.Msg.GetName())
171 if err != nil {
172 return nil, connectError(ctx, err)
173 }
174 return connect.NewResponse(&pb.CreateEnvironmentResponse{Environment: environmentToPB(e)}), nil
175}
176
177func (s *environmentService) ListEnvironments(ctx context.Context, req *connect.Request[pb.ListEnvironmentsRequest]) (*connect.Response[pb.ListEnvironmentsResponse], error) {
178 repo, err := repoOf(req.Msg.GetRepo())
179 if err != nil {
180 return nil, connectError(ctx, err)
181 }
182 page, err := s.envs.ListEnvironments(ctx, caller(ctx), repo, int(req.Msg.GetPageSize()), req.Msg.GetPageToken())
183 if err != nil {
184 return nil, connectError(ctx, err)
185 }
186 out := &pb.ListEnvironmentsResponse{NextPageToken: page.NextPageToken, Environments: make([]*pb.Environment, 0, len(page.Environments))}
187 for _, x := range page.Environments {
188 out.Environments = append(out.Environments, environmentToPB(x))
189 }
190 return connect.NewResponse(out), nil
191}
192
193func (s *environmentService) DeleteEnvironment(ctx context.Context, req *connect.Request[pb.DeleteEnvironmentRequest]) (*connect.Response[pb.DeleteEnvironmentResponse], error) {
194 repo, err := repoOf(req.Msg.GetRepo())
195 if err != nil {
196 return nil, connectError(ctx, err)
197 }
198 if err := s.envs.DeleteEnvironment(ctx, caller(ctx), repo, req.Msg.GetName()); err != nil {
199 return nil, connectError(ctx, err)
200 }
201 return connect.NewResponse(&pb.DeleteEnvironmentResponse{}), nil
202}