Skip to content

Commit cca1505

Browse files
authored
Merge pull request #72 from linuxfoundation/jme/LFXV2-1317
feat(committees): resolve org ID via search/create in v2->v1 sync
2 parents fede151 + 91bc534 commit cca1505

2 files changed

Lines changed: 165 additions & 8 deletions

File tree

cmd/lfx-v1-sync-helper/ingest_indexer.go

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,10 @@ func syncCommitteeMemberCreateToV1(ctx context.Context, memberUID, committeeUID,
377377
payload.VotingEndDate = ved
378378
}
379379
}
380-
if org, ok := data["organization"].(map[string]any); ok {
381-
if orgID, ok := org["id"].(string); ok {
382-
payload.OrganizationID = orgID
383-
}
380+
if orgID, err := resolveOrgIDFromEventData(ctx, data); err != nil {
381+
log.With(errKey, err).WarnContext(ctx, "failed to resolve organization ID, proceeding without org")
382+
} else if orgID != "" {
383+
payload.OrganizationID = orgID
384384
}
385385

386386
result, err := createV1CommitteeMember(ctx, projectSFID, committeeSFID, payload)
@@ -442,10 +442,10 @@ func syncCommitteeMemberUpdateToV1(ctx context.Context, memberUID, projectSFID,
442442
payload.VotingEndDate = ved
443443
}
444444
}
445-
if org, ok := data["organization"].(map[string]any); ok {
446-
if orgID, ok := org["id"].(string); ok {
447-
payload.OrganizationID = orgID
448-
}
445+
if orgID, err := resolveOrgIDFromEventData(ctx, data); err != nil {
446+
log.With(errKey, err).WarnContext(ctx, "failed to resolve organization ID, proceeding without org")
447+
} else if orgID != "" {
448+
payload.OrganizationID = orgID
449449
}
450450

451451
if err := updateV1CommitteeMember(ctx, projectSFID, committeeSFID, memberSFID, payload); err != nil {
@@ -475,6 +475,25 @@ func syncCommitteeMemberDeleteToV1(ctx context.Context, memberUID, projectSFID,
475475
log.InfoContext(ctx, "successfully deleted committee member in v1 from indexer event")
476476
}
477477

478+
// resolveOrgIDFromEventData extracts and resolves an organization SFID from committee member event data.
479+
// Returns empty string (no error) if no organization data is present or fields are all empty.
480+
func resolveOrgIDFromEventData(ctx context.Context, data map[string]any) (string, error) {
481+
org, ok := data["organization"].(map[string]any)
482+
if !ok {
483+
return "", nil
484+
}
485+
orgID, _ := org["id"].(string)
486+
orgName, _ := org["name"].(string)
487+
orgWebsite, _ := org["website"].(string)
488+
if orgID != "" {
489+
return orgID, nil
490+
}
491+
if orgName == "" && orgWebsite == "" {
492+
return "", nil
493+
}
494+
return resolveV1OrgID(ctx, orgName, orgWebsite)
495+
}
496+
478497
// splitTwoParts splits an "a:b" string into its two parts.
479498
func splitTwoParts(s string) (string, string, bool) {
480499
for i := 0; i < len(s); i++ {

cmd/lfx-v1-sync-helper/lfx_v1_client.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ type V1OrganizationResponse struct {
7474
Domain string `json:"Domains"`
7575
}
7676

77+
// V1OrganizationListResponse represents the list response from GET /v1/orgs/search
78+
type V1OrganizationListResponse struct {
79+
Data []V1OrganizationResponse `json:"Data"`
80+
}
81+
82+
// V1OrganizationCreateRequest is the request body for POST /v1/orgs
83+
type V1OrganizationCreateRequest struct {
84+
Name string `json:"Name"`
85+
Website string `json:"Website"`
86+
}
87+
7788
// ClientCredentialsTokenSource implements oauth2.TokenSource for Auth0 private key JWT
7889
type ClientCredentialsTokenSource struct {
7990
ctx context.Context
@@ -313,6 +324,133 @@ func getV1OrganizationFromOrgSvc(ctx context.Context, sfid string) (*V1Organizat
313324
return org, nil
314325
}
315326

327+
// searchV1OrgsByNameAndWebsite searches for organizations in the v1 Organization Service by name and/or website.
328+
// Returns the first matching organization, or nil if none found.
329+
func searchV1OrgsByNameAndWebsite(ctx context.Context, name, website string) (*V1Organization, error) {
330+
baseURL := fmt.Sprintf("%sorganization-service/v1/orgs/search", cfg.LFXAPIGateway.String())
331+
params := url.Values{}
332+
if name != "" {
333+
params.Set("name", name)
334+
}
335+
if website != "" {
336+
params.Add("website-array", website)
337+
}
338+
fullURL := baseURL + "?" + params.Encode()
339+
340+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
341+
if err != nil {
342+
return nil, fmt.Errorf("failed to create org search request: %w", err)
343+
}
344+
345+
resp, err := v1HTTPClient.Do(req)
346+
if err != nil {
347+
return nil, fmt.Errorf("failed to send org search request: %w", err)
348+
}
349+
defer resp.Body.Close()
350+
351+
body, err := io.ReadAll(resp.Body)
352+
if err != nil {
353+
return nil, fmt.Errorf("failed to read org search response: %w", err)
354+
}
355+
356+
if resp.StatusCode != http.StatusOK {
357+
return nil, fmt.Errorf("org service returned status %d searching by name=%q website=%q: %s", resp.StatusCode, name, website, string(body))
358+
}
359+
360+
var listResp V1OrganizationListResponse
361+
if err := json.Unmarshal(body, &listResp); err != nil {
362+
return nil, fmt.Errorf("failed to unmarshal org search response: %w", err)
363+
}
364+
365+
if len(listResp.Data) == 0 {
366+
return nil, nil
367+
}
368+
369+
first := listResp.Data[0]
370+
return &V1Organization{
371+
ID: first.ID,
372+
Name: first.Name,
373+
Domain: first.Domain,
374+
LastFetched: time.Now().UTC(),
375+
}, nil
376+
}
377+
378+
// createV1OrgInOrgSvc creates a new organization in the v1 Organization Service.
379+
func createV1OrgInOrgSvc(ctx context.Context, name, website string) (*V1Organization, error) {
380+
apiURL := fmt.Sprintf("%sorganization-service/v1/orgs", cfg.LFXAPIGateway.String())
381+
382+
reqBody, err := json.Marshal(V1OrganizationCreateRequest{Name: name, Website: website})
383+
if err != nil {
384+
return nil, fmt.Errorf("failed to marshal org create request: %w", err)
385+
}
386+
387+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(reqBody))
388+
if err != nil {
389+
return nil, fmt.Errorf("failed to create org create request: %w", err)
390+
}
391+
req.Header.Set("Content-Type", "application/json")
392+
393+
resp, err := v1HTTPClient.Do(req)
394+
if err != nil {
395+
return nil, fmt.Errorf("failed to send org create request: %w", err)
396+
}
397+
defer resp.Body.Close()
398+
399+
body, err := io.ReadAll(resp.Body)
400+
if err != nil {
401+
return nil, fmt.Errorf("failed to read org create response: %w", err)
402+
}
403+
404+
if resp.StatusCode != http.StatusCreated {
405+
return nil, fmt.Errorf("org service returned status %d creating org name=%q website=%q: %s", resp.StatusCode, name, website, string(body))
406+
}
407+
408+
var orgResp V1OrganizationResponse
409+
if err := json.Unmarshal(body, &orgResp); err != nil {
410+
return nil, fmt.Errorf("failed to unmarshal org create response: %w", err)
411+
}
412+
413+
return &V1Organization{
414+
ID: orgResp.ID,
415+
Name: orgResp.Name,
416+
Domain: orgResp.Domain,
417+
LastFetched: time.Now().UTC(),
418+
}, nil
419+
}
420+
421+
// resolveV1OrgID resolves a v1 Organization SFID from name and/or website.
422+
// It searches first; if not found and both name and website are provided, it creates a new org.
423+
func resolveV1OrgID(ctx context.Context, name, website string) (string, error) {
424+
if name == "" && website == "" {
425+
return "", fmt.Errorf("cannot resolve org: both name and website are empty")
426+
}
427+
428+
org, err := searchV1OrgsByNameAndWebsite(ctx, name, website)
429+
if err != nil {
430+
return "", fmt.Errorf("org search failed: %w", err)
431+
}
432+
if org != nil {
433+
return org.ID, nil
434+
}
435+
436+
if name == "" || website == "" {
437+
return "", fmt.Errorf("org not found and cannot create: missing %s", func() string {
438+
if name == "" && website == "" {
439+
return "name and website"
440+
} else if name == "" {
441+
return "name"
442+
}
443+
return "website"
444+
}())
445+
}
446+
447+
created, err := createV1OrgInOrgSvc(ctx, name, website)
448+
if err != nil {
449+
return "", fmt.Errorf("org create failed: %w", err)
450+
}
451+
return created.ID, nil
452+
}
453+
316454
// getCachedV1Org retrieves an organization from the mappings KV cache
317455
func getCachedV1Org(ctx context.Context, sfid string) (*V1Organization, error) {
318456
cacheKey := orgCacheKeyPrefix + sfid

0 commit comments

Comments
 (0)