[x/go.dev] cmd/events: add utility for updating event content

Adds a command to update event.yaml for rendering events in our Home and
Learn pages. Times are rendered in the local time for the event.
X-GoDev-Commit: 1e492202bb964c17b8a087c54019da84ff7ed3f2
diff --git a/go.dev/cmd/events/main.go b/go.dev/cmd/events/main.go
new file mode 100644
index 0000000..e2e9711
--- /dev/null
+++ b/go.dev/cmd/events/main.go
@@ -0,0 +1,302 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"path"
+	"strings"
+	"time"
+
+	"github.com/microcosm-cc/bluemonday"
+	"gopkg.in/yaml.v2"
+)
+
+const (
+	// eventLimit is the maximum number of events that will be output.
+	eventLimit = 15
+	// groupsSummaryPath is an API endpoint that returns global Go groups.
+	// Fetching from this API path allows to sort groups by next upcoming event.
+	groupsSummaryPath = "/pro/go/es_groups_summary?location=global&order=next_event&desc=false"
+	// eventsHeader is a header comment for the output content.
+	eventsHeader = `# DO NOT EDIT: Autogenerated from cmd/events.
+# To update, run:
+#    go run github.com/godevsite/go.dev/cmd/events > data/events.yaml`
+)
+
+func main() {
+	c := &meetupAPI{
+		baseURL: "https://api.meetup.com",
+	}
+	ue, err := getUpcomingEvents(c)
+	if err != nil {
+		log.Fatal(err)
+	}
+	printYAML(ue)
+}
+
+type client interface {
+	getGroupsSummary() (*GroupsSummary, error)
+	getGroup(urlName string) (*Group, error)
+}
+
+// getUpcomingEvents returns upcoming events globally.
+func getUpcomingEvents(c client) (*UpcomingEvents, error) {
+	summary, err := c.getGroupsSummary()
+	if err != nil {
+		return nil, err
+	}
+	p := bluemonday.NewPolicy()
+	p.AllowStandardURLs()
+	p.AllowAttrs("href").OnElements("a")
+	p.AllowElements("br")
+	// Work around messy newlines in content.
+	r := strings.NewReplacer("\n", "<br/>\n", "&lt;br&gt;", "<br/>\n")
+	var events []EventData
+	for _, chapter := range summary.Chapters {
+		if len(events) >= eventLimit {
+			break
+		}
+		group, err := c.getGroup(chapter.URLName)
+		if err != nil || group.NextEvent == nil {
+			continue
+		}
+		tz, err := time.LoadLocation(group.Timezone)
+		if err != nil {
+			tz = time.UTC
+		}
+		// group.NextEvent.Time is in milliseconds since UTC epoch.
+		nextEventTime := time.Unix(group.NextEvent.Time/1000, 0).In(tz)
+		events = append(events, EventData{
+			City:              chapter.City,
+			Country:           chapter.Country,
+			Description:       r.Replace(p.Sanitize(chapter.Description)), // Event descriptions are often blank, use Group description.
+			ID:                group.NextEvent.ID,
+			LocalDate:         nextEventTime.Format("Jan 2, 2006"),
+			LocalTime:         nextEventTime.Format(time.RFC3339),
+			LocalizedCountry:  group.LocalizedCountryName,
+			LocalizedLocation: group.LocalizedLocation,
+			Name:              group.NextEvent.Name,
+			State:             chapter.State,
+			ThumbnailURL:      chapter.GroupPhoto.ThumbLink,
+			URL:               "https://www.meetup.com/" + path.Join(chapter.URLName, "events", group.NextEvent.ID),
+		})
+	}
+	return &UpcomingEvents{All: events}, nil
+}
+
+type meetupAPI struct {
+	baseURL string
+}
+
+func (c *meetupAPI) getGroupsSummary() (*GroupsSummary, error) {
+	resp, err := http.Get(c.baseURL + groupsSummaryPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get events from %q: %v", groupsSummaryPath, err)
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("failed to get events from %q: %v", groupsSummaryPath, resp.Status)
+	}
+	var summary *GroupsSummary
+	d := json.NewDecoder(resp.Body)
+	if err := d.Decode(&summary); err != nil {
+		return summary, fmt.Errorf("failed to decode events from %q: %w", groupsSummaryPath, err)
+	}
+	return summary, nil
+}
+
+// getGroup fetches group details, which are useful for getting details of the next upcoming event, and timezones.
+func (c *meetupAPI) getGroup(urlName string) (*Group, error) {
+	u := c.baseURL + "/" + urlName
+	resp, err := http.Get(u)
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch group details from %q: %w", u, err)
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("failed to fetch group details from %q: %v", u, resp.Status)
+	}
+
+	var group Group
+	d := json.NewDecoder(resp.Body)
+	if err := d.Decode(&group); err != nil {
+		return nil, fmt.Errorf("failed to decode group from %q: %w", u, err)
+	}
+	return &group, nil
+}
+
+func printYAML(v interface{}) {
+	fmt.Println(eventsHeader)
+	e := yaml.NewEncoder(os.Stdout)
+	defer e.Close()
+	if err := e.Encode(v); err != nil {
+		log.Fatalf("failed to encode event yaml: %v", err)
+	}
+}
+
+// UpcomingEvents is a list of EventData written out to YAML for rendering in Hugo.
+type UpcomingEvents struct {
+	All []EventData
+}
+
+// EventData is the structure written out to YAML for rendering in Hugo.
+type EventData struct {
+	City              string
+	Country           string
+	Description       string
+	ID                string
+	LocalDate         string `yaml:"local_date"`
+	LocalTime         string `yaml:"local_time"`
+	LocalizedCountry  string
+	LocalizedLocation string
+	Name              string
+	State             string
+	ThumbnailURL      string
+	URL               string
+}
+
+// GroupsSummary is the structure returned from /pro/go/es_groups_summary.
+type GroupsSummary struct {
+	Chapters []*Chapter
+}
+
+type Event struct {
+	Created       int    `json:"created"`
+	Description   string `json:"description"`
+	Duration      int    `json:"duration"`
+	Fee           *Fee   `json:"fee"`
+	Group         *Group `json:"group"`
+	LocalDate     string `json:"local_date"`
+	LocalTime     string `json:"local_time"`
+	ID            string `json:"id"`
+	Link          string `json:"link"`
+	Name          string `json:"name"`
+	RSVPLimit     int    `json:"rsvp_limit"`
+	Status        string `json:"status"`
+	Time          int64  `json:"time"`
+	UTCOffset     int    `json:"utc_offset"`
+	Updated       int    `json:"updated"`
+	Venue         *Venue `json:"venue"`
+	WaitlistCount int    `json:"waitlist_count"`
+	YesRSVPCount  int    `json:"yes_rsvp_count"`
+}
+
+type Venue struct {
+	Address1             string  `json:"address_1"`
+	Address2             string  `json:"address_2"`
+	Address3             string  `json:"address_3"`
+	City                 string  `json:"city"`
+	Country              string  `json:"country"`
+	ID                   int     `json:"id"`
+	Lat                  float64 `json:"lat"`
+	LocalizedCountryName string  `json:"localized_country_name"`
+	Lon                  float64 `json:"lon"`
+	Name                 string  `json:"name"`
+	Repinned             bool    `json:"repinned"`
+	State                string  `json:"state"`
+	Zip                  string  `json:"zip"`
+}
+
+type Group struct {
+	Country              string  `json:"country"`
+	Created              int     `json:"created"`
+	Description          string  `json:"description"`
+	ID                   int     `json:"id"`
+	JoinMode             string  `json:"join_mode"`
+	Lat                  float64 `json:"lat"`
+	LocalizedLocation    string  `json:"localized_location"`
+	LocalizedCountryName string  `json:"localized_country_name"`
+	Lon                  float64 `json:"lon"`
+	Name                 string  `json:"name"`
+	NextEvent            *Event  `json:"next_event"`
+	Region               string  `json:"region"`
+	Timezone             string  `json:"timezone"`
+	URLName              string  `json:"urlname"`
+	Who                  string  `json:"who"`
+}
+
+type Fee struct {
+	Accepts     string  `json:"accepts"`
+	Amount      float64 `json:"amount"`
+	Currency    string  `json:"currency"`
+	Description string  `json:"description"`
+	Label       string  `json:"label"`
+	Required    bool    `json:"required"`
+}
+
+type Chapter struct {
+	AverageAge     float64        `json:"average_age"`
+	Category       []Category     `json:"category"`
+	City           string         `json:"city"`
+	Country        string         `json:"country"`
+	Description    string         `json:"description"`
+	FoundedDate    int64          `json:"founded_date"`
+	GenderFemale   float64        `json:"gender_female"`
+	GenderMale     float64        `json:"gender_male"`
+	GenderOther    float64        `json:"gender_other"`
+	GenderUnknown  float64        `json:"gender_unknown"`
+	GroupPhoto     GroupPhoto     `json:"group_photo"`
+	ID             int            `json:"id"`
+	LastEvent      int64          `json:"last_event"`
+	Lat            float64        `json:"lat"`
+	Lon            float64        `json:"lon"`
+	MemberCount    int            `json:"member_count"`
+	Name           string         `json:"name"`
+	NextEvent      int64          `json:"next_event"`
+	OrganizerPhoto OrganizerPhoto `json:"organizer_photo"`
+	Organizers     []Organizer    `json:"organizers"`
+	PastEvents     int            `json:"past_events"`
+	PastRSVPs      int            `json:"past_rsvps"`
+	ProJoinDate    int64          `json:"pro_join_date"`
+	RSVPsPerEvent  float64        `json:"rsvps_per_event"`
+	RepeatRSVPers  int            `json:"repeat_rsvpers"`
+	State          string         `json:"state"`
+	Status         string         `json:"status"`
+	Topics         []Topic        `json:"topics"`
+	URLName        string         `json:"urlname"`
+	UpcomingEvents int            `json:"upcoming_events"`
+}
+
+type Topic struct {
+	ID     int    `json:"id"`
+	Name   string `json:"name"`
+	URLkey string `json:"urlkey"`
+	Lang   string `json:"lang"`
+}
+
+type Category struct {
+	ID        int    `json:"id"`
+	Name      string `json:"name"`
+	Shortname string `json:"shortname"`
+	SortName  string `json:"sort_name"`
+}
+
+type Organizer struct {
+	Name       string `json:"name"`
+	MemberID   int    `json:"member_id"`
+	Permission string `json:"permission"`
+}
+
+type OrganizerPhoto struct {
+	BaseURL     string `json:"base_url"`
+	HighresLink string `json:"highres_link"`
+	ID          int    `json:"id"`
+	PhotoLink   string `json:"photo_link"`
+	ThumbLink   string `json:"thumb_link"`
+	Type        string `json:"type"`
+}
+
+type GroupPhoto struct {
+	BaseURL     string `json:"base_url"`
+	HighresLink string `json:"highres_link"`
+	ID          int    `json:"id"`
+	PhotoLink   string `json:"photo_link"`
+	ThumbLink   string `json:"thumb_link"`
+	Type        string `json:"type"`
+}
diff --git a/go.dev/cmd/events/main_test.go b/go.dev/cmd/events/main_test.go
new file mode 100644
index 0000000..4f729b2
--- /dev/null
+++ b/go.dev/cmd/events/main_test.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+var (
+	noEventGroup       = &Group{Name: "no event group"}
+	upcomingEventGroup = &Group{
+		Name: "Upcoming Event Group",
+		Timezone: "Europe/Oslo",
+		NextEvent: &Event{
+			ID:            "12345",
+			Name:          "Upcoming Event",
+			Time:          1262976000000,
+		},
+	}
+	fakeGroups = map[string]*Group{
+		"noEvent": noEventGroup,
+		"ueg":     upcomingEventGroup,
+	}
+)
+
+type fakeClient struct{}
+
+func (f fakeClient) getGroupsSummary() (*GroupsSummary, error) {
+	return &GroupsSummary{Chapters: []*Chapter{
+		{URLName: "noEvent"},
+		{
+			URLName: "ueg",
+			Description: "We host our own events\n",
+		},
+	}}, nil
+}
+
+func (f fakeClient) getGroup(urlName string) (*Group, error) {
+	g, ok := fakeGroups[urlName]
+	if !ok {
+		return nil, fmt.Errorf("no group %q", urlName)
+	}
+	return g, nil
+}
+
+func TestGetUpcomingEvents(t *testing.T) {
+	want := &UpcomingEvents{All: []EventData{
+		{
+			Name:        "Upcoming Event",
+			ID:          "12345",
+			Description: "We host our own events<br/>\n",
+			LocalDate:   "Jan 8, 2010",
+			LocalTime:   "2010-01-08T19:40:00+01:00",
+			URL:         "https://www.meetup.com/ueg/events/12345",
+		},
+	}}
+	f := fakeClient{}
+	got, err := getUpcomingEvents(f)
+	if err != nil {
+		t.Fatalf("getUpcomingEvents(%v) error = %v, wanted no error", f, err)
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("getUpcomingEvents(%v) mismatch (-want +got):\n%s", f, diff)
+	}
+}
diff --git a/go.dev/data/events.yaml b/go.dev/data/events.yaml
index 53b145a..ba9ac3c 100644
--- a/go.dev/data/events.yaml
+++ b/go.dev/data/events.yaml
@@ -1,66 +1,7 @@
 # DO NOT EDIT: Autogenerated from cmd/events.
+# To update, run:
+#    go run github.com/godevsite/go.dev/cmd/events > data/events.yaml
 all:
-- city: Pune
-  country: India
-  description: |-
-    WWG Pune is a chapter of Women Who Go.<br/>
-    We built this meetup to provide a better entry point to women who are interested in Go. We also want to provide a safe space for networking, learning and finding mentors. This group shall bring us together so we can continue to grow in Go. <br><br/>
-     <br><br/>
-    Even though any person can attend this meetup regardless of gender, we wish to empower the less-represented developers of the programming community.<br/>
-     <br><br/>
-    We would love for you to speak at Women Who Go! DM us at <a href="https://twitter.com/wwg_pune" rel="nofollow">@wwg_pune</a> about a short talk, demo, or whatever you have in mind!
-  id: "265180374"
-  local_date: Sep 28, 2019
-  local_time: "2019-09-28T10:00:00+05:30"
-  localizedcountry: India
-  localizedlocation: Pune, India
-  name: Learning Golang Part 2 - Mastering the basics of Go
-  state: ""
-  thumbnailurl: ""
-  url: https://www.meetup.com/wwg_pune/events/265180374
-- city: Paris
-  country: France
-  description: |-
-    This is the Paris Chapter of Women Who Go: <a href="http://www.meetup.com/Women-Who-Go/" rel="nofollow">http://www.meetup.com/Women-Who-Go/</a> This group is an inclusive space for everyone.<br/>
-    This group is for you if any of the following: <br>1. You identify as a woman, publicly or privately. 2. You have some interest in the Go programming language, or programming in general.
-  id: "264458143"
-  local_date: Sep 28, 2019
-  local_time: "2019-09-28T10:00:00+02:00"
-  localizedcountry: France
-  localizedlocation: Paris, France
-  name: 'Workshop - Production ready cloud-native services with Go and Kubernetes '
-  state: ""
-  thumbnailurl: https://secure.meetupstatic.com/photos/event/2/d/3/b/thumb_480491579.jpeg
-  url: https://www.meetup.com/Women-Who-Go-Paris/events/264458143
-- city: Chennai
-  country: India
-  description: |-
-    A group for anyone using (want to use) golang for programming and development of software or engineering applications.<br/>
-    We are on slack #chennaigolang
-  id: "264717526"
-  local_date: Sep 28, 2019
-  local_time: "2019-09-28T14:00:00+05:30"
-  localizedcountry: India
-  localizedlocation: Chennai, India
-  name: September Go Meetup
-  state: ""
-  thumbnailurl: https://secure.meetupstatic.com/photos/event/9/1/b/e/thumb_449917310.jpeg
-  url: https://www.meetup.com/Chennai-golang-Meetup/events/264717526
-- city: Delhi
-  country: India
-  description: |-
-    You do not need to know Go, be a professional developer or enjoy talking to strangers in order to come hang out with us! <br/>
-    This group is for you if you are interested in Go, and are looking for a safe space to learn. Membership is restricted to women and gender minorities. <br/>
-    We would love for you to speak at Women Who Go! Contact delhi@womenwhogo.org about a short talk, demo, or whatever you have in mind!
-  id: "265075250"
-  local_date: Sep 29, 2019
-  local_time: "2019-09-29T09:30:00+05:30"
-  localizedcountry: India
-  localizedlocation: Delhi, India
-  name: 'DevFest '
-  state: ""
-  thumbnailurl: ""
-  url: https://www.meetup.com/New-Delhi-Women-Who-Go/events/265075250
 - city: London
   country: United Kingdom
   description: |-
@@ -217,3 +158,92 @@
   state: ""
   thumbnailurl: https://secure.meetupstatic.com/photos/event/e/4/7/e/thumb_455218494.jpeg
   url: https://www.meetup.com/golang-mcr/events/tcljtqyznbmb
+- city: Seattle
+  country: USA
+  description: |-
+    The Seattle Go User Group is a community for anyone interested in the Go programming language. <br><br/>
+    <br><br/>
+    We provide opportunities to:<br/>
+    • Discuss Go and related topics <br/>
+    • Socialize with people who are interested in Go<br/>
+    • Find or fill Go-related jobs <br/>
+    <br><br/>
+    If you want to chat all things Go, feel free to join us on the Gopher slack. <br/>
+    Invites can be found at <a href="https://invite.slack.golangbridge.org" rel="nofollow">https://invite.slack.golangbridge.org</a><br/>
+    There is a #seattle channel which can be joined by anyone, so come say hi!<br/>
+    <br><br/>
+    Our aim is to be a welcoming environment. As such all attendees, organizers and sponsors are required to follow the <a href="https://golang.org/conduct" rel="nofollow">code of conduct</a>.
+  id: dpshmpyznbmb
+  local_date: Oct 9, 2019
+  local_time: "2019-10-09T13:00:00-07:00"
+  localizedcountry: USA
+  localizedlocation: Seattle, WA
+  name: Eastside Go Coffee
+  state: WA
+  thumbnailurl: https://secure.meetupstatic.com/photos/event/a/4/b/e/thumb_450342174.jpeg
+  url: https://www.meetup.com/golang/events/dpshmpyznbmb
+- city: Hannover
+  country: Germany
+  description: |-
+    The place to meet Go(lang) enthusiasts from the Hannover region. Drop by if you are interested in Golang or back end stuff in general :-)<br/>
+     <br><br/>
+    Find recently announced talks at <a href="http://www.golang.wtf/" rel="nofollow">golang.wtf</a>!
+  id: nhvmtqyznbfb
+  local_date: Oct 10, 2019
+  local_time: "2019-10-10T19:00:00+02:00"
+  localizedcountry: Germany
+  localizedlocation: Hannover, Germany
+  name: Hannover Gophers - Vol. 15
+  state: ""
+  thumbnailurl: https://secure.meetupstatic.com/photos/event/a/2/9/2/thumb_475541618.jpeg
+  url: https://www.meetup.com/Hannover-Gophers/events/nhvmtqyznbfb
+- city: Lisbon
+  country: Portugal
+  description: "PortugalGophers is a Portugal based community for anyone interested
+    in the Go programming language.<br/>\n <br>Our get together provides offline opportunities
+    to: <br>✅Discuss Go and related topics <br>✅ Socialise with friendly people who
+    are interested in Go <br>✅ Find or fill Go-related jobs <br/>\n <br><br/>\nAll
+    attendees, organisers and sponsors are required to follow the Go community code
+    of conduct (<a href=\"https://golang.org/conduct\" rel=\"nofollow\">https://golang.org/conduct</a>).
+    <br> <br>If you would like to make a talk in one of our meetups, click <a href=\"http://bit.ly/2Ig1RiV\"
+    rel=\"nofollow\">here</a>.<br/>\n\U0001F4E2<a href=\"https://twitter.com/GophersPortugal\"
+    rel=\"nofollow\">Twitter</a> <br/>\n\U0001F4F9<a href=\"https://www.youtube.com/channel/UCwxeVe6zJSGfOON5C6Wg3Eg/\"
+    rel=\"nofollow\">YouTube</a> <br/>\n <br>"
+  id: "265144357"
+  local_date: Oct 10, 2019
+  local_time: "2019-10-10T19:00:00+01:00"
+  localizedcountry: Portugal
+  localizedlocation: Lisbon, Portugal
+  name: October Gophers with Kat Zień
+  state: ""
+  thumbnailurl: https://secure.meetupstatic.com/photos/event/9/c/f/2/thumb_485200178.jpeg
+  url: https://www.meetup.com/PortugalGophers/events/265144357
+- city: Orlando
+  country: USA
+  description: Orlando&#39;s first meetup group dedicated to the Go Programming Language.
+    All skill levels are welcome - whether you&#39;re a beginner or a full-fledged
+    gopher.
+  id: blgfpqyznbnb
+  local_date: Oct 10, 2019
+  local_time: "2019-10-10T19:00:00-04:00"
+  localizedcountry: USA
+  localizedlocation: Orlando, FL
+  name: Orlando Golang Monthly Meetup
+  state: FL
+  thumbnailurl: https://secure.meetupstatic.com/photos/event/9/7/c/4/thumb_441638852.jpeg
+  url: https://www.meetup.com/OrlanGo/events/blgfpqyznbnb
+- city: Graz
+  country: Austria
+  description: |-
+    Die <a href="https://golang.org" rel="nofollow">Programmiersprache Go</a> hat einen Grazer &#34;Stammtisch&#34;. <br/>
+    Wir treffen uns jeden 2. Montag im Monat bei TAO Digital am Lendplatz.<br/>
+    Schau einfach vorbei oder besuche uns online unter <a href="https://gograz.org" rel="nofollow">GoGraz</a>! <br>
+  id: lbbhjlyznbtb
+  local_date: Oct 14, 2019
+  local_time: "2019-10-14T19:00:00+02:00"
+  localizedcountry: Austria
+  localizedlocation: Graz, Austria
+  name: Go Language Usergroup Graz
+  state: ""
+  thumbnailurl: https://secure.meetupstatic.com/photos/event/3/3/e/0/thumb_459373280.jpeg
+  url: https://www.meetup.com/Graz-Open-Source-Meetup/events/lbbhjlyznbtb
diff --git a/go.dev/go.mod b/go.dev/go.mod
new file mode 100644
index 0000000..de83898
--- /dev/null
+++ b/go.dev/go.mod
@@ -0,0 +1,9 @@
+module github.com/godevsite/go.dev
+
+go 1.13
+
+require (
+	github.com/google/go-cmp v0.3.1
+	github.com/microcosm-cc/bluemonday v1.0.2
+	gopkg.in/yaml.v2 v2.2.2
+)
diff --git a/go.dev/go.sum b/go.dev/go.sum
new file mode 100644
index 0000000..23d783e
--- /dev/null
+++ b/go.dev/go.sum
@@ -0,0 +1,10 @@
+github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
+github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=