"git.curoverse.com/arvados.git/sdk/go/arvados"
)
-// const remoteGroupParentName string = "Externally synchronized groups"
+var version = "dev"
type resourceList interface {
Len() int
GetItems() []interface{}
}
-type groupInfo struct {
+// GroupInfo tracks previous and current members of a particular Group
+type GroupInfo struct {
Group arvados.Group
PreviousMembers map[string]bool
CurrentMembers map[string]bool
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"`
+type LinkList struct {
+ arvados.LinkList
}
// Len returns the amount of items this list holds
-func (l linkList) Len() int {
+func (l LinkList) Len() int {
return len(l.Items)
}
// GetItems returns the list of items
-func (l linkList) GetItems() (out []interface{}) {
+func (l LinkList) GetItems() (out []interface{}) {
for _, item := range l.Items {
out = append(out, item)
}
}
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)
}
}
"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",
"verbose",
false,
"Log informational messages. Off by default.")
+ getVersion := flags.Bool(
+ "version",
+ false,
+ "Print version information and exit.")
parentGroupUUID := flags.String(
"parent-group-uuid",
"",
// 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
"name": cfg.ParentGroupName,
"owner_uuid": cfg.SysUserUUID,
}
- if err := cfg.Client.RequestAndDecode(&parentGroup, "POST", "/arvados/v1/groups", jsonReader("group", groupData), nil); err != nil {
+ if err := CreateGroup(cfg, &parentGroup, groupData); err != nil {
return fmt.Errorf("error creating system user owned group named %q: %s", groupData["name"], err)
}
} else if len(gl.Items) == 1 {
cfg.ParentGroupUUID = parentGroup.UUID
} else {
// UUID provided. Check if exists and if it's owned by system user
- if err := cfg.Client.RequestAndDecode(&parentGroup, "GET", "/arvados/v1/groups/"+cfg.ParentGroupUUID, nil, nil); err != nil {
+ if err := GetGroup(cfg, &parentGroup, cfg.ParentGroupUUID); err != nil {
return fmt.Errorf("error searching for parent group with UUID %q: %s", cfg.ParentGroupUUID, err)
}
if parentGroup.OwnerUUID != cfg.SysUserUUID {
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 {
}
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)
userIDToUUID := make(map[string]string) // Index by email or username
- results, err := ListAll(cfg.Client, "users", arvados.ResourceListParams{}, &UserList{})
+ results, err := GetAll(cfg.Client, "users", arvados.ResourceListParams{}, &UserList{})
if err != nil {
return fmt.Errorf("error getting user list: %s", err)
}
}
// Get remote groups and their members
- remoteGroups := make(map[string]*groupInfo)
- groupNameToUUID := make(map[string]string) // Index by group name
+ remoteGroups, groupNameToUUID, err := GetRemoteGroups(cfg, allUsers)
+ if err != nil {
+ return err
+ }
+ log.Printf("Found %d remote groups", len(remoteGroups))
+
+ membershipsRemoved := 0
+
+ // Read the CSV file
+ groupsCreated, membershipsAdded, membershipsSkipped, err := ProcessFile(cfg, f, userIDToUUID, groupNameToUUID, remoteGroups, allUsers)
+ if err != nil {
+ return err
+ }
+
+ // Remove previous members not listed on this run
+ for groupUUID := range remoteGroups {
+ gi := remoteGroups[groupUUID]
+ evictedMembers := subtract(gi.PreviousMembers, gi.CurrentMembers)
+ groupName := gi.Group.Name
+ if len(evictedMembers) > 0 {
+ 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 {
+ return err
+ }
+ membershipsRemoved++
+ }
+ }
+ log.Printf("Groups created: %d. Memberships added: %d, removed: %d, skipped: %d", groupsCreated, membershipsAdded, membershipsRemoved, membershipsSkipped)
+
+ return nil
+}
+
+// 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) {
+ 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 parsing %q, line %d", cfg.Path, lineNo)
+ return
+ }
+ groupName := strings.TrimSpace(record[0])
+ groupMember := strings.TrimSpace(record[1]) // User ID (username or email)
+ if groupName == "" || groupMember == "" {
+ log.Printf("Warning: CSV record has at least one empty field (%s, %s). Skipping", groupName, groupMember)
+ membersSkipped++
+ continue
+ }
+ if _, found := userIDToUUID[groupMember]; !found {
+ // User not present on the system, skip.
+ log.Printf("Warning: there's no user with %s %q on the system, skipping.", cfg.UserID, groupMember)
+ membersSkipped++
+ continue
+ }
+ if _, found := groupNameToUUID[groupName]; !found {
+ // Group doesn't exist, create it before continuing
+ if cfg.Verbose {
+ log.Printf("Remote group %q not found, creating...", groupName)
+ }
+ var newGroup arvados.Group
+ groupData := map[string]string{
+ "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)
+ return
+ }
+ // Update cached group data
+ groupNameToUUID[groupName] = newGroup.UUID
+ remoteGroups[newGroup.UUID] = &GroupInfo{
+ Group: newGroup,
+ PreviousMembers: make(map[string]bool), // Empty set
+ CurrentMembers: make(map[string]bool), // Empty set
+ }
+ groupsCreated++
+ }
+ // Both group & user exist, check if user is a member
+ groupUUID := groupNameToUUID[groupName]
+ gi := remoteGroups[groupUUID]
+ if !gi.PreviousMembers[groupMember] && !gi.CurrentMembers[groupMember] {
+ if cfg.Verbose {
+ log.Printf("Adding %q to group %q", groupMember, groupName)
+ }
+ // User wasn't a member, but should be.
+ if e := AddMemberToGroup(cfg, allUsers[userIDToUUID[groupMember]], gi.Group); e != nil {
+ err = e
+ return
+ }
+ membersAdded++
+ }
+ gi.CurrentMembers[groupMember] = true
+ }
+ return
+}
+
+// GetAll : Adds all objects of type 'resource' to the 'allItems' list
+func GetAll(c *arvados.Client, res string, params arvados.ResourceListParams, page resourceList) (allItems []interface{}, err error) {
+ // Use the maximum page size the server allows
+ limit := 1<<31 - 1
+ params.Limit = &limit
+ params.Offset = 0
+ params.Order = "uuid"
+ for {
+ if err = GetResourceList(c, &page, res, params); err != nil {
+ return allItems, err
+ }
+ // Have we finished paging?
+ if page.Len() == 0 {
+ break
+ }
+ for _, i := range page.GetItems() {
+ allItems = append(allItems, i)
+ }
+ params.Offset += page.Len()
+ }
+ return allItems, nil
+}
+
+func subtract(setA map[string]bool, setB map[string]bool) map[string]bool {
+ result := make(map[string]bool)
+ for element := range setA {
+ if !setB[element] {
+ result[element] = true
+ }
+ }
+ return result
+}
+
+func jsonReader(rscName string, ob interface{}) io.Reader {
+ j, err := json.Marshal(ob)
+ if err != nil {
+ panic(err)
+ }
+ v := url.Values{}
+ v[rscName] = []string{string(j)}
+ return bytes.NewBufferString(v.Encode())
+}
+
+// GetRemoteGroups fetches all remote groups with their members
+func GetRemoteGroups(cfg *ConfigParams, allUsers map[string]arvados.User) (remoteGroups map[string]*GroupInfo, groupNameToUUID map[string]string, err error) {
+ remoteGroups = make(map[string]*GroupInfo)
+ groupNameToUUID = make(map[string]string) // Index by group name
+
params := arvados.ResourceListParams{
Filters: []arvados.Filter{{
Attr: "owner_uuid",
Operand: cfg.ParentGroupUUID,
}},
}
- results, err = ListAll(cfg.Client, "groups", params, &GroupList{})
+ results, err := GetAll(cfg.Client, "groups", params, &GroupList{})
if err != nil {
- return fmt.Errorf("error getting remote groups: %s", err)
+ return remoteGroups, groupNameToUUID, fmt.Errorf("error getting remote groups: %s", err)
}
for _, item := range results {
group := item.(arvados.Group)
}, {
Attr: "name",
Operator: "=",
- Operand: "manage",
+ Operand: "can_write",
}, {
Attr: "head_uuid",
Operator: "=",
Operand: "arvados#user",
}},
}
- g2uLinks, err := ListAll(cfg.Client, "links", g2uFilter, &linkList{})
+ g2uLinks, err := GetAll(cfg.Client, "links", g2uFilter, &LinkList{})
if err != nil {
- return fmt.Errorf("error getting member (can_read) links for group %q: %s", group.Name, err)
+ return remoteGroups, groupNameToUUID, fmt.Errorf("error getting member (can_read) links for group %q: %s", group.Name, err)
}
- u2gLinks, err := ListAll(cfg.Client, "links", u2gFilter, &linkList{})
+ u2gLinks, err := GetAll(cfg.Client, "links", u2gFilter, &LinkList{})
if err != nil {
- return fmt.Errorf("error getting member (manage) links for group %q: %s", group.Name, err)
+ return remoteGroups, groupNameToUUID, fmt.Errorf("error getting member (can_write) links for group %q: %s", group.Name, err)
}
// Build a list of user ids (email or username) belonging to this group
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
}
// The matching User -> Group link may not exist if the link
// creation failed on a previous run. If that's the case, don't
- // include this account on the previous members list.
+ // include this account on the "previous members" list.
if _, found := u2gLinkSet[link.HeadUUID]; !found {
continue
}
memberID, err := GetUserID(allUsers[link.HeadUUID], cfg.UserID)
if err != nil {
- return err
+ return remoteGroups, groupNameToUUID, err
}
membersSet[memberID] = true
}
- remoteGroups[group.UUID] = &groupInfo{
+ remoteGroups[group.UUID] = &GroupInfo{
Group: group,
PreviousMembers: membersSet,
CurrentMembers: make(map[string]bool), // Empty set
}
groupNameToUUID[group.Name] = group.UUID
}
- log.Printf("Found %d remote groups", len(remoteGroups))
-
- groupsCreated := 0
- membershipsAdded := 0
- membershipsRemoved := 0
- membershipsSkipped := 0
+ return remoteGroups, groupNameToUUID, nil
+}
- csvReader := csv.NewReader(f)
- for {
- record, err := csvReader.Read()
- if err == io.EOF {
- break
- }
+// RemoveMemberFromGroup remove all links related to the membership
+func RemoveMemberFromGroup(cfg *ConfigParams, user arvados.User, group arvados.Group) error {
+ if cfg.Verbose {
+ log.Printf("Getting group membership links for user %q (%s) on group %q (%s)", user.Email, user.UUID, group.Name, group.UUID)
+ }
+ var links []interface{}
+ // Search for all group<->user links (both ways)
+ for _, filterset := range [][]arvados.Filter{
+ // Group -> User
+ {{
+ Attr: "link_class",
+ Operator: "=",
+ Operand: "permission",
+ }, {
+ Attr: "tail_uuid",
+ Operator: "=",
+ Operand: group.UUID,
+ }, {
+ Attr: "head_uuid",
+ Operator: "=",
+ Operand: user.UUID,
+ }},
+ // Group <- User
+ {{
+ Attr: "link_class",
+ Operator: "=",
+ Operand: "permission",
+ }, {
+ Attr: "tail_uuid",
+ Operator: "=",
+ Operand: user.UUID,
+ }, {
+ Attr: "head_uuid",
+ Operator: "=",
+ Operand: group.UUID,
+ }},
+ } {
+ l, err := GetAll(cfg.Client, "links", arvados.ResourceListParams{Filters: filterset}, &LinkList{})
if err != nil {
- return fmt.Errorf("error reading %q: %s", cfg.Path, err)
- }
- groupName := strings.TrimSpace(record[0])
- groupMember := strings.TrimSpace(record[1]) // User ID (username or email)
- if groupName == "" || groupMember == "" {
- log.Printf("Warning: CSV record has at least one empty field (%s, %s). Skipping", groupName, groupMember)
- membershipsSkipped++
- continue
+ userID, _ := GetUserID(user, cfg.UserID)
+ return fmt.Errorf("error getting links needed to remove user %q from group %q: %s", userID, group.Name, err)
}
- if _, found := userIDToUUID[groupMember]; !found {
- // User not present on the system, skip.
- log.Printf("Warning: there's no user with %s %q on the system, skipping.", cfg.UserID, groupMember)
- membershipsSkipped++
- continue
- }
- if _, found := groupNameToUUID[groupName]; !found {
- // Group doesn't exist, create it before continuing
- if cfg.Verbose {
- log.Printf("Remote group %q not found, creating...", groupName)
- }
- var newGroup arvados.Group
- groupData := map[string]string{
- "name": groupName,
- "owner_uuid": cfg.ParentGroupUUID,
- }
- if err := cfg.Client.RequestAndDecode(&newGroup, "POST", "/arvados/v1/groups", jsonReader("group", groupData), nil); err != nil {
- return fmt.Errorf("error creating group named %q: %s", groupName, err)
- }
- // Update cached group data
- groupNameToUUID[groupName] = newGroup.UUID
- remoteGroups[newGroup.UUID] = &groupInfo{
- Group: newGroup,
- PreviousMembers: make(map[string]bool), // Empty set
- CurrentMembers: make(map[string]bool), // Empty set
- }
- groupsCreated++
- }
- // Both group & user exist, check if user is a member
- groupUUID := groupNameToUUID[groupName]
- gi := remoteGroups[groupUUID]
- if !gi.PreviousMembers[groupMember] && !gi.CurrentMembers[groupMember] {
- if cfg.Verbose {
- log.Printf("Adding %q to group %q", groupMember, groupName)
- }
- // User wasn't a member, but should be.
- var newLink Link
- linkData := map[string]string{
- "owner_uuid": cfg.SysUserUUID,
- "link_class": "permission",
- "name": "can_read",
- "tail_uuid": groupUUID,
- "head_uuid": userIDToUUID[groupMember],
- }
- if err := cfg.Client.RequestAndDecode(&newLink, "POST", "/arvados/v1/links", jsonReader("link", linkData), nil); err != nil {
- return fmt.Errorf("error adding group %q -> user %q read permission: %s", groupName, groupMember, err)
- }
- linkData = map[string]string{
- "owner_uuid": cfg.SysUserUUID,
- "link_class": "permission",
- "name": "manage",
- "tail_uuid": userIDToUUID[groupMember],
- "head_uuid": groupUUID,
- }
- if err = cfg.Client.RequestAndDecode(&newLink, "POST", "/arvados/v1/links", jsonReader("link", linkData), nil); err != nil {
- return fmt.Errorf("error adding user %q -> group %q manage permission: %s", groupMember, groupName, err)
- }
- membershipsAdded++
+ for _, link := range l {
+ links = append(links, link)
}
- gi.CurrentMembers[groupMember] = true
}
-
- // Remove previous members not listed on this run
- for groupUUID := range remoteGroups {
- gi := remoteGroups[groupUUID]
- evictedMembers := subtract(gi.PreviousMembers, gi.CurrentMembers)
- groupName := gi.Group.Name
- if len(evictedMembers) > 0 {
- log.Printf("Removing %d users from group %q", len(evictedMembers), groupName)
+ for _, item := range links {
+ 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, userID, group.Name)
}
- for evictedUser := range evictedMembers {
- if cfg.Verbose {
- log.Printf("Getting group membership links for user %q (%s) on group %q (%s)", evictedUser, userIDToUUID[evictedUser], groupName, groupUUID)
- }
- var links []interface{}
- // Search for all group<->user links (both ways)
- for _, filterset := range [][]arvados.Filter{
- // Group -> User
- {{
- Attr: "link_class",
- Operator: "=",
- Operand: "permission",
- }, {
- Attr: "tail_uuid",
- Operator: "=",
- Operand: groupUUID,
- }, {
- Attr: "head_uuid",
- Operator: "=",
- Operand: userIDToUUID[evictedUser],
- }},
- // Group <- User
- {{
- Attr: "link_class",
- Operator: "=",
- Operand: "permission",
- }, {
- Attr: "tail_uuid",
- Operator: "=",
- Operand: userIDToUUID[evictedUser],
- }, {
- Attr: "head_uuid",
- Operator: "=",
- Operand: groupUUID,
- }},
- } {
- l, err := ListAll(cfg.Client, "links", arvados.ResourceListParams{Filters: filterset}, &linkList{})
- if err != nil {
- return fmt.Errorf("error getting links needed to remove user %q from group %q: %s", evictedUser, groupName, err)
- }
- for _, link := range l {
- links = append(links, link)
- }
- }
- for _, item := range links {
- link := item.(Link)
- if cfg.Verbose {
- log.Printf("Removing permission link for %q on group %q", evictedUser, gi.Group.Name)
- }
- if err := cfg.Client.RequestAndDecode(&link, "DELETE", "/arvados/v1/links/"+link.UUID, nil, nil); err != nil {
- return fmt.Errorf("error removing user %q from group %q: %s", evictedUser, groupName, err)
- }
- }
- membershipsRemoved++
+ if err := DeleteLink(cfg, link.UUID); err != nil {
+ return fmt.Errorf("error removing user %q from group %q: %s", userID, group.Name, err)
}
}
- log.Printf("Groups created: %d. Memberships added: %d, removed: %d, skipped: %d", groupsCreated, membershipsAdded, membershipsRemoved, membershipsSkipped)
-
return nil
}
-// ListAll : Adds all objects of type 'resource' to the 'allItems' list
-func ListAll(c *arvados.Client, res string, params arvados.ResourceListParams, page resourceList) (allItems []interface{}, err error) {
- // Use the maximum page size the server allows
- limit := 1<<31 - 1
- params.Limit = &limit
- params.Offset = 0
- params.Order = "uuid"
- for {
- if err = c.RequestAndDecode(&page, "GET", "/arvados/v1/"+res, nil, params); err != nil {
- return allItems, err
- }
- // Have we finished paging?
- if page.Len() == 0 {
- break
- }
- for _, i := range page.GetItems() {
- allItems = append(allItems, i)
- }
- params.Offset += page.Len()
+// AddMemberToGroup create membership links
+func AddMemberToGroup(cfg *ConfigParams, user arvados.User, group arvados.Group) error {
+ var newLink arvados.Link
+ linkData := map[string]string{
+ "owner_uuid": cfg.SysUserUUID,
+ "link_class": "permission",
+ "name": "can_read",
+ "tail_uuid": group.UUID,
+ "head_uuid": user.UUID,
}
- return allItems, nil
+ 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", group.Name, userID, err)
+ }
+ linkData = map[string]string{
+ "owner_uuid": cfg.SysUserUUID,
+ "link_class": "permission",
+ "name": "can_write",
+ "tail_uuid": user.UUID,
+ "head_uuid": group.UUID,
+ }
+ if err := CreateLink(cfg, &newLink, linkData); err != nil {
+ userID, _ := GetUserID(user, cfg.UserID)
+ return fmt.Errorf("error adding user %q -> group %q write permission: %s", userID, group.Name, err)
+ }
+ return nil
}
-func subtract(setA map[string]bool, setB map[string]bool) map[string]bool {
- result := make(map[string]bool)
- for element := range setA {
- if !setB[element] {
- result[element] = true
- }
- }
- return result
+// CreateGroup creates a group with groupData parameters, assigns it to dst
+func CreateGroup(cfg *ConfigParams, dst *arvados.Group, groupData map[string]string) error {
+ return cfg.Client.RequestAndDecode(dst, "POST", "/arvados/v1/groups", jsonReader("group", groupData), nil)
}
-func jsonReader(rscName string, ob interface{}) io.Reader {
- j, err := json.Marshal(ob)
- if err != nil {
- panic(err)
+// GetGroup fetches a group by its UUID
+func GetGroup(cfg *ConfigParams, dst *arvados.Group, groupUUID string) error {
+ return cfg.Client.RequestAndDecode(&dst, "GET", "/arvados/v1/groups/"+groupUUID, nil, nil)
+}
+
+// CreateLink creates a link with linkData parameters, assigns it to dst
+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)
+}
+
+// DeleteLink deletes a link by its UUID
+func DeleteLink(cfg *ConfigParams, linkUUID string) error {
+ if linkUUID == "" {
+ return fmt.Errorf("cannot delete link with invalid UUID: %q", linkUUID)
}
- v := url.Values{}
- v[rscName] = []string{string(j)}
- return bytes.NewBufferString(v.Encode())
+ return cfg.Client.RequestAndDecode(&arvados.Link{}, "DELETE", "/arvados/v1/links/"+linkUUID, nil, nil)
+}
+
+// GetResourceList fetches res list using params
+func GetResourceList(c *arvados.Client, dst *resourceList, res string, params interface{}) error {
+ return c.RequestAndDecode(dst, "GET", "/arvados/v1/"+res, nil, params)
}