11453: Merge branch 'master' into 11453-federated-tokens
[arvados.git] / tools / arv-sync-groups / arv-sync-groups.go
index 0f702562aede5c189800ea0705787b8dda92e6b9..6b4781c3549627f0f9874cc0be734b611b41c5dd 100644 (file)
@@ -19,7 +19,7 @@ import (
        "git.curoverse.com/arvados.git/sdk/go/arvados"
 )
 
-// const remoteGroupParentName string = "Externally synchronized groups"
+var version = "dev"
 
 type resourceList interface {
        Len() int
@@ -81,21 +81,9 @@ func (l GroupList) GetItems() (out []interface{}) {
        return
 }
 
-// Link is an arvados#link record
-type Link struct {
-       UUID      string `json:"uuid,omiempty"`
-       OwnerUUID string `json:"owner_uuid,omitempty"`
-       Name      string `json:"name,omitempty"`
-       LinkClass string `json:"link_class,omitempty"`
-       HeadUUID  string `json:"head_uuid,omitempty"`
-       HeadKind  string `json:"head_kind,omitempty"`
-       TailUUID  string `json:"tail_uuid,omitempty"`
-       TailKind  string `json:"tail_kind,omitempty"`
-}
-
 // LinkList implements resourceList interface
 type LinkList struct {
-       Items []Link `json:"items"`
+       arvados.LinkList
 }
 
 // Len returns the amount of items this list holds
@@ -112,7 +100,13 @@ func (l LinkList) GetItems() (out []interface{}) {
 }
 
 func main() {
-       if err := doMain(); err != nil {
+       // Parse & validate arguments, set up arvados client.
+       cfg, err := GetConfig()
+       if err != nil {
+               log.Fatalf("%v", err)
+       }
+
+       if err := doMain(&cfg); err != nil {
                log.Fatalf("%v", err)
        }
 }
@@ -135,11 +129,21 @@ func ParseFlags(config *ConfigParams) error {
                "email":    true, // default
                "username": true,
        }
+
        flags := flag.NewFlagSet("arv-sync-groups", flag.ExitOnError)
-       srcPath := flags.String(
-               "path",
-               "",
-               "Local file path containing a CSV format: GroupName,UserID")
+
+       // Set up usage message
+       flags.Usage = func() {
+               usageStr := `Synchronize remote groups into Arvados from a CSV format file with 2 columns:
+  * 1st column: Group name
+  * 2nd column: User identifier`
+               fmt.Fprintf(os.Stderr, "%s\n\n", usageStr)
+               fmt.Fprintf(os.Stderr, "Usage:\n%s [OPTIONS] <input-file.csv>\n\n", os.Args[0])
+               fmt.Fprintf(os.Stderr, "Options:\n")
+               flags.PrintDefaults()
+       }
+
+       // Set up option flags
        userID := flags.String(
                "user-id",
                "email",
@@ -148,6 +152,10 @@ func ParseFlags(config *ConfigParams) error {
                "verbose",
                false,
                "Log informational messages. Off by default.")
+       getVersion := flags.Bool(
+               "version",
+               false,
+               "Print version information and exit.")
        parentGroupUUID := flags.String(
                "parent-group-uuid",
                "",
@@ -156,9 +164,21 @@ func ParseFlags(config *ConfigParams) error {
        // Parse args; omit the first arg which is the command name
        flags.Parse(os.Args[1:])
 
+       // Print version information if requested
+       if *getVersion {
+               fmt.Printf("arv-sync-groups %s\n", version)
+               os.Exit(0)
+       }
+
+       // Input file as a required positional argument
+       if flags.NArg() == 0 {
+               return fmt.Errorf("please provide a path to an input file")
+       }
+       srcPath := &os.Args[flags.NFlag()+1]
+
        // Validations
        if *srcPath == "" {
-               return fmt.Errorf("please provide a path to an input file")
+               return fmt.Errorf("input file path invalid")
        }
        if !userIDOpts[*userID] {
                var options []string
@@ -260,13 +280,7 @@ func GetConfig() (config ConfigParams, err error) {
        return config, nil
 }
 
-func doMain() error {
-       // Parse & validate arguments, set up arvados client.
-       cfg, err := GetConfig()
-       if err != nil {
-               return err
-       }
-
+func doMain(cfg *ConfigParams) error {
        // Try opening the input file early, just in case there's a problem.
        f, err := os.Open(cfg.Path)
        if err != nil {
@@ -274,7 +288,7 @@ func doMain() error {
        }
        defer f.Close()
 
-       log.Printf("Group sync starting. Using %q as users id and parent group UUID %q", cfg.UserID, cfg.ParentGroupUUID)
+       log.Printf("arv-sync-groups %s started. Using %q as users id and parent group UUID %q", version, cfg.UserID, cfg.ParentGroupUUID)
 
        // Get the complete user list to minimize API Server requests
        allUsers := make(map[string]arvados.User)
@@ -298,7 +312,7 @@ func doMain() error {
        }
 
        // Get remote groups and their members
-       remoteGroups, groupNameToUUID, err := GetRemoteGroups(&cfg, allUsers)
+       remoteGroups, groupNameToUUID, err := GetRemoteGroups(cfg, allUsers)
        if err != nil {
                return err
        }
@@ -307,7 +321,7 @@ func doMain() error {
        membershipsRemoved := 0
 
        // Read the CSV file
-       groupsCreated, membershipsAdded, membershipsSkipped, err := ProcessFile(&cfg, f, userIDToUUID, groupNameToUUID, remoteGroups, allUsers)
+       groupsCreated, membershipsAdded, membershipsSkipped, err := ProcessFile(cfg, f, userIDToUUID, groupNameToUUID, remoteGroups, allUsers)
        if err != nil {
                return err
        }
@@ -321,7 +335,7 @@ func doMain() error {
                        log.Printf("Removing %d users from group %q", len(evictedMembers), groupName)
                }
                for evictedUser := range evictedMembers {
-                       if err := RemoveMemberFromGroup(&cfg, allUsers[userIDToUUID[evictedUser]], gi.Group); err != nil {
+                       if err := RemoveMemberFromGroup(cfg, allUsers[userIDToUUID[evictedUser]], gi.Group); err != nil {
                                return err
                        }
                        membershipsRemoved++
@@ -333,15 +347,25 @@ func doMain() error {
 }
 
 // ProcessFile reads the CSV file and process every record
-func ProcessFile(cfg *ConfigParams, f *os.File, userIDToUUID map[string]string, groupNameToUUID map[string]string, remoteGroups map[string]*GroupInfo, allUsers map[string]arvados.User) (groupsCreated, membersAdded, membersSkipped int, err error) {
+func ProcessFile(
+       cfg *ConfigParams,
+       f *os.File,
+       userIDToUUID map[string]string,
+       groupNameToUUID map[string]string,
+       remoteGroups map[string]*GroupInfo,
+       allUsers map[string]arvados.User,
+) (groupsCreated, membersAdded, membersSkipped int, err error) {
+       lineNo := 0
        csvReader := csv.NewReader(f)
+       csvReader.FieldsPerRecord = 2
        for {
                record, e := csvReader.Read()
                if e == io.EOF {
                        break
                }
+               lineNo++
                if e != nil {
-                       err = fmt.Errorf("error reading %q: %s", cfg.Path, err)
+                       err = fmt.Errorf("error parsing %q, line %d", cfg.Path, lineNo)
                        return
                }
                groupName := strings.TrimSpace(record[0])
@@ -364,8 +388,9 @@ func ProcessFile(cfg *ConfigParams, f *os.File, userIDToUUID map[string]string,
                        }
                        var newGroup arvados.Group
                        groupData := map[string]string{
-                               "name":       groupName,
-                               "owner_uuid": cfg.ParentGroupUUID,
+                               "name":        groupName,
+                               "owner_uuid":  cfg.ParentGroupUUID,
+                               "group_class": "role",
                        }
                        if e := CreateGroup(cfg, &newGroup, groupData); e != nil {
                                err = fmt.Errorf("error creating group named %q: %s", groupName, err)
@@ -520,11 +545,11 @@ func GetRemoteGroups(cfg *ConfigParams, allUsers map[string]arvados.User) (remot
                membersSet := make(map[string]bool)
                u2gLinkSet := make(map[string]bool)
                for _, l := range u2gLinks {
-                       linkedMemberUUID := l.(Link).TailUUID
+                       linkedMemberUUID := l.(arvados.Link).TailUUID
                        u2gLinkSet[linkedMemberUUID] = true
                }
                for _, item := range g2uLinks {
-                       link := item.(Link)
+                       link := item.(arvados.Link)
                        // We may have received an old link pointing to a removed account.
                        if _, found := allUsers[link.HeadUUID]; !found {
                                continue
@@ -598,12 +623,12 @@ func RemoveMemberFromGroup(cfg *ConfigParams, user arvados.User, group arvados.G
                }
        }
        for _, item := range links {
-               link := item.(Link)
+               link := item.(arvados.Link)
+               userID, _ := GetUserID(user, cfg.UserID)
                if cfg.Verbose {
-                       log.Printf("Removing %q permission link for %q on group %q", link.Name, user.Email, group.Name)
+                       log.Printf("Removing %q permission link for %q on group %q", link.Name, userID, group.Name)
                }
                if err := DeleteLink(cfg, link.UUID); err != nil {
-                       userID, _ := GetUserID(user, cfg.UserID)
                        return fmt.Errorf("error removing user %q from group %q: %s", userID, group.Name, err)
                }
        }
@@ -612,7 +637,7 @@ func RemoveMemberFromGroup(cfg *ConfigParams, user arvados.User, group arvados.G
 
 // AddMemberToGroup create membership links
 func AddMemberToGroup(cfg *ConfigParams, user arvados.User, group arvados.Group) error {
-       var newLink Link
+       var newLink arvados.Link
        linkData := map[string]string{
                "owner_uuid": cfg.SysUserUUID,
                "link_class": "permission",
@@ -622,7 +647,7 @@ func AddMemberToGroup(cfg *ConfigParams, user arvados.User, group arvados.Group)
        }
        if err := CreateLink(cfg, &newLink, linkData); err != nil {
                userID, _ := GetUserID(user, cfg.UserID)
-               return fmt.Errorf("error adding group %q -> user %q read permission: %s", userID, user.Email, err)
+               return fmt.Errorf("error adding group %q -> user %q read permission: %s", group.Name, userID, err)
        }
        linkData = map[string]string{
                "owner_uuid": cfg.SysUserUUID,
@@ -649,7 +674,7 @@ func GetGroup(cfg *ConfigParams, dst *arvados.Group, groupUUID string) error {
 }
 
 // CreateLink creates a link with linkData parameters, assigns it to dst
-func CreateLink(cfg *ConfigParams, dst *Link, linkData map[string]string) error {
+func CreateLink(cfg *ConfigParams, dst *arvados.Link, linkData map[string]string) error {
        return cfg.Client.RequestAndDecode(dst, "POST", "/arvados/v1/links", jsonReader("link", linkData), nil)
 }
 
@@ -658,7 +683,7 @@ func DeleteLink(cfg *ConfigParams, linkUUID string) error {
        if linkUUID == "" {
                return fmt.Errorf("cannot delete link with invalid UUID: %q", linkUUID)
        }
-       return cfg.Client.RequestAndDecode(&Link{}, "DELETE", "/arvados/v1/links/"+linkUUID, nil, nil)
+       return cfg.Client.RequestAndDecode(&arvados.Link{}, "DELETE", "/arvados/v1/links/"+linkUUID, nil, nil)
 }
 
 // GetResourceList fetches res list using params