Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add data source github_users #900

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions github/data_source_github_users.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package github

import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/shurcooL/githubv4"
"log"
"reflect"
"strings"
)

func dataSourceGithubUsers() *schema.Resource {
return &schema.Resource{
Read: dataSourceGithubUsersRead,

Schema: map[string]*schema.Schema{
"usernames": {
Type: schema.TypeList,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Required: true,
},
"logins": {
Type: schema.TypeList,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Computed: true,
},
"node_ids": {
Type: schema.TypeList,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Computed: true,
},
"unknown_logins": {
Type: schema.TypeList,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Computed: true,
},
},
}
}

func dataSourceGithubUsersRead(d *schema.ResourceData, meta interface{}) error {
usernames := expandStringList(d.Get("usernames").([]interface{}))

// Create GraphQL variables and query struct
type (
UserFragment struct {
Id string
Login string
}
)
var fields []reflect.StructField
variables := make(map[string]interface{})
for idx, username := range usernames {
label := fmt.Sprintf("User%d", idx)
variables[label] = githubv4.String(username)
fields = append(fields, reflect.StructField{
Name: label, Type: reflect.TypeOf(UserFragment{}), Tag: reflect.StructTag(fmt.Sprintf("graphql:\"%[1]s: user(login: $%[1]s)\"", label)),
})
}
query := reflect.New(reflect.StructOf(fields)).Elem()

if len(usernames) > 0 {
log.Printf("[INFO] Refreshing GitHub Users: %s", strings.Join(usernames, ", "))
ctx := context.WithValue(context.Background(), ctxId, d.Id())
client := meta.(*Owner).v4client
err := client.Query(ctx, query.Addr().Interface(), variables)
if err != nil && !strings.Contains(err.Error(), "Could not resolve to a User with the login of") {
return err
}
}

var logins, nodeIDs, unknownLogins []string
for idx, username := range usernames {
label := fmt.Sprintf("User%d", idx)
user := query.FieldByName(label).Interface().(UserFragment)
if user.Login != "" {
logins = append(logins, user.Login)
nodeIDs = append(nodeIDs, user.Id)
} else {
unknownLogins = append(unknownLogins, username)
}
}

d.SetId(buildChecksumID(usernames))
d.Set("logins", logins)
d.Set("node_ids", nodeIDs)
d.Set("unknown_logins", unknownLogins)

return nil
}
95 changes: 95 additions & 0 deletions github/data_source_github_users_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package github

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
)

func TestAccGithubUsersDataSource(t *testing.T) {

t.Run("queries multiple accounts", func(t *testing.T) {

config := fmt.Sprintf(`
data "github_users" "test" {
usernames = ["%[1]s", "!%[1]s"]
}
`, testOwnerFunc())

check := resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.github_users.test", "logins.#", "1"),
resource.TestCheckResourceAttr("data.github_users.test", "logins.0", testOwnerFunc()),
resource.TestCheckResourceAttr("data.github_users.test", "node_ids.#", "1"),
resource.TestCheckResourceAttr("data.github_users.test", "unknown_logins.#", "1"),
resource.TestCheckResourceAttr("data.github_users.test", "unknown_logins.0", fmt.Sprintf("!%s", testOwnerFunc())),
)

testCase := func(t *testing.T, mode string) {
resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessMode(t, mode) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: check,
},
},
})
}

t.Run("with an anonymous account", func(t *testing.T) {
t.Skip("anonymous account not supported for this operation")
})

t.Run("with an individual account", func(t *testing.T) {
testCase(t, individual)
})

t.Run("with an organization account", func(t *testing.T) {
testCase(t, organization)
})

})

t.Run("does not fail if called with empty list of usernames", func(t *testing.T) {

config := `
data "github_users" "test" {
usernames = []
}
`

check := resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.github_users.test", "logins.#", "0"),
resource.TestCheckResourceAttr("data.github_users.test", "node_ids.#", "0"),
resource.TestCheckResourceAttr("data.github_users.test", "unknown_logins.#", "0"),
)

testCase := func(t *testing.T, mode string) {
resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessMode(t, mode) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: check,
},
},
})
}

t.Run("with an anonymous account", func(t *testing.T) {
t.Skip("anonymous account not supported for this operation")
})

t.Run("with an individual account", func(t *testing.T) {
testCase(t, individual)
})

t.Run("with an organization account", func(t *testing.T) {
testCase(t, organization)
})

})
}
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func Provider() terraform.ResourceProvider {
"github_repository_pull_requests": dataSourceGithubRepositoryPullRequests(),
"github_team": dataSourceGithubTeam(),
"github_user": dataSourceGithubUser(),
"github_users": dataSourceGithubUsers(),
},
}

Expand Down
13 changes: 13 additions & 0 deletions github/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package github

import (
"context"
"crypto/md5"
"errors"
"fmt"
"regexp"
"sort"
"strconv"
"strings"

Expand Down Expand Up @@ -78,6 +80,17 @@ func buildThreePartID(a, b, c string) string {
return fmt.Sprintf("%s:%s:%s", a, b, c)
}

func buildChecksumID(v []string) string {
sort.Strings(v)

h := md5.New()
// Hash.Write never returns an error. See https://pkg.go.dev/hash#Hash
_, _ = h.Write([]byte(strings.Join(v, "")))
bs := h.Sum(nil)

return fmt.Sprintf("%x", bs)
}

func expandStringList(configured []interface{}) []string {
vs := make([]string, 0, len(configured))
for _, v := range configured {
Expand Down
37 changes: 37 additions & 0 deletions website/docs/d/users.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
layout: "github"
page_title: "GitHub: github_users"
description: |-
Get information about multiple GitHub users.
---

# github\_users

Use this data source to retrieve information about multiple GitHub users at once.

## Example Usage

```hcl
# Retrieve information about multiple GitHub users.
data "github_users" "example" {
usernames = ["example1", "example2", "example3"]
}
output "valid_users" {
value = "${data.github_user.example.logins}"
}
output "invalid_users" {
value = "${data.github_user.example.unknown_logins}"
}
```

## Argument Reference

* `usernames` - (Required) List of usernames.

## Attributes Reference

* `node_ids` - list of Node IDs of users that could be found.
* `logins` - list of logins of users that could be found.
* `unknown_logins` - list of logins without matching user.
3 changes: 3 additions & 0 deletions website/github.erb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
<li>
<a href="/docs/providers/github/d/user.html">github_user</a>
</li>
<li>
<a href="/docs/providers/github/d/users.html">github_users</a>
</li>
</ul>
</li>

Expand Down