package service import ( "context" "go.infratographer.com/x/events" "go.infratographer.com/x/gidx" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" ) // processRelationships determines the changes between what is wanted and what is live and executes on the differences. // Relationship creations use events. // Relationship deletions use the api, as delete events delete all related resources and not just the provided ones. func (s *service) processRelationships(ctx context.Context, eventType string, relationships Relationships) int { ctx, span := tracer.Start(ctx, "processRelationships", trace.WithAttributes( attribute.String("resource.id", relationships.Resource.PrefixedID().String()), attribute.Int("resource.subject_relationships", len(relationships.SubjectRelationships)), ), ) defer span.End() rlogger := s.logger.With("resource.id", relationships.Resource.PrefixedID()) wantParentRelationship, wantSubjectRelationships := s.mapRelationWants(relationships) liveParentRelationships, liveSubjectRelationships, err := s.getRelationshipMap(ctx, relationships.Resource, relationships.SubjectRelation) if err != nil { rlogger.Errorw("failed to get relationship map", "relationships.subject_relation", relationships.SubjectRelation, "error", err, ) return 0 } var ( createParentRelationship *Relation deleteParentRelationships []gidx.PrefixedID foundParent bool createSubjectRelationships []Relation deleteSubjectRelationships []Relation ) if wantParentRelationship != nil { for subjID := range liveParentRelationships { if subjID == wantParentRelationship.Resource.PrefixedID() { foundParent = true continue } deleteParentRelationships = append(deleteParentRelationships, subjID) } if !foundParent { createParentRelationship = wantParentRelationship } } else { for subjID := range liveParentRelationships { deleteParentRelationships = append(deleteParentRelationships, subjID) } } for resID, relation := range wantSubjectRelationships { if _, ok := liveSubjectRelationships[resID]; ok { continue } createSubjectRelationships = append(createSubjectRelationships, Relation{ Resource: prefixedID{resID}, Relation: relation, }) } for resID, relation := range liveSubjectRelationships { if _, ok := wantSubjectRelationships[resID]; ok { continue } deleteSubjectRelationships = append(deleteSubjectRelationships, Relation{ Resource: prefixedID{resID}, Relation: relation, }) } var processEvents []events.ChangeMessage rlogger.Debugw("processing relationships", "parent.create", createParentRelationship != nil, "parent.delete", len(deleteParentRelationships), "subject.create", len(createSubjectRelationships), "subject.delete", len(deleteSubjectRelationships), ) if createParentRelationship != nil { processEvents = append(processEvents, events.ChangeMessage{ SubjectID: relationships.Resource.PrefixedID(), EventType: string(events.CreateChangeType), AdditionalSubjectIDs: []gidx.PrefixedID{ createParentRelationship.Resource.PrefixedID(), }, }) } for _, relation := range createSubjectRelationships { processEvents = append(processEvents, events.ChangeMessage{ SubjectID: relation.Resource.PrefixedID(), EventType: string(events.CreateChangeType), AdditionalSubjectIDs: []gidx.PrefixedID{ relationships.Resource.PrefixedID(), }, }) } for _, relatedResourceID := range deleteParentRelationships { err = s.perms.DeleteResourceRelationship(ctx, relationships.Resource.PrefixedID(), string(RelateParent), relatedResourceID) if err != nil { rlogger.Errorw("error deleting parent relationship", "parent.resource.id", relatedResourceID.String(), ) } } for _, relation := range deleteSubjectRelationships { err = s.perms.DeleteResourceRelationship(ctx, relation.Resource.PrefixedID(), string(relation.Relation), relationships.Resource.PrefixedID()) if err != nil { rlogger.Errorw("error deleting relationship", "relation", relation.Relation, "subject.id", relation.Resource.PrefixedID().String(), ) } } for _, event := range processEvents { err = s.publisher.PublishChange(ctx, eventType, event) if err != nil { rlogger.Errorw("error publishing change", "subject_type", eventType, "subject.id", event.SubjectID, "event.type", event.EventType, "additional_subject_ids", event.AdditionalSubjectIDs, "error", err, ) } } rlogger.Debugw("relationships processed", "parent.create", createParentRelationship != nil, "parent.delete", len(deleteParentRelationships), "subject.create", len(createSubjectRelationships), "subject.delete", len(deleteSubjectRelationships), ) changes := len(deleteParentRelationships) + len(createSubjectRelationships) + len(deleteSubjectRelationships) if createParentRelationship != nil { changes++ } return changes } // mapRelationWants returns the parent relation if provided and a map of Subjects -> relation. func (s *service) mapRelationWants(relationships Relationships) (*Relation, map[gidx.PrefixedID]RelationshipType) { var wantParent *Relation wantSubject := make(map[gidx.PrefixedID]RelationshipType) if relationships.Parent.Resource != nil { wantParent = &relationships.Parent } for _, relationship := range relationships.SubjectRelationships { wantSubject[relationship.Resource.PrefixedID()] = relationship.Relation } return wantParent, wantSubject } // getRelationshipMap fetches the provided resources relationships, as the source resource and the destination subject. // Returned are two maps, the first maps Subject IDs -> Relationship // The second map, maps Resource IDs -> relationship func (s *service) getRelationshipMap(ctx context.Context, resource IDPrefixableResource, relation RelationshipType) (map[gidx.PrefixedID]RelationshipType, map[gidx.PrefixedID]RelationshipType, error) { liveResource, err := s.perms.ListResourceRelationshipsFrom(ctx, resource.PrefixedID()) if err != nil { return nil, nil, err } var liveSubject []permissions.ResourceRelationship if relation != "" { liveSubject, err = s.perms.ListResourceRelationshipsTo(ctx, resource.PrefixedID()) if err != nil { return nil, nil, err } } parents := make(map[gidx.PrefixedID]RelationshipType, len(liveResource)) for _, relationship := range liveResource { if relationship.Relation != string(RelateParent) { continue } parents[relationship.SubjectID] = RelationshipType(relationship.Relation) } subject := make(map[gidx.PrefixedID]RelationshipType, len(liveSubject)) for _, relationship := range liveSubject { if relationship.Relation != string(relation) { continue } subject[relationship.ResourceID] = RelationshipType(relationship.Relation) } return parents, subject, nil }