1 package services
2
3 import (
4 "context"
5 "fmt"
6 "sync"
7 "time"
8
9 "github.com/rs/zerolog/log"
10 "google.golang.org/api/cloudresourcemanager/v1"
11 "google.golang.org/api/googleapi"
12 "google.golang.org/api/iam/v1"
13 "google.golang.org/api/option"
14 "k8s.io/apimachinery/pkg/util/wait"
15 "k8s.io/client-go/util/retry"
16
17 edgeiam "edge-infra.dev/pkg/lib/gcp/iam"
18 )
19
20 type IAMService interface {
21 CreateServiceAccount(ctx context.Context, projectID, serviceAccountName, displayName, description string) (*iam.ServiceAccount, error)
22 CreatePolicyBinding(ctx context.Context, serviceAccountProjectID, policyProjectID, serviceAccountName string, roles []IAMRole) (*cloudresourcemanager.Policy, error)
23 CreateSAKey(ctx context.Context, projectID string, serviceAccount *iam.ServiceAccount) (*iam.ServiceAccountKey, error)
24 CreateSAandSAKey(ctx context.Context, ProjectID, serviceAccountName, displayName, description string) (*edgeiam.Component, error)
25 CreateOrUpdateEdgeIAM(ctx context.Context, serviceAccountProjectID, policyProjectID, serviceAccountName string, componentIAM *edgeiam.Component, roles []IAMRole) (*edgeiam.Component, error)
26 }
27
28 type iamService struct {
29 opts []option.ClientOption
30 sync.Mutex
31 }
32
33 type IAMRole struct {
34 name string
35 roleType string
36 wiNameSpace string
37 wiK8sSA string
38 }
39
40 const (
41 WIType = "wiUser"
42 GenericType = "generic"
43 )
44
45 func NewIamRole(name string) IAMRole {
46 return IAMRole{
47 name: name,
48 roleType: GenericType,
49 wiNameSpace: "",
50 wiK8sSA: "",
51 }
52 }
53
54 func NewWIIamRole(name, ns, k8sSAName string) IAMRole {
55 return IAMRole{
56 name: name,
57 roleType: WIType,
58 wiNameSpace: ns,
59 wiK8sSA: k8sSAName,
60 }
61 }
62
63 func (r IAMRole) getMemberName(projectID, name string) string {
64 memberstr := edgeiam.StandardSvcAccountMember(name, projectID)
65 if r.roleType == WIType {
66 memberstr = edgeiam.WorkloadIdentityMember(projectID, r.wiNameSpace, r.wiK8sSA)
67 }
68 return memberstr
69 }
70
71 var (
72 defaultRetry = wait.Backoff{
73 Duration: 500 * time.Millisecond,
74 Steps: 10,
75 Factor: 1.5,
76 Jitter: 0.1,
77 }
78 )
79
80
81 func (s *iamService) CreateServiceAccount(ctx context.Context, projectID, serviceAccountName, displayName, description string) (*iam.ServiceAccount, error) {
82 iamservice, err := edgeiam.NewIAMService(ctx, s.opts...)
83 project := fmt.Sprintf("projects/%s", projectID)
84 if err != nil {
85 log.Ctx(ctx).Err(fmt.Errorf("failed to init iamservice & failed to create configconnectormeta resource name: %s %s %w", project, serviceAccountName, err)).
86 Msg("create service account failed")
87 return nil, err
88 }
89
90
91 serviceAccount := edgeiam.NewServiceAccount(displayName, description)
92 serviceAccountRequest := edgeiam.NewServiceAccountRequest(serviceAccountName, serviceAccount)
93 serviceAccount, err = iamservice.CreateServiceAccount(ctx, project, serviceAccountRequest)
94 if err != nil {
95 log.Ctx(ctx).Err(fmt.Errorf("failed to create service account: %w", err)).
96 Msg("create service account failed")
97 if errorIs409(err) {
98 return handleErrorIs409(ctx, serviceAccountName, projectID, project, iamservice)
99 }
100 return nil, err
101 }
102 return serviceAccount, nil
103 }
104
105 func handleErrorIs409(ctx context.Context, name, projectID, project string, iamservice *edgeiam.IAMService) (*iam.ServiceAccount, error) {
106 saEmail := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", name, projectID)
107 projectName := fmt.Sprintf("%s/serviceAccounts/%s", project, saEmail)
108 sa, err := iamservice.GetServiceAccount(ctx, projectName)
109 if err != nil {
110 log.Ctx(ctx).Err(fmt.Errorf("SA not found: %s, %w", projectName, err)).
111 Msg("getting service account failed")
112 return nil, err
113 }
114 saKeyProjectID := fmt.Sprintf("%s/serviceAccounts/%s", project, sa.Email)
115 err = cleanupSAKeys(ctx, iamservice, saKeyProjectID)
116 if err != nil {
117 return nil, err
118 }
119 return sa, nil
120 }
121
122 func cleanupSAKeys(ctx context.Context, iamservice *edgeiam.IAMService, saKeyProjectID string) error {
123 if err := iamservice.DeleteExcessServiceAccountKeys(ctx, saKeyProjectID); err != nil {
124 if errorIs404(err) {
125 log.Ctx(ctx).Err(fmt.Errorf("SA keys not found: %s, %w", saKeyProjectID, err)).
126 Msg("deleting service account key failed")
127 } else {
128 log.Ctx(ctx).Err(err).Msg("deleting service account key failed")
129 return err
130 }
131 }
132 return nil
133 }
134
135 func (s *iamService) CreateSAKey(ctx context.Context, projectID string, serviceAccount *iam.ServiceAccount) (*iam.ServiceAccountKey, error) {
136 iamservice, err := edgeiam.NewIAMService(ctx, s.opts...)
137 if err != nil {
138 log.Ctx(ctx).Err(fmt.Errorf("failed to init iamservice: %s %w", projectID, err)).
139 Msg("create service account failed")
140 return nil, err
141 }
142
143 project := fmt.Sprintf("projects/%s", projectID)
144
145 serviceAccountKeyRequest := edgeiam.NewServiceAccountKeyRequest()
146
147 saKeyProjectID := fmt.Sprintf("%s/serviceAccounts/%s", project, serviceAccount.Email)
148 serviceAccountKey, err := iamservice.CreateServiceAccountKey(ctx, saKeyProjectID, serviceAccountKeyRequest)
149 if err != nil {
150 return nil, err
151 }
152 return serviceAccountKey, nil
153 }
154
155
156 func (s *iamService) CreatePolicyBinding(ctx context.Context, serviceAccountProjectID, policyProjectID, serviceAccountName string, roles []IAMRole) (*cloudresourcemanager.Policy, error) {
157 modifiedIamPolicy := &cloudresourcemanager.Policy{}
158 if len(roles) > 0 {
159 crmService, err := edgeiam.NewCRMService(ctx, s.opts...)
160 if err != nil {
161 return nil, err
162 }
163 iampolicy, err := crmService.GetPolicy(ctx, policyProjectID)
164 if err != nil {
165 return nil, err
166 }
167 bindingRoles := make(map[string]int)
168 for index, binding := range iampolicy.Bindings {
169 bindingRoles[binding.Role] = index
170 }
171 iampolicy = addBindingsToIAMPolicy(roles, serviceAccountName, serviceAccountProjectID, bindingRoles, iampolicy)
172 modifiedIamPolicy, err = crmService.SetPolicy(ctx, policyProjectID, iampolicy)
173 if err != nil {
174 return nil, err
175 }
176 }
177 return modifiedIamPolicy, nil
178 }
179
180 func addBindingsToIAMPolicy(roles []IAMRole, name, projectID string, bindingRoles map[string]int, iampolicy *cloudresourcemanager.Policy) *cloudresourcemanager.Policy {
181 for _, role := range roles {
182 memberstr := role.getMemberName(projectID, name)
183 if value, exists := bindingRoles[role.name]; exists {
184 iampolicy.Bindings[value].Members = append(iampolicy.Bindings[value].Members, memberstr)
185 } else {
186 policyBinding := edgeiam.CreatePolicyBinding(role.name, []string{memberstr})
187 iampolicy.Bindings = append(iampolicy.Bindings, policyBinding)
188 }
189 }
190 return iampolicy
191 }
192
193 func (s *iamService) CreateSAandSAKey(ctx context.Context, projectID, serviceAccountName, displayName, description string) (*edgeiam.Component, error) {
194 s.Lock()
195 defer s.Unlock()
196 sa, err := s.CreateServiceAccount(ctx, projectID, serviceAccountName, displayName, description)
197 if err != nil {
198 return nil, err
199 }
200 var saKey *iam.ServiceAccountKey
201
202 err = retry.OnError(defaultRetry, func(err error) bool { return isErrorCode(err, 409, 400, 404) }, func() error {
203 saKey, err = s.CreateSAKey(ctx, projectID, sa)
204 if err != nil {
205 log.Ctx(ctx).Err(fmt.Errorf("failed to create sa key: %s %v", projectID, sa)).
206 Msg("create service account failed. Will retry.")
207 }
208 return err
209 })
210 if err != nil {
211 log.Ctx(ctx).Err(fmt.Errorf("failed to create sa key: %s %v", projectID, sa)).
212 Msg("create service account failed. Will NOT retry.")
213 return nil, err
214 }
215 return edgeiam.NewComponent(saKey, sa, nil), nil
216 }
217
218 func (s *iamService) CreateOrUpdateEdgeIAM(ctx context.Context, serviceAccountProjectID, policyProjectID, serviceAccountName string, componentIAM *edgeiam.Component, roles []IAMRole) (*edgeiam.Component, error) {
219 s.Lock()
220 defer s.Unlock()
221 var (
222 modifiedIamPolicy *cloudresourcemanager.Policy
223 err error
224 )
225
226 err = retry.OnError(defaultRetry, func(err error) bool { return isErrorCode(err, 409, 400, 404) }, func() error {
227 modifiedIamPolicy, err = s.CreatePolicyBinding(ctx, serviceAccountProjectID, policyProjectID, serviceAccountName, roles)
228 if err != nil {
229 log.Ctx(ctx).Err(fmt.Errorf("failed to update iam policy: %v", err)).
230 Msg("update iam policy failed due to etag conflict. Will retry.")
231 }
232 return err
233 })
234 if err != nil {
235 return nil, err
236 }
237 return edgeiam.NewComponent(componentIAM.APIKey, componentIAM.ServiceAccount, modifiedIamPolicy), nil
238 }
239
240 func isErrorCode(err error, codes ...int) bool {
241 if e, ok := err.(*googleapi.Error); ok {
242 for _, code := range codes {
243 if e.Code == code {
244 return true
245 }
246 }
247 }
248 return false
249 }
250
251
252 func errorIs409(err error) bool {
253 if e, ok := err.(*googleapi.Error); ok {
254 return e.Code == 409
255 }
256 return false
257 }
258
259
260 func errorIs404(err error) bool {
261 if e, ok := err.(*googleapi.Error); ok {
262 return e.Code == 404
263 }
264 return false
265 }
266
267
268 func NewIAMService(opts ...option.ClientOption) IAMService {
269 return &iamService{
270 opts: opts,
271 }
272 }
273
View as plain text